From c13a79d67b72adfb342dd30c6e1745114250c5af Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 7 Jul 2023 22:41:37 +0900 Subject: [PATCH 001/146] Update base.txt updated dependency version of datumaro --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 5b06c5d06d6..2dcf5c9ddfd 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro@ git+https://github.com/openvinotoolkit/datumaro@3e77b3138d063db68a4efba3c03a6bac7df086b1#egg=datumaro +datumaro==1.4.0rc1 psutil scipy>=1.8 bayesian-optimization>=1.2.0 From 228a8ede7acc2557b7a17873ff5c76471ea9d16f Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 7 Jul 2023 23:02:35 +0900 Subject: [PATCH 002/146] Update __init__.py update version string --- src/otx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otx/__init__.py b/src/otx/__init__.py index c1da1fa8d2c..884f6b9a96d 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0rc0" +__version__ = "1.4.0rc1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release From 5dfd5fdaa65d46b9374e43b2be5b73990c730c5e Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 7 Jul 2023 23:03:45 +0900 Subject: [PATCH 003/146] Update requirements.txt --- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 54532300161..891041200cb 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.2 -otx @ git+https://github.com/openvinotoolkit/training_extensions/@77b635f4fba0a8acca221ec7e8b1fadd734358da#egg=otx +otx==1.4.0rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From e67d2617e9b044c91a5dd1a57be13d17e8152108 Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Mon, 10 Jul 2023 12:49:54 +0900 Subject: [PATCH 004/146] Temporarily skip visual prompting openvino integration test (#2323) --- tests/integration/cli/visual_prompting/test_visual_prompting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/cli/visual_prompting/test_visual_prompting.py b/tests/integration/cli/visual_prompting/test_visual_prompting.py index 485f9528afb..8fb35794b62 100644 --- a/tests/integration/cli/visual_prompting/test_visual_prompting.py +++ b/tests/integration/cli/visual_prompting/test_visual_prompting.py @@ -99,6 +99,7 @@ def test_otx_export_onnx(self, template, tmp_dir_path): otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) @e2e_pytest_component + @pytest.mark.skip("arguments order of create_model and decoder's input layout will be updated.") @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): From 344f5269b293af3e07150ab4767dd57f47229383 Mon Sep 17 00:00:00 2001 From: Vinnam Kim Date: Mon, 10 Jul 2023 15:25:14 +0900 Subject: [PATCH 005/146] Fix import dm.DatasetSubset (#2324) Signed-off-by: Kim, Vinnam --- tests/unit/core/data/manager/test_dataset_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/core/data/manager/test_dataset_manager.py b/tests/unit/core/data/manager/test_dataset_manager.py index ba1daa768f9..41a84740d95 100644 --- a/tests/unit/core/data/manager/test_dataset_manager.py +++ b/tests/unit/core/data/manager/test_dataset_manager.py @@ -6,9 +6,9 @@ from typing import List from tempfile import TemporaryDirectory -import datumaro as dm 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 @@ -51,7 +51,7 @@ def test_get_train_dataset(self, task: List[str], subset: List[str]): DatasetManager.get_train_dataset(self.dataset[subset][task]) else: train_dataset = DatasetManager.get_train_dataset(self.dataset[subset][task]) - assert isinstance(train_dataset, dm.DatasetSubset) + assert isinstance(train_dataset, DatasetSubset) @e2e_pytest_unit @pytest.mark.parametrize("task", AVAILABLE_TASKS) @@ -61,7 +61,7 @@ def test_get_val_dataset(self, task: List[str], subset: List[str]): 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, dm.DatasetSubset) + assert isinstance(val_dataset, DatasetSubset) @e2e_pytest_unit @pytest.mark.parametrize("data_root", AVAILABLE_DATA_ROOTS) From cfd77068ceb67540df8c04780ecfb10866fc7fb1 Mon Sep 17 00:00:00 2001 From: Evgeny Tsykunov Date: Mon, 10 Jul 2023 10:53:18 +0200 Subject: [PATCH 006/146] Fix semantic segmentation soft prediction dtype (#2322) * Fix semantic segmentation soft prediction dtype * relax ref sal vals check --------- Co-authored-by: Songki Choi --- src/otx/algorithms/segmentation/adapters/openvino/task.py | 2 ++ src/otx/algorithms/segmentation/task.py | 2 ++ .../classification/test_xai_classification_validity.py | 2 +- tests/unit/algorithms/detection/test_xai_detection_validity.py | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/otx/algorithms/segmentation/adapters/openvino/task.py b/src/otx/algorithms/segmentation/adapters/openvino/task.py index 268eb2e933a..270216505c6 100644 --- a/src/otx/algorithms/segmentation/adapters/openvino/task.py +++ b/src/otx/algorithms/segmentation/adapters/openvino/task.py @@ -240,6 +240,8 @@ def add_prediction( 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", diff --git a/src/otx/algorithms/segmentation/task.py b/src/otx/algorithms/segmentation/task.py index a3a8923454b..08860a3e168 100644 --- a/src/otx/algorithms/segmentation/task.py +++ b/src/otx/algorithms/segmentation/task.py @@ -290,6 +290,8 @@ def _add_predictions_to_dataset(self, prediction_results, dataset, dump_soft_pre 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", diff --git a/tests/unit/algorithms/classification/test_xai_classification_validity.py b/tests/unit/algorithms/classification/test_xai_classification_validity.py index 8f15ede6133..1ec20d0c2c1 100644 --- a/tests/unit/algorithms/classification/test_xai_classification_validity.py +++ b/tests/unit/algorithms/classification/test_xai_classification_validity.py @@ -54,4 +54,4 @@ def test_saliency_map_cls(self, template): assert len(saliency_maps) == 2 assert saliency_maps[0].ndim == 3 assert saliency_maps[0].shape == (1000, 7, 7) - assert (saliency_maps[0][0][0] == self.ref_saliency_vals_cls[template.name]).all() + assert np.all(np.abs(saliency_maps[0][0][0] - self.ref_saliency_vals_cls[template.name]) <= 1) diff --git a/tests/unit/algorithms/detection/test_xai_detection_validity.py b/tests/unit/algorithms/detection/test_xai_detection_validity.py index 6ed47397992..3e903a0b5d6 100644 --- a/tests/unit/algorithms/detection/test_xai_detection_validity.py +++ b/tests/unit/algorithms/detection/test_xai_detection_validity.py @@ -80,7 +80,7 @@ def test_saliency_map_det(self, template): assert len(saliency_maps) == 2 assert saliency_maps[0].ndim == 3 assert saliency_maps[0].shape == self.ref_saliency_shapes[template.name] - assert (saliency_maps[0][0][0] == self.ref_saliency_vals_det[template.name]).all() + assert np.all(np.abs(saliency_maps[0][0][0] - self.ref_saliency_vals_det[template.name]) <= 1) @e2e_pytest_unit @pytest.mark.parametrize("template", templates_det, ids=templates_det_ids) From c3dd4aa025ffc2fb9b62eccfcf2a03acdf984f1f Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Mon, 10 Jul 2023 18:00:52 +0900 Subject: [PATCH 007/146] Contrain yapf verison lesser than 0.40.0 (#2328) contrain_yapf_version --- requirements/action.txt | 1 + requirements/classification.txt | 1 + requirements/detection.txt | 1 + requirements/segmentation.txt | 1 + 4 files changed, 4 insertions(+) diff --git a/requirements/action.txt b/requirements/action.txt index 070d2c8ff49..c60d30ca89c 100644 --- a/requirements/action.txt +++ b/requirements/action.txt @@ -4,3 +4,4 @@ mmcv-full==1.7.0 mmaction2==0.24.1 mmdet==2.28.1 mmdeploy==0.14.0 +yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/classification.txt b/requirements/classification.txt index 4a5a4a68144..facc2dca543 100644 --- a/requirements/classification.txt +++ b/requirements/classification.txt @@ -5,3 +5,4 @@ mmcls==0.25.0 timm==0.6.12 mmdeploy==0.14.0 pytorchcv +yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/detection.txt b/requirements/detection.txt index a0e94ea71bc..9118ffec5c1 100644 --- a/requirements/detection.txt +++ b/requirements/detection.txt @@ -8,3 +8,4 @@ timm==0.6.12 mmdeploy==0.14.0 mmengine==0.7.4 scikit-image +yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/segmentation.txt b/requirements/segmentation.txt index 31f503884aa..17820fdbe16 100644 --- a/requirements/segmentation.txt +++ b/requirements/segmentation.txt @@ -7,3 +7,4 @@ mmdeploy==0.14.0 timm==0.6.12 pytorchcv einops==0.6.1 +yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved From 19477a3d130fb8f4166e043c8d3a3abfbf78f924 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Mon, 10 Jul 2023 20:25:27 +0900 Subject: [PATCH 008/146] Fix detection e2e tests (#2327) Fix for detection --- tests/e2e/cli/detection/test_detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py index b9d35e1095c..c814ab7ca9e 100644 --- a/tests/e2e/cli/detection/test_detection.py +++ b/tests/e2e/cli/detection/test_detection.py @@ -46,7 +46,7 @@ "--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", "15", "--learning_parameters.batch_size", "4"], + "train_params": ["params", "--learning_parameters.num_iters", "7", "--learning_parameters.batch_size", "4"], } # Class-Incremental learning w/ 'vehicle', 'person', 'non-vehicle' classes From 6b51ba0f454d7db64ce26675e87758c78041dfd2 Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Tue, 11 Jul 2023 12:22:00 +0900 Subject: [PATCH 009/146] Mergeback: Label addtion/deletion 1.2.4 --> 1.4.0 (#2326) * Make black happy * Fix conflicts * Merge-back: add test datasets and edit the test code * Make black happy * Fix mis-merge * Make balck happy * Fix typo * Fix typoi --------- Co-authored-by: Songki Choi --- .../adapters/mmcls/datasets/otx_datasets.py | 5 +- .../models/classifiers/sam_classifier.py | 32 +++- src/otx/algorithms/classification/task.py | 22 ++- .../annotations/train.json | 181 ++++++++++++++++++ .../annotations/validation.json | 141 ++++++++++++++ .../images/train/a.jpg | Bin 0 -> 631 bytes .../images/train/b.jpg | Bin 0 -> 631 bytes .../images/validation/d.jpg | Bin 0 -> 631 bytes .../annotations/train.json | 20 +- .../annotations/validation.json | 22 ++- .../annotations/train.json | 68 +++++++ .../annotations/validation.json | 54 ++++++ .../images/train/a.jpg | Bin 0 -> 631 bytes .../images/train/b.jpg | Bin 0 -> 631 bytes .../images/validation/d.jpg | Bin 0 -> 631 bytes .../cli/classification/test_classification.py | 24 +++ 16 files changed, 546 insertions(+), 23 deletions(-) create mode 100755 tests/assets/datumaro_h-label_class_decremental/annotations/train.json create mode 100755 tests/assets/datumaro_h-label_class_decremental/annotations/validation.json create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/a.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/b.jpg create mode 100644 tests/assets/datumaro_h-label_class_decremental/images/validation/d.jpg create mode 100755 tests/assets/datumaro_multilabel_class_decremental/annotations/train.json create mode 100755 tests/assets/datumaro_multilabel_class_decremental/annotations/validation.json create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/a.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/b.jpg create mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/validation/d.jpg diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py index 99194cbc23e..70a4500d1b5 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -416,7 +416,10 @@ def evaluate( ) eval_results["MHAcc"] = total_acc - eval_results["avgClsAcc"] = total_acc_sl / self.hierarchical_info["num_multiclass_heads"] + 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 diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/sam_classifier.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/sam_classifier.py index 68249a8f6be..5b03e7c5f0a 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/sam_classifier.py +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/sam_classifier.py @@ -16,6 +16,14 @@ 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 SAMImageClassifier(SAMClassifierMixin, ClsLossDynamicsTrackingMixin, ImageClassifier): """SAM-enabled ImageClassifier.""" @@ -193,11 +201,19 @@ def load_state_dict_pre_hook(module, state_dict, prefix, *args, **kwargs): # no 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.""" + """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) @@ -249,13 +265,15 @@ def load_state_dict_mixing_hook( continue # Mix weights - chkpt_param = chkpt_dict[chkpt_name] - for module, c in enumerate(model2chkpt): - if c >= 0: - model_param[module].copy_(chkpt_param[c]) + # 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 + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param def extract_feat(self, img): """Directly extract features from the backbone + neck. diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index eb6fab8161d..3c74230dcab 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -47,6 +47,8 @@ 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, @@ -125,16 +127,22 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] if self._task_environment.model is not None: self._load_model() + 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): - self._multilabel = len(self._task_environment.label_schema.get_groups(False)) > 1 and len( - self._task_environment.label_schema.get_groups(False) - ) == len( - self._task_environment.get_labels(include_empty=False) - ) # noqa:E127 + 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") - - if not self._multilabel and len(self._task_environment.label_schema.get_groups(False)) > 1: + elif len(label_groups) > 1: logger.info("Classification mode: hierarchical") self._hierarchical = True self._hierarchical_info = get_hierarchical_info(self._task_environment.label_schema) 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 100755 index 00000000000..4bb4caae751 --- /dev/null +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/train.json @@ -0,0 +1,181 @@ +{ + "info": {}, + "categories": { + "label": { + "labels": [ + { + "name": "right", + "parent": "triangle", + "attributes": [] + }, + { + "name": "multi a", + "parent": "triangle", + "attributes": [] + }, + { + "name": "equilateral", + "parent": "triangle", + "attributes": [] + }, + { + "name": "square", + "parent": "rectangle", + "attributes": [] + }, + { + "name": "triangle", + "parent": "", + "attributes": [] + }, + { + "name": "non_square", + "parent": "rectangle", + "attributes": [] + }, + { + "name": "rectangle", + "parent": "", + "attributes": [] + } + ], + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["rectangle", "triangle"] + }, + { + "name": "rectangle default", + "group_type": "exclusive", + "labels": ["non_square", "square"] + }, + { + "name": "triangle default", + "group_type": "exclusive", + "labels": ["equilateral", "right"] + }, + { + "name": "shape___multiple example___multi a", + "group_type": "exclusive", + "labels": ["multi a"] + } + ], + "attributes": [] + }, + "mask": { + "colormap": [ + { + "label_id": 0, + "r": 129, + "g": 64, + "b": 123 + }, + { + "label_id": 1, + "r": 91, + "g": 105, + "b": 255 + }, + { + "label_id": 2, + "r": 91, + "g": 105, + "b": 255 + }, + { + "label_id": 3, + "r": 255, + "g": 86, + "b": 98 + }, + { + "label_id": 4, + "r": 204, + "g": 148, + "b": 218 + }, + { + "label_id": 5, + "r": 0, + "g": 251, + "b": 87 + }, + { + "label_id": 6, + "r": 84, + "g": 143, + "b": 173 + } + ] + } + }, + "items": [ + { + "id": "a", + "annotations": [ + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 4 + }, + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 5 + }, + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 1 + } + ], + "image": { + "path": "a.jpg", + "size": [10, 5] + }, + "media": { + "path": "" + } + }, + { + "id": "b", + "annotations": [ + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 6 + }, + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 5 + }, + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "b.jpg", + "size": [10, 5] + }, + "media": { + "path": "" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label_class_decremental/annotations/validation.json b/tests/assets/datumaro_h-label_class_decremental/annotations/validation.json new file mode 100755 index 00000000000..d97956af708 --- /dev/null +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/validation.json @@ -0,0 +1,141 @@ +{ + "info": {}, + "categories": { + "label": { + "labels": [ + { + "name": "right", + "parent": "triangle", + "attributes": [] + }, + { + "name": "multi a", + "parent": "triangle", + "attributes": [] + }, + { + "name": "equilateral", + "parent": "triangle", + "attributes": [] + }, + { + "name": "square", + "parent": "rectangle", + "attributes": [] + }, + { + "name": "triangle", + "parent": "", + "attributes": [] + }, + { + "name": "non_square", + "parent": "rectangle", + "attributes": [] + }, + { + "name": "rectangle", + "parent": "", + "attributes": [] + } + ], + "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["rectangle", "triangle"] + }, + { + "name": "rectangle default", + "group_type": "exclusive", + "labels": ["non_square", "square"] + }, + { + "name": "triangle default", + "group_type": "exclusive", + "labels": ["equilateral", "right"] + }, + { + "name": "shape___multiple example___multi a", + "group_type": "exclusive", + "labels": ["multi a"] + } + ], + "attributes": [] + }, + "mask": { + "colormap": [ + { + "label_id": 0, + "r": 129, + "g": 64, + "b": 123 + }, + { + "label_id": 1, + "r": 91, + "g": 105, + "b": 255 + }, + { + "label_id": 2, + "r": 91, + "g": 105, + "b": 255 + }, + { + "label_id": 3, + "r": 255, + "g": 86, + "b": 98 + }, + { + "label_id": 4, + "r": 204, + "g": 148, + "b": 218 + }, + { + "label_id": 5, + "r": 0, + "g": 251, + "b": 87 + }, + { + "label_id": 6, + "r": 84, + "g": 143, + "b": 173 + } + ] + } + }, + "items": [ + { + "id": "d", + "annotations": [ + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 5 + }, + { + "id": 0, + "type": "label", + "attributes": {}, + "group": 0, + "label_id": 2 + } + ], + "image": { + "path": "d.jpg", + "size": [10, 5] + }, + "media": { + "path": "" + } + } + ] +} diff --git a/tests/assets/datumaro_h-label_class_decremental/images/train/a.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..222682d80bf9740d8eb672035ae34a240f949592 GIT binary patch literal 631 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`*nMGf}^(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}^(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}^(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}^(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}^(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} Date: Tue, 11 Jul 2023 17:08:03 +0900 Subject: [PATCH 010/146] Bump datumaro up to 1.4.0rc2 (#2332) bump datumaro up to 1.4.0rc2 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 2dcf5c9ddfd..02ea591ac61 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro==1.4.0rc1 +datumaro==1.4.0rc2 psutil scipy>=1.8 bayesian-optimization>=1.2.0 From 3a0fff0a50d62bfdebf698ceb6095f3e646e9ae0 Mon Sep 17 00:00:00 2001 From: Eugene Liu Date: Tue, 11 Jul 2023 14:33:42 +0100 Subject: [PATCH 011/146] Tiling Doc for releases 1.4.0 (#2333) * Add tiling documentation --- .../explanation/additional_features/index.rst | 1 + .../additional_features/tiling.rst | 190 ++++++++++++++++++ docs/utils/images/dota_tiling_example.jpg | Bin 0 -> 1233988 bytes 3 files changed, 191 insertions(+) create mode 100644 docs/source/guide/explanation/additional_features/tiling.rst create mode 100644 docs/utils/images/dota_tiling_example.jpg diff --git a/docs/source/guide/explanation/additional_features/index.rst b/docs/source/guide/explanation/additional_features/index.rst index b9b24ddc43e..72e50a31b06 100644 --- a/docs/source/guide/explanation/additional_features/index.rst +++ b/docs/source/guide/explanation/additional_features/index.rst @@ -12,3 +12,4 @@ Additional Features xai noisy_label_detection fast_data_loading + tiling diff --git a/docs/source/guide/explanation/additional_features/tiling.rst b/docs/source/guide/explanation/additional_features/tiling.rst new file mode 100644 index 00000000000..6bfd9adb0db --- /dev/null +++ b/docs/source/guide/explanation/additional_features/tiling.rst @@ -0,0 +1,190 @@ +Improve Small Object Detection with Image Tiling +************************************************* + +The OpenVINO Training Extensions introduces the concept of image tiling to enhance the accuracy of detection algorithms and instance segmentation algorithms, particularly for small and densely packed objects in high-resolution images. + +Image tiling involves dividing the original full-resolution image into multiple smaller tiles or patches. This division allows objects within the tiles to appear larger in relation to the tile size, effectively addressing the challenge of objects becoming nearly invisible in deeper layers of feature maps due to downsampling operations. Image tiling proves especially beneficial for datasets where objects can be as small as 20 by 20 pixels in a 4K image. + +However, it's important to consider the trade-off associated with image tiling. Dividing a single image sample into several tiles increases the number of samples for training, evaluation, and testing. This trade-off impacts the execution speed, as processing more images requires additional computational resources. To strike a balance between patch size and computational efficiency, the OpenVINO Training incorporates tile dataset samples and 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. + +By leveraging image tiling, the OpenVINO Training Extensions empowers detection and instance segmentation algorithms to effectively detect and localize small and crowded objects in large-resolution images, ultimately leading to improved overall performance and accuracy. + +Tiling Strategies +================= +Below we provided an example of tiling used on one of the image from `DOTA `_. + +.. image:: ../../../../utils/images/dota_tiling_example.jpg + :width: 800 + :alt: this image uploaded from this `source `_ + + +In this example, the full image is cropped into 9 tiles. During training, only the tiles with annotations (bounding boxes or masks) are used for training. + +During evaluation in training, only the tiles with annotations are used for evaluation, and evaluation is performed at the tile level. + +During testing, each tile is processed and predicted separately. The tiles are then stitched back together to form the full image, and the tile predictions are merged to form the full image prediction. + +The tiling strategy is implemented in the OpenVINO Training Extensions through the following steps: + +.. 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 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:: + + [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] 650/650, 17.2 task/s, elapsed: 38s, ETA: 0s + ==== merge: 7.326097726821899 sec ==== + + +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 ``tiling_parameters.enable_tiling`` parameter to 1. Here's an example of enabling tiling for the SSD model template: + +.. 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 + +.. note:: + + To learn how to deploy the trained model and run the exported demo, refer to :doc:`../../tutorials/base/deploy`. + + To learn how to run the demo in CLI and visualize results, refer to :doc:`../../tutorials/base/demo`. + +Enable Tiling via OTX Build +=========================== +Here's another way of enabling tiling for the SSD model template using the workspace: + +.. code-block:: + + otx build Custom_Object_Detection_Gen3_SSD --train-data-roots tests/assets/small_objects --val-data-roots tests/assets/small_objects + +The above command will create a workspace folder with the necessary files for training under ``otx-workspace-DETECTION``. + +You can then train the model with tiling enabled using the following command without specifying any data-related paths: + +.. code-block:: + + cd otx-workspace-DETECTION + otx train params --tiling_parameters.enable_tiling 1 + +Alternatively, you can update the ``tiling_parameters`` in ``configuration.yaml`` file under the workspace folder to configure tiling parameters: + +.. code-block:: + + hyper_parameters: + parameter_overrides: + tiling_parameters: + enable_tiling: + default_value: true + +And then train the model with tiling enabled using the following command: + +.. code-block:: + + otx train + + +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. + +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. + +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. + +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. + +You can also manually configure the tile overlap using ``tiling_parameters.tile_overlap parameter`` parameter. For more details, please refer to the section on `Manual Tiling Parameter Configuration`_ . + + +Tiling Sampling Strategy +------------------------ +To accelerate the training process, the OpenVINO Training Extensions introduces a tile sampling strategy. This strategy involves randomly sampling a percentage of tile images from the dataset to be used for training. + +Since training and validation on all tiles from a high-resolution image dataset can be time-consuming, sampling the tile dataset can significantly reduce the training and validation time. + +It's important to note that sampling is applied to the training and validation datasets, not the test dataset. + +This can be configured with ``tiling_parameters.tile_sampling_ratio`` parameter. Here's an example of setting the tile sampling ratio to 50%: + +.. 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 +------------------------------------- + +Users can disable adaptive tiling and customize the tiling process by setting the following parameters: + +.. 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: + +- ``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 +====================================== + +After training a model with tiling enabled, you can export the model to OpenVINO IR format using the following command: + +.. code-block:: + + otx export Custom_Object_Detection_Gen3_SSD --load-weights /weights.pth --output + + +After exporting the model, you can run inference on the exported model using the following command: + +.. code-block:: + + 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 in ``template.yaml`` file located in the workspace. \ No newline at end of file diff --git a/docs/utils/images/dota_tiling_example.jpg b/docs/utils/images/dota_tiling_example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b260de0b3fde3f81722cf1f25ee6a0c71a321919 GIT binary patch literal 1233988 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≷WQJgY%71jbVWU^` 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

^q`98eCJm4-GR>(DBtt=SwnW} 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�>#S$-&V!xUmNIJ? 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 z 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

!}Z7vSuimSQgMq5NOEq zlK8R{UKx)VgYY|_NQ&mK{Vk89DOx*a{KOcV^gRA?OP=ryL?@DXRWnZ@6irov`XSNO#aaI3i{vR#1E(^ z;m$gZEF}Dc@x|HdlqeX7Abrzl(eqiWoFw6~8hN*B>=;`%eQp9n^0i%p8&QK7pR6HQ z&V=;nQ5p953=BuN}3QPdp%h^3Dt&9^1MWRNk;6KVQtRF)xa zvKqCMevuaFK{@uryJn8ihn0+{ZM9mSCfrzlmNDR1&|jBs+0Nb}LF?*2tVPZZalumw zbi<)6y1BELZA$8~EN8x=2ikW1GEO(+?KVk|u{BWD)nsZ%(R^^w!Pbk9rJe6Ty_;A%g|TUOwb0()Tr30xRmSu+M-N&!$P}( zOsgN`i51+n8D_U5nzC|wZK^vGJ#?itT-S1Or4nBXP_^wJbqjUR)!yY@QJph!$apE; z(s5Zat7cGGcqyE_uEjlnM=94g2sKR+h&;y8sOGb7Dp=RVqruTD@$`BGtHzgIQ~fTi zNIv3)gnggE^OkWozvkFaxzGocQNKS&D33RvGh~N|e!RKkrH9wIU9zBhGx#A-JK+}L zR)YRGz~&!asg7{r0VeB4LLMZnrugcrGTYne&o%%D9Vk9< zt*;IHuXFEdh!mtFt{Fn{!Mqw|%gUE}2g!D! zMjJK6N`e|^>Q+XjDp>Il^ z#C5<{cizeJvIhvK&~t^bKU9Y`YQFHBfe@?qogkZu5di8h{sSByw)`20$@>R5)q2hi zOK`Y-v}_$Ed5M1@R#ZuiG&V?mASq53P$hh~AL1P}YwV^r#WwviCi}_gBu3sBM0CHM z1mI^*KEWZGxxG1=7(QH_c8jj?vnO=$&FDg)^d>-tj(yT<>rF*5Irw{Pa7Y58cfw<@ zA=-nFp)SaaWO^3kM2uTx9~^fe5s^wqVV6$*DKK9rh(Mf^T77+L9;$P9y1k|tJM;MT zeoUi1=N{#0vxVsC#?4T^>Cs!W_2wIUAbX36g;&t}RoNt7U2uQWs-h$adtOZP%gE@n z^eqTh1ws&Fd*J+ChW*6R<+&K-)H}2KN}M>j=`G8moh(s(cnmecvL+! zt0JE;{JeOMSgnF*b(rKdLsvjqS(7aXQ)pI)`|!%#S`aF|Ab-5<{@Awgx_u;I*IIa9 zy7-{AqS)lrtkP^C&g`L5!&~E9z@UD(DUt%O6E(;(ei+MESxtKlB4Yp!efdrvK6H)LC>xsH6>iRbc9w$$Za^>+&S79ApP+)pu1WN7*e^^gOP3z4&y8-u)mi zs;!(kO6fkMssho`Cb%KW%guH(w|06|ZKf0A0OGAzFQacruut!LDwMO^X^TEP^1X!z z_;RvcVjPOU$~TX~%c#H3?|tsmJ&lg?^12CuIBzAB{sAm$gkShA480np2d?9KiqzFG zjLvPrgO>Pb!`=@fU(J7j_uL?FDfKs9%ymX}4@VGljRVwP(!g;&z(sZnWb zzrVm_^T&t8*ORzn&Q^pW52gGWIk*gK6~};TI^uIC=dX0b0pn$D6yz|2`k5x}DeXFm z2e)y@nhkX-t)=CnBG$Vqy*8TbD+4W#*M?Ob)#jxd)MlWZf$FCl^Ymt`m?jCwLSau=9dvL7U)axTO@;FQAE!5+ngw++mCan zZ>qiCNLn`<#Om1xFQL4&rIUpL6D=P`JqM7 zp|LJRJyhoEDn_tfC_VkOtz@e24L+Wk12!72hGVr&A1$_#^+$EYC=Zun(Dd*?R8{k9N&jI{gSUSw3TSS+22fy)f}dC92yZL0PMfj9$D| zOl#tE>Y(;T-P{o{s-Q<|6ety(cLb>T1vb6I@I(&lMR#zb5JpR5TYq7a#|&?7rlEpB=Z51CCk{&@{pOe=!BH7w_ZO`C5MFU}-Qr>z3egi$7QeSX z{s9zMFj4^)BI>$rce=*j3VpVw$=Wp=x<==rs}L;AwnoNiZN$2^-U2#?%14C4l4#uYu@dN7qd3NR^JbVUs)noQ zxXs+j==Jgfmrd6j=^cJ%#7TmP@PX0GeXun4a$Y<+CofmU&St*wo*A68z?U946(o^T z@^2YaYkr}{mo0q82s|2GQ-6c;sytwfWQU}cf|u~=Z^pGLf8sxOBG@s_BHHPB6S3;# z6c+Da6q#PkcQDT2vnn_E54^D$$9$!yFNsQV-orvnrl+MhNl|0`HY?u)_I%M}tB7L~ zT4n;QKB@C@e}FXY&ja|_m+RRbsg2Yk@SDiepqR-2ED}LW>_eeQZlW}25 zB52;nhzUY5E+rAUP95}vzwypCDRdl$Rd|!pV`j+&yX&b_4+~FxVcaEma|Ba|Xb@uH z_}p}+?bV0x93F7YQm1oD6i4;I0*(9|MZs*Uatn)NJa1`sgVb>dLfUo+x!)4l=M@KE z6oi~-{!qM89TAsUb{pPHzc=Z09L_tbSl@0)w|J)x{R3cqq}*w0CNS#^{K!yw@w5hK zn=I-r7FBkGA26)H8SVS;c`UI>84BK!gsgO_z2MvS>&ETqBe(Mh-Z(iuKk$!sGOoTD z!o!)v5K$N~-Fw9hzFWT58%WF;kA$x#(Rd4G6F5%Jvnc4c8u?~q{)iLFnZ>);N^-6C z620Ik0f6vqMPC(&neoK(79}?UqE1FJBbxaM(VFRtfh70h@YbD1!X=F6q zsC^%L335eEvc~wcpSMA@>MTw6b3u@4mmA{QXZM!~mwKPJGDwvzNmE{qiZ4Fm6nong z!Hc$zYAbre0ChZmyQ?FDS21Q&QhPUucNF&+?Z~4mC+nx%j;L$u9}dZsa}mvri9el^IbN~=Vg{y8!GcLOMzwR-4*hV!`smrQc)~d~ z)j(WRv?~e*YoEFriBL|~w6%yZ&;_h6>dK-uhn`sgvNmsyj1y}p z8;~7S4*-4E?3#XxaKK@kk87%R1xh?F++6-EgK3QEkw@zVtYat_?A4KAVBP<2#qaES zRTBp#NYuuZ81!efAmo6{SfVCI+IX48CnbFLnjtAJMmu|NrUH-^#qE(yGVT0GpQNk! z%kRI726$==lTs1LB^{Kfu`KdWm+Z?3jtFt@l>s(U%f8|<5(1M8Z&N&hgcNE4u;?>7CN;Xw* z_HfmzkuFGT;1CC>ps#mu<*{5Y;1T`=eV-<}A>o*8gv>rx4mU5(XPZJp3}-Mf{8CTE zzmu@|-34A?B#;+DRema3TZ2wT(pu8BU7h>Vw=L6@V)d>SS9SjS2}6D1`pS|?pYPP7 z1X=d`kt8+ipl{xOBa|u?&DgTCUWv@FS)=F+YVkXUpgWj{%TQ1?Zjt?SWDHM7X6Um- zO>jB2M{82o(ca-T=L=@}o*u}*+X7KExKA5E&H+a*YFZU-#RZ+PpF8`nt02$!Z zvi`P*#~O{dW|{6_W}*<)%MtS}`ZRBK(}lw8if!r?e}hz3>fXDJzO1Ok=+K%e5MsN0 zV48y=Od7)x7BeL$Zn^B1&r{;S{Ag_$|s5E5?ec zoZ*zts-Qw@i~5sGVzbuK-d+z=cc~#Jl5T9>GwLWNYdmeT;BG7r7_ew?bOGIexd07+ zXp&)3zHhs$ncJAlI(Hh3&-Q&#Qd|q41|I#Xqq#bWp*T4`7BQVlD_&+WKO$kjxPRME zPk=%%YhfV%Bp0sW;MpTtGLH-<|M z<0lBVLx5${8W_7)Wwz$BS<%5*%USkYo~%hKVG)>#DpC*mlPPxPy?pWTFD|&AjB{Detv~oK>vHeDPNLla;d^XNhin3vWFPkWFz69 z2DwqL+Vbkkl+E!oDoaW)0Q6cobsxk%B+0aoqIzx>=b0^Q5QLdk`i#4@_4HLxT zcafDE*_`XjwhU2;ku|DkGaIFTA^%E}vidoB<(IFER$5#VZ3)cC zTk>3y2!}pEuoZXp`^x9J+l=QV4cZ+ypQ7Nnzm!#0oJ_{bJOG?7y&BGLd2o1SHGm0- z`Qg?H+-jo9uqLr`>8&8E8e?y!zfvsdEQOyy8NU~GJ>udi@iHlUvp zxrFtSQ8;hr}8lS*07Ml&8 z@k?lDI&3!4iBQSiE`_;T(Q3&%J7C6yo(CDv*~g_N)a3NkNEUY845G((20AeFU9P|n zMC+=|E1S7oau^Vxk5yGt8g=zO;9T+l~<57e-LN@UE#EDZxb!$~)Qh=#70h&34cI;hs;=&Zn7OZdOxH*#UQ7Q``c|7RUnt zxNyN;5@K!T1w%Gj{0Yq8Qiu8$JDb3qAg`ZHsthqQVka%8#%D|+dNFf!gI^|tw55=G z9{&L}2PS^Y!J|r$tKedF*27xAB0qoUk4Bz7izrvu8^OHbH!550puuxRX9O(T*r529 z{tZfmCGX{)F0&}otL9kJoMillUs!_E^ZB{O5`HmllfW*o0W=DN2{CRFRG~>F>E9hA~yry@g8#yH2yY=IQ; zuw73i>8*-U!nNpeTce{{k#6}znoM3MmiDghMR9ihc!Rix%HlY>?uBhaz-Z(*NKBu_ z0p#-`T!THMCi>;1sG#1%|Gu8m{%;f@;AbOdN`J;~_wRe*+*5x@vLX*%Eu2W{R@v2m zfE=K|qgG+Q+J?%*J@|a&Q=bQnq#qfT%+#sdcHqdp>-&Spfi8Jk}_gr>2nRw}jGpUWBw|@K?ys*DKe3AOmNr{IrdR0Prw!1XOUg zyb+YQ-Vc(Q;BcfWgOhi>?$<|F=b*HJJU_h?TF+W>M^qr^LQ=}|VC4e|w6p*s=U0-) zQJiF`xJ3vKTIb;OQ%C^Jic}bb8EML+H30TN0zed3$0+pYj$2vkW}p}u8mt#0Q01AvzSiZ(ilo)vM*$c&o^ zcKgyF`Xq*FE*xU4`-x9vpbmAcoPvzbWG^=_2=cyzA09&lHt){6w_+zR3JkSyMLBD2w9}EJ>BO;f;+f z3<|v7RIl>WE){KUvS4QF^m12?@|)jiE#W|iU1mRdEk$a@+YM$h+gOt_GY+x~{sm)YDFQIZjx z@?UuoJ&mkIBV|fMSS*RO{ZjH(5lw7eLHHI5U+IC@GLNGFYVexHnt;|3VMcxYi_)jl zU^a_0jKdGf^sAE;`30jL>H-$L2T!KXU}v$3PT2QO>l{z5%}k z81#nQq<-unYm+&DDuZ(T`{>OFtUBlyZuO+`U53IItHNmjEek z;Hg6FiQ-}GjKRRnSJ~qy@C)~l@Ep9%5^v&X-HVPf!KFAz|D@EM! zblvjl=l25NL-w$;+FbHcE+Cf3EK?Dc8M>d|tPjm`06+>@H@=KEfbZgb^v3c-C5yRJp*Tq-pKBf*l&M|ezNiX1C)96f`@&W%dEPg?n-PpI3?wPzdf68 z_r2aMdzl0F|M@C$EMdN+pWmI|T%L>I*&VjwpbgD-7~;?E@j$hyg1}oYEp||VC48p`E4MfEA#vpH`N#=DwAzT;$sf%;X5(J z!Y9ȱqx^@y|kmA$60;~(^W4|c*qAZKMkc0Lwdm7O8Iz>p_`eXRWbse?N+ihbP) z+p#tf|A(3B7fz+9K16>!h?8%Ej^hdV8TsOy5px9!Qfcf2iS?}t&&O%JN|Y7x*o)&9 zp6CMyUPe0grsMFMS``Q*`Wox<)1$sAeerx)MBlC1^o4gk-Ef|T>B zD%ApV@Tp!rOYKmBb)=Eo>#GH#O{PP5CAQE;9|sGmae)+AQF2oFqGzN4Or(_s?5Ir4 zXjpi4W|ebUWhzzB<^XS{ITx|@**}0JcBBE@>^AbOxeITV>zbOy*#*vSNt-PoPEY4x zGrM{PF~d~#+q8Qre^HfdDOyw4k~Sp(!=XjR)rAz@M(u51PiZ;ag9Ht~1HJ+tKOEoH z5QP)eB3a~*4 zi{ea^L+zHKs9HGeO%V;~het`U8AESvmZ4($7_xTodSb5{DIucDl zc@Zy%dsSUI$+2A>f~6>yYDK1T?W@Viz_DgdU4RWiRhUMP(_ACcMqz08<^FHf1B@yI z1MP8HUPP8EJbkuaDWuuFmVT;y@Kmjzw~KLda@0kjF!G`XKgS(RGL@kzH*-BS7#&($ zL#%3B}- z-MfGlH6d9?>�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
rn2k+!Go6 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
    ?o7)KDtkpXxGwvbJ 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
  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/train/a.jpg b/tests/assets/datumaro_h-label/images/train/a.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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 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 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_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/train/a.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/train/a.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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/datumaro_multilabel_class_decremental/images/validation/d.jpg b/tests/assets/datumaro_multilabel_class_decremental/images/validation/d.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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`*nMGf} Date: Fri, 14 Jul 2023 03:05:53 +0200 Subject: [PATCH 017/146] Update ModelAPI in 1.4 release (#2347) * Upgrade model API * Update otx in exportable code * Fix unit tests * Fix black * Fix detection inference * Fix det tiling * Fix mypy * Fix demo * Fix visualizer in demo * Fix black --- requirements/openvino.txt | 2 +- src/otx/algorithms/detection/adapters/openvino/task.py | 2 +- .../demo/demo_package/executors/asynchronous.py | 4 +++- .../exportable_code/demo/demo_package/model_container.py | 2 +- .../api/usecases/exportable_code/demo/requirements.txt | 4 ++-- .../prediction_to_annotation_converter.py | 2 +- src/otx/api/utils/tiler.py | 9 +++++---- .../adapters/openvino/test_action_openvino_models.py | 2 +- .../model_wrappers/test_detection_openvino_models.py | 4 +++- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/requirements/openvino.txt b/requirements/openvino.txt index 71903a3f264..41c6ecbfeb7 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -2,7 +2,7 @@ # OpenVINO Requirements. # nncf==2.5.0 onnx==1.13.0 -openvino-model-api==0.1.2 +openvino-model-api==0.1.3 openvino==2023.0 openvino-dev==2023.0 openvino-telemetry>=2022.1.0 diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index b8df0281652..4ad7abba8fc 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -235,7 +235,7 @@ def __init__( def post_process(self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Detection specific post-process.""" detections = self.model.postprocess(prediction, metadata) - detections = detection2array(detections) + detections = detection2array(detections.objects) return self.converter.convert_to_annotation(detections, metadata) 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 index 9a1eb31b456..50b1b0c4707 100644 --- 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 @@ -78,7 +78,9 @@ def render_result(self, results: Tuple[Any, dict]) -> np.ndarray: predictions, frame_meta = results if isinstance(self.converter, DetectionToAnnotationConverter): # Predictions for the detection task - predictions = np.array([[pred.id, pred.score, *pred.get_coords()] for pred in predictions]) + 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"] 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 index 5fe97861cbb..eda80faab7a 100644 --- 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 @@ -121,7 +121,7 @@ def infer(self, frame): # MaskRCNN returns tuple so no need to process if self._task_type == TaskType.DETECTION: - predictions = detection2array(predictions) + predictions = detection2array(predictions.objects) return predictions, frame_meta def infer_tile(self, frame): diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 3496770b4dc..86aa5654450 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 -openvino-model-api==0.1.2 -otx==1.4.0rc2 +openvino-model-api==0.1.3 +otx @ git+https://github.com/openvinotoolkit/training_extensions/@e4269e035bcaa3903c6b99044f46c42fcbf98f25#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/prediction_to_annotation_converter.py b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py index 94c53477bb9..b9931aed01e 100644 --- a/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py +++ b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py @@ -214,7 +214,7 @@ def convert_to_annotation( 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.get_coords(), dtype=float) + 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) diff --git a/src/otx/api/utils/tiler.py b/src/otx/api/utils/tiler.py index 55ba42197d1..19ae38cd31b 100644 --- a/src/otx/api/utils/tiler.py +++ b/src/otx/api/utils/tiler.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: Apache-2.0 # -import copy import cv2 from itertools import product from typing import Dict, List, Optional, Tuple, Union @@ -12,6 +11,8 @@ import numpy as np from openvino.model_api.models import Model, ImageModel +from openvino.model_api.models.utils import DetectionResult + from otx.algorithms.common.utils.logger import get_logger from otx.api.utils.async_pipeline import OTXDetectionAsyncPipeline from otx.api.utils.detection_utils import detection2array @@ -196,7 +197,7 @@ def predict_async(self, image: np.ndarray, tile_coords: List[List[int]]): merged_features = self.merge_features(features, merged_results) return merged_results, merged_features - def postprocess_tile(self, predictions: Union[List, Tuple], offset_x: int, offset_y: int) -> Dict[str, List]: + def postprocess_tile(self, predictions: DetectionResult, offset_x: int, offset_y: int) -> Dict[str, List]: """Postprocess single tile prediction. Args: @@ -221,8 +222,8 @@ def postprocess_tile(self, predictions: Union[List, Tuple], offset_x: int, offse ) output_dict["masks"] = tile_masks else: - assert isinstance(predictions, list) - out = detection2array(predictions) + 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 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 index 46cc29ff3bd..0be96ffd228 100644 --- a/tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py +++ b/tests/unit/algorithms/action/adapters/openvino/test_action_openvino_models.py @@ -221,4 +221,4 @@ def test_postprocess(self) -> None: # argmax index is 2 because first index is for background assert out[0].id == 2 assert out[0].score == 0.7 - assert out[0].get_coords() == (0, 0, 256, 256) + assert (out[0].xmin, out[0].ymin, out[0].xmax, out[0].ymax) == (0, 0, 256, 256) diff --git a/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py b/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py index 05935ff8095..d7b1646bdc2 100644 --- a/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py +++ b/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py @@ -78,6 +78,8 @@ def __init__(self, *args): self.resize_type = "standard" self.output_parser = MockBatchBoxesLabelsParser() self.labels = [] + self.w = 10 + self.h = 10 super().__init__(MockOpenvinoAdapter) @@ -142,7 +144,7 @@ def test_postprocess(self) -> None: } sample_meta = {"original_shape": (10, 10, 3), "resized_shape": (5, 5, 3)} out = self.model.postprocess(sample_output, meta=sample_meta) - assert len(out) <= 1 + assert len(out.objects) <= 1 class TestBatchBoxesLabelsParser: From 4a8b0793a9d411b52bfbf16977e7ce41ea061928 Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Fri, 14 Jul 2023 10:30:36 +0900 Subject: [PATCH 018/146] Add OTX optimize for visual prompting task (#2318) * Initial commit * Update block * (WIP) otx optimize * Fix * WIP * Update configs & exported outputs * Remove unused modules for torch * Add unit tests * pre-commit * Update CHANGELOG --- CHANGELOG.md | 1 + .../model_wrappers/openvino_models.py | 14 +- .../config/visual_prompting_config.py | 4 +- .../datasets/pipelines/sam_transforms.py | 59 +----- .../visual_prompters/segment_anything.py | 26 +-- .../configs/base/configuration.py | 19 +- .../configs/configuration.yaml | 4 +- .../configs/sam_vit_b/config.yaml | 4 +- .../configs/sam_vit_b/configuration.yaml | 60 +----- .../visual_prompting/tasks/inference.py | 3 +- .../visual_prompting/tasks/openvino.py | 174 ++++++++++++++++-- src/otx/cli/tools/optimize.py | 8 +- .../model_wrappers/test_openvino_models.py | 7 + .../config/test_visual_prompting_config.py | 11 +- .../datasets/pipelines/test_sam_transforms.py | 99 +++++----- .../visual_prompting/tasks/test_openvino.py | 108 ++++++++++- 16 files changed, 396 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb7d47864d..7bbe22f40e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Add new visual prompting task: train/eval (https://github.com/openvinotoolkit/training_extensions/pull/2203) - Add new visual prompting task: export (https://github.com/openvinotoolkit/training_extensions/pull/2274) - Add new visual prompting task: deploy (https://github.com/openvinotoolkit/training_extensions/pull/2311) +- Add new visual prompting task: optimize (PTQ) (https://github.com/openvinotoolkit/training_extensions/pull/2318) - Add new object detector ResNeXt101-ATSS () ### Enhancements 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 index 83f327a7eca..ee18acd4bd6 100644 --- 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 @@ -23,6 +23,7 @@ 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 from otx.api.utils.segmentation_utils import create_hard_prediction_from_soft_prediction @@ -40,13 +41,20 @@ def parameters(cls) -> Dict[str, Any]: # noqa: D102 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]]: + 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 @@ -63,7 +71,6 @@ def __init__( preload: bool = False, ): super().__init__(model_adapter, configuration, preload) - self.output_blob_name = "low_res_masks" @classmethod def parameters(cls): # noqa: D102 @@ -71,6 +78,9 @@ def parameters(cls): # noqa: D102 parameters.update({"image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048)}) return parameters + def _get_outputs(self): + return "low_res_masks" + def preprocess(self, inputs: Dict[str, Any], meta: Dict[str, Any]): """Preprocess prompts.""" processed_prompts = [] 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 index ddd4d4dc070..e3382f25526 100644 --- 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 @@ -97,8 +97,8 @@ def update_visual_prompting_config( if groups: for group in groups: if group in ["learning_parameters", "nncf_optimization", "pot_parameters", "postprocessing"]: - if group in ["nncf_optimization", "pot_parameters"]: - # TODO (sungchul): Consider pot_parameters, nncf_optimization, and postprocessing + 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)) 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 index 74e80f1b383..aeb0cc98baf 100644 --- 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 @@ -10,7 +10,6 @@ import numpy as np import torch from torch import Tensor -from torch.nn import functional as F from torchvision.transforms.functional import resize, to_pil_image # type: ignore @@ -36,7 +35,7 @@ def __call__(self, item: Dict[str, Union[List, Tensor]]) -> Dict[str, Union[List Dict[str, Union[List, Tensor]]: Dictionary of batch data. """ item["images"] = torch.as_tensor( - self.apply_image(item["images"]).transpose((2, 0, 1)), dtype=torch.get_default_dtype() + self.apply_image(item["images"], self.target_length).transpose((2, 0, 1)), dtype=torch.get_default_dtype() ) item["gt_masks"] = [torch.as_tensor(gt_mask) for gt_mask in item["gt_masks"]] item["bboxes"] = self.apply_boxes(item["bboxes"], item["original_size"]) @@ -44,16 +43,18 @@ def __call__(self, item: Dict[str, Union[List, Tensor]]) -> Dict[str, Union[List item["points"] = self.apply_coords(item["points"], item["original_size"]) return item - def apply_image(self, image: np.ndarray) -> np.ndarray: + @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 = self.get_preprocess_shape(image.shape[0], image.shape[1], self.target_length) + target_size = cls.get_preprocess_shape(image.shape[0], image.shape[1], target_length) return np.array(resize(to_pil_image(image), target_size)) def apply_coords(self, coords: np.ndarray, original_size: Union[List[Any], Tensor]) -> np.ndarray: @@ -88,56 +89,6 @@ def apply_boxes(self, boxes: np.ndarray, original_size: Union[List[Any], Tensor] boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size) return boxes.reshape(-1, 4) - def apply_image_torch(self, image: torch.Tensor) -> torch.Tensor: - """Expects batched images with shape BxCxHxW and float format. - - This transformation may not exactly match apply_image. - apply_image is the transformation expected by the model. - - Args: - image (torch.Tensor): Image tensor. - - Returns: - torch.Tensor: Resized image. - """ - # Expects an image in BCHW format. May not exactly match apply_image. - target_size = self.get_preprocess_shape(image.shape[2], image.shape[3], self.target_length) - return F.interpolate(image, target_size, mode="bilinear", align_corners=False, antialias=True) - - def apply_coords_torch(self, coords: torch.Tensor, original_size: Tuple[int, ...]) -> torch.Tensor: - """Expects a torch tensor with length 2 in the last dimension. - - Requires the original image size in (H, W) format. - - Args: - coords (torch.Tensor): Coordinates tensor. - original_size (Tuple[int, ...]): Original size of image. - - Returns: - torch.Tensor: Resized coordinates. - """ - old_h, old_w = original_size - new_h, new_w = self.get_preprocess_shape(original_size[0], original_size[1], self.target_length) - coords = deepcopy(coords).to(torch.float) - coords[..., 0] = coords[..., 0] * (new_w / old_w) - coords[..., 1] = coords[..., 1] * (new_h / old_h) - return coords - - def apply_boxes_torch(self, boxes: torch.Tensor, original_size: Tuple[int, ...]) -> torch.Tensor: - """Expects a torch tensor with shape Bx4. - - Requires the original image size in (H, W) format. - - Args: - boxes (torch.Tensor): Boxes tensor. - original_size (Tuple[int, ...]): Original size of image. - - Returns: - torch.Tensor: Resized boxes. - """ - boxes = self.apply_coords_torch(boxes.reshape(-1, 2, 2), original_size) - 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. 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 index 3dbe568091f..efa3f792265 100644 --- 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 @@ -174,9 +174,9 @@ def replace_state_dict_keys(state_dict, revise_keys): state_dict = replace_state_dict_keys(state_dict, revise_keys) self.load_state_dict(state_dict) - ################################################# - # forward for inference (export/deploy) # - ################################################# + ########################################################## + # forward for inference (export/deploy/optimize) # + ########################################################## @torch.no_grad() def forward( self, @@ -185,7 +185,7 @@ def forward( point_labels: Tensor, mask_input: Tensor, has_mask_input: Tensor, - orig_size: Tensor, + # orig_size: Tensor, ): """Forward method for SAM inference (export/deploy). @@ -227,16 +227,18 @@ def forward( if self.config.model.return_single_mask: masks, scores = self.select_masks(masks, scores, point_coords.shape[1]) - upscaled_masks = self.mask_postprocessing(masks, orig_size[0]) + return scores, masks + # TODO (sungchul): apply inner postprocessing + # upscaled_masks = self.mask_postprocessing(masks, 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 + # 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 + # return upscaled_masks, scores, masks def _embed_points(self, point_coords: Tensor, point_labels: Tensor) -> Tensor: """Embed sparse input prompts. diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration.py b/src/otx/algorithms/visual_prompting/configs/base/configuration.py index eeb174c4875..63dc1e726a2 100644 --- a/src/otx/algorithms/visual_prompting/configs/base/configuration.py +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration.py @@ -17,13 +17,15 @@ from attr import attrs -from otx.algorithms.common.configs import BaseConfig +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 @@ -95,5 +97,20 @@ class __Postprocessing(ParameterGroup): 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) postprocessing = add_parameter_group(__Postprocessing) + pot_parameters = add_parameter_group(__POTParameter) diff --git a/src/otx/algorithms/visual_prompting/configs/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/configuration.yaml index e20429f60b2..1949d14f2a3 100644 --- a/src/otx/algorithms/visual_prompting/configs/configuration.yaml +++ b/src/otx/algorithms/visual_prompting/configs/configuration.yaml @@ -148,7 +148,7 @@ pot_parameters: affects_outcome_of: NONE auto_hpo_state: not_possible auto_hpo_value: null - default_value: Performance + default_value: Mixed description: Quantization preset that defines quantization scheme editable: true enum_name: POTQuantizationPreset @@ -162,7 +162,7 @@ pot_parameters: operator: AND rules: [] type: UI_RULES - value: Performance + value: Mixed visible_in_ui: true warning: null stat_subset_size: 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 index 393cfa468a2..3738303c911 100644 --- a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml @@ -1,6 +1,6 @@ dataset: task: visual_prompting - train_batch_size: 2 + train_batch_size: 4 val_batch_size: 1 test_batch_size: 1 num_workers: 4 @@ -35,7 +35,7 @@ model: optimizer: name: Adam - lr: 0.0001 + lr: 0.000001 callback: checkpoint: # arguments for ModelCheckpoint diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml index e20429f60b2..8a867588912 100644 --- a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml @@ -85,62 +85,6 @@ learning_parameters: visible_in_ui: true warning: null auto_hpo_state: NOT_POSSIBLE -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 @@ -148,7 +92,7 @@ pot_parameters: affects_outcome_of: NONE auto_hpo_state: not_possible auto_hpo_value: null - default_value: Performance + default_value: Mixed description: Quantization preset that defines quantization scheme editable: true enum_name: POTQuantizationPreset @@ -162,7 +106,7 @@ pot_parameters: operator: AND rules: [] type: UI_RULES - value: Performance + value: Mixed visible_in_ui: true warning: null stat_subset_size: diff --git a/src/otx/algorithms/visual_prompting/tasks/inference.py b/src/otx/algorithms/visual_prompting/tasks/inference.py index 6c93a05caa9..b84984e5fef 100644 --- a/src/otx/algorithms/visual_prompting/tasks/inference.py +++ b/src/otx/algorithms/visual_prompting/tasks/inference.py @@ -281,9 +281,8 @@ def _export_to_onnx(self, onnx_path: Dict[str, str]): "point_labels": torch.randint(low=0, high=4, size=(1, 2), dtype=torch.float), "mask_input": torch.randn(1, 1, *mask_input_size, dtype=torch.float), "has_mask_input": torch.tensor([[1]], dtype=torch.float), - "orig_size": torch.tensor([[height, width]], dtype=torch.float), } - output_names = ["masks", "iou_predictions", "low_res_masks"] + output_names = ["iou_predictions", "low_res_masks"] model_to_export = self.model with warnings.catch_warnings(): diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index e2d24c9d14a..f7d045f1e6c 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -17,16 +17,22 @@ import io import json import os +import random +import tempfile import time from pathlib import Path 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 nncf.common.quantization.structs import QuantizationPreset from openvino.model_api.adapters import create_core from openvino.model_api.models import Model +from otx.algorithms.common.utils.ir import check_if_quantized from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.utils import get_default_async_reqs_num from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers @@ -46,12 +52,19 @@ default_progress_callback, ) from otx.api.entities.label_schema import LabelSchemaEntity -from otx.api.entities.model import ModelEntity +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 +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 BaseInferencer @@ -101,13 +114,16 @@ def __init__( 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"]) + }, "decoder": { **attr.asdict( hparams.postprocessing, filter=lambda attr, value: attr.name not in ["header", "description", "type", "visible_in_ui", "class_name"], ) - } + }, } for name in ["image_encoder", "decoder"]: model_adapter = VisualPromptingOpenvinoAdapter( @@ -124,13 +140,14 @@ def __init__( self.labels = label_schema.get_labels(include_empty=False) self.transform = get_transform() # TODO (sungchul): insert args - def pre_process(self, dataset_item: DatasetItemEntity) -> Dict[str, Any]: # type: ignore + def pre_process( # type: ignore + self, dataset_item: DatasetItemEntity, extra_processing: bool = False + ) -> Tuple[Dict[str, Any], Dict[str, Any], List[Dict[str, Any]]]: """Pre-process function of OpenVINO Visual Prompting Inferencer for image encoder.""" - images, meta = self.model["image_encoder"].preprocess(dataset_item.numpy) + images, meta = self.model["image_encoder"].preprocess(dataset_item.numpy, extra_processing) prompts = OTXVisualPromptingDataset.get_prompts(dataset_item, self.labels) # to be replaced prompts = self.model["decoder"].preprocess(prompts, meta) - items = {**images, **meta, "prompts": prompts} - return items + return images, meta, prompts # type: ignore def post_process( self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any] @@ -143,19 +160,20 @@ def post_process( def predict(self, dataset_item: DatasetItemEntity) -> List[Annotation]: # type: ignore """Perform a prediction for a given input image.""" # forward image encoder - items = self.pre_process(dataset_item) - image_embeddings = self.forward({"images": items["images"]}) + images, meta, prompts = self.pre_process(dataset_item) + image_embeddings = self.forward(images) annotations: List[Annotation] = [] hard_predictions: List[np.ndarray] = [] soft_predictions: List[np.ndarray] = [] - for prompt in items["prompts"]: + for prompt in prompts: label = prompt.pop("label") + orig_size = prompt.pop("orig_size") prompt.update(image_embeddings) # forward decoder to get predicted mask prediction = self.forward_decoder(prompt) - metadata = {"label": label, "original_size": prompt["orig_size"]} + metadata = {"label": label, "original_size": orig_size} # set annotation for eval annotation, hard_prediction, soft_prediction = self.post_process(prediction, metadata) @@ -178,6 +196,62 @@ def await_all(self) -> None: self.model["decoder"].await_all() +class OTXOpenVinoDataLoader: + """DataLoader implementation for VisualPromptingOpenVINOTask.""" + + def __init__( + self, + dataset: Any, + inferencer: OpenVINOVisualPromptingInferencer, + shuffle: bool = True, + is_encoder: bool = True, + output_model: Optional[ModelEntity] = None, + ): + self.dataset = dataset + self.inferencer = inferencer + self.shuffler = None + if shuffle: + self.shuffler = list(range(len(dataset))) + random.shuffle(self.shuffler) + + self.is_encoder = is_encoder + self.target_length = self.inferencer.model["image_encoder"].orig_width + if not self.is_encoder: + core = ov.Core() + compressed_model = core.read_model( + output_model.get_data("visual_prompting_image_encoder.xml"), + output_model.get_data("visual_prompting_image_encoder.bin"), + ) + self.compressed_model = core.compile_model( + model=compressed_model, device_name=inferencer.model["image_encoder"].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.is_encoder: + return images + else: + image_embeddings = self.compressed_model(images["images"]) + prompt = prompts[0] # only use the first prompt + prompt.pop("label") + prompt.pop("orig_size") + 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.""" @@ -335,4 +409,80 @@ def optimize( optimization_parameters: Optional[OptimizationParameters] = None, ): """Optimize function of OpenVINOVisualPromptingTask.""" - raise NotImplementedError + 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, (name, is_encoder) in enumerate(zip(["image_encoder", "decoder"], [True, False]), 1): + if name == "decoder": + # TODO (sungchul): quantize decoder, too + logger.info(f"{name} won't do PTQ.") + output_model.set_data( + f"visual_prompting_{name}.xml", self.model.get_data(f"visual_prompting_{name}.xml") + ) + output_model.set_data( + f"visual_prompting_{name}.bin", self.model.get_data(f"visual_prompting_{name}.bin") + ) + continue + + data_loader = OTXOpenVinoDataLoader( + dataset, self.inferencer, is_encoder=is_encoder, output_model=output_model + ) + quantization_dataset = nncf.Dataset(data_loader, lambda data: data) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, f"visual_prompting_{name}.xml") + bin_path = os.path.join(tempdir, f"visual_prompting_{name}.bin") + with open(xml_path, "wb") as f: + f.write(self.model.get_data(f"visual_prompting_{name}.xml")) + with open(bin_path, "wb") as f: + f.write(self.model.get_data(f"visual_prompting_{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") + + if optimization_parameters is not None: + optimization_parameters.update_progress(10 * i + 35 * (i - 1), 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(45 * i, None) + + with tempfile.TemporaryDirectory() as tempdir: + xml_path = os.path.join(tempdir, f"visual_prompting_{name}.xml") + bin_path = os.path.join(tempdir, f"visual_prompting_{name}.bin") + ov.serialize(compressed_model, xml_path) + with open(xml_path, "rb") as f: + output_model.set_data(f"visual_prompting_{name}.xml", f.read()) + with open(bin_path, "rb") as f: + output_model.set_data(f"visual_prompting_{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("POT optimization completed") diff --git a/src/otx/cli/tools/optimize.py b/src/otx/cli/tools/optimize.py index eaa9cc2e7c3..df866c63009 100644 --- a/src/otx/cli/tools/optimize.py +++ b/src/otx/cli/tools/optimize.py @@ -19,6 +19,7 @@ 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 @@ -140,8 +141,11 @@ def main(): validation_dataset = dataset.get_subset(Subset.VALIDATION) predicted_validation_dataset = task.infer( - validation_dataset.with_empty_annotations(), - InferenceParameters(is_evaluation=True), + # 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( 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 index efdf2c0b495..437e4f9d326 100644 --- 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 @@ -64,6 +64,13 @@ def test_parameters(self): 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 "low_res_masks" == results + @e2e_pytest_unit def test_preprocess(self): """Test preprocess""" 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 index d5d57119ca2..c61e6b46589 100644 --- 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 @@ -61,13 +61,20 @@ def test_update_visual_prompting_config(): """Test update_visual_prompting_config.""" otx_config = OmegaConf.create( { - "groups": ["learning_parameters"], + "groups": ["learning_parameters", "pot_parameters", "postprocessing"], "learning_parameters": {"parameters": ["param1"], "param1": "updated_value1"}, + "pot_parameters": {"parameters": ["param2"], "param2": "updated_value2"}, + "postprocessing": {"parameters": ["param3"], "param3": "updated_value3"}, "parameters": [], } ) - visual_prompting_config = OmegaConf.create({"param1": "value1", "param2": "value2"}) + visual_prompting_config = OmegaConf.create( + {"param1": "value1", "param2": "value2", "param3": "value3", "param4": "value4"} + ) 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"] == "value4" 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 index c79be668f22..35c00c0198b 100644 --- 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 @@ -5,7 +5,8 @@ # import numpy as np -import torch +from typing import Tuple +import pytest from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines.sam_transforms import ( ResizeLongestSide, ) @@ -14,60 +15,66 @@ class TestResizeLongestSide: - @e2e_pytest_unit - def test_apply_boxes(self): - """Test apply_boxes.""" - resize_longest_side = ResizeLongestSide(100) - boxes = np.array([[10, 20, 30, 40], [50, 60, 70, 80]]) - original_size = (200, 200) - expected_result = np.array([[5, 10, 15, 20], [25, 30, 35, 40]]) - - result = resize_longest_side.apply_boxes(boxes, original_size) - - assert np.array_equal(result, expected_result) + @pytest.fixture(autouse=True) + def setup(self): + self.resize_longest_side = ResizeLongestSide(8) @e2e_pytest_unit - def test_apply_image_torch(self): - """Test apply_image_torch.""" - resize_longest_side = ResizeLongestSide(100) - image = torch.zeros((1, 3, 200, 300), dtype=torch.float32) - expected_result_shape = (1, 3, 67, 100) - - result = resize_longest_side.apply_image_torch(image) - - assert result.shape == expected_result_shape + def test_call(self): + """Test __call__.""" @e2e_pytest_unit - def test_apply_coords_torch(self): - """Test apply_coords_torch.""" - resize_longest_side = ResizeLongestSide(100) - coords = torch.Tensor([[50, 50], [100, 100]]) - original_size = (200, 200) - expected_result = torch.Tensor([[25, 25], [50, 50]]) - - result = resize_longest_side.apply_coords_torch(coords, original_size) - - assert torch.allclose(result, expected_result) + @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 - def test_apply_boxes_torch(self): - """Test apply_boxes_torch.""" - resize_longest_side = ResizeLongestSide(100) - boxes = torch.Tensor([[10, 20, 30, 40], [50, 60, 70, 80]]) - original_size = (200, 200) - expected_result = torch.Tensor([[5, 10, 15, 20], [25, 30, 35, 40]]) + @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]])), + ], + ) + def test_apply_coords(self, coords: np.ndarray, original_size: Tuple[int, int], expected: np.ndarray): + """Test apply_coords.""" + result = self.resize_longest_side.apply_coords(coords, original_size) + + assert np.array_equal(result, expected) - result = resize_longest_side.apply_boxes_torch(boxes, original_size) + @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]])), + ], + ) + def test_apply_boxes(self, boxes: np.ndarray, original_size: Tuple[int, int], expected: np.ndarray): + """Test apply_boxes.""" + result = self.resize_longest_side.apply_boxes(boxes, original_size) - assert torch.allclose(result, expected_result) + assert np.array_equal(result, expected) @e2e_pytest_unit - def test_get_preprocess_shape(self): + @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.""" - resize_longest_side = ResizeLongestSide(100) - oldh, oldw = 200, 300 - expected_result = (67, 100) - - result = resize_longest_side.get_preprocess_shape(oldh, oldw, resize_longest_side.target_length) + result = self.resize_longest_side.get_preprocess_shape(oldh, oldw, self.resize_longest_side.target_length) - assert result == expected_result + assert result == expected diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py index d7e97499649..8a8229a9bf9 100644 --- a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py +++ b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py @@ -5,11 +5,15 @@ # from copy import deepcopy +from typing import Optional import numpy as np +import pathlib 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, @@ -18,6 +22,7 @@ from otx.algorithms.visual_prompting.tasks.openvino import ( OpenVINOVisualPromptingInferencer, OpenVINOVisualPromptingTask, + OTXOpenVinoDataLoader, ) from otx.api.configuration.configurable_parameters import ConfigurableParameters from otx.api.entities.annotation import Annotation @@ -86,7 +91,7 @@ def test_pre_process(self, mocker): returned_value = self.visual_prompting_ov_inferencer.pre_process(fake_input) - assert isinstance(returned_value, dict) + assert isinstance(returned_value, tuple) mocker_get_prompts.assert_called_once() @e2e_pytest_unit @@ -112,10 +117,10 @@ def test_predict(self, mocker): mocker_pre_process = mocker.patch.object( OpenVINOVisualPromptingInferencer, "pre_process", - return_value={ - "index": 0, - "images": torch.rand((1, 3, 2, 2)), - "prompts": [ + return_value=( + torch.zeros((1, 3, 2, 2)), + {}, + [ { "point_coords": [np.array([[[1, 1], [2, 2]]])], "point_labels": [1, 2], @@ -123,7 +128,7 @@ def test_predict(self, mocker): "orig_size": (4, 4), } ], - }, + ), ) mocker_forward = mocker.patch.object( OpenVINOVisualPromptingInferencer, "forward", return_value={"image_embeddings": np.empty((4, 2, 2))} @@ -165,6 +170,55 @@ def test_forward_decoder(self): assert returned_value == fake_output +class TestOTXOpenVinoDataLoader: + @pytest.fixture + def load_dataloader(self, mocker): + def _load_dataloader(is_encoder: bool = True, output_model: Optional[ModelEntity] = None): + dataset = generate_visual_prompting_dataset() + dataset = dataset.get_subset(Subset.TRAINING) + return OTXOpenVinoDataLoader( + dataset, self.mocker_inferencer, is_encoder=is_encoder, 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("is_encoder", [True, False]) + def test_getitem(self, mocker, load_dataloader, is_encoder: bool): + """Test __getitem__.""" + mocker_output_model = mocker.patch("otx.api.entities.model.ModelEntity") + if not is_encoder: + mocker.patch.object(mocker_output_model, "get_data") + self.mocker_read_model.reset_mock() + self.mocker_compile_model.reset_mock() + + dataloader = load_dataloader(is_encoder, 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 is_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" not in results + assert "image_embeddings" in results + + class TestOpenVINOVisualPromptingTask: @pytest.fixture def otx_model(self): @@ -240,11 +294,49 @@ def test_evaluate(self, mocker): @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.bin", b"image_encoder_bin") 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.model.set_data("visual_prompting_decoder.xml", b"deocder_xml") + 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): + with open(output_xml, "wb") as f: + f.write(b"compressed_image_encoder_xml") + 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"compressed_image_encoder_bin") + + 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.serialize", new=patch_save_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_once() + # check if only image encoder was compressed + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.xml") + == b"compressed_image_encoder_xml" + ) + assert ( + self.visual_prompting_ov_task.model.get_data("visual_prompting_image_encoder.bin") + == b"compressed_image_encoder_bin" + ) + assert self.visual_prompting_ov_task.model.get_data("visual_prompting_decoder.xml") == b"decoder_xml" + assert self.visual_prompting_ov_task.model.get_data("visual_prompting_decoder.bin") == b"decoder_bin" From 06142b643a1d08f7dd501f86947a22eb617e2ddd Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Fri, 14 Jul 2023 15:36:54 +0900 Subject: [PATCH 019/146] Update detection docs (#2335) * Update detection docs * Revert template id changes * Fix wrong template id * Update docs/source/guide/explanation/algorithms/object_detection/object_detection.rst Co-authored-by: Eunwoo Shin * Update docs/source/guide/explanation/algorithms/object_detection/object_detection.rst Co-authored-by: Eunwoo Shin --------- Co-authored-by: Eunwoo Shin --- .../object_detection/object_detection.rst | 127 +++++++++++------- .../source/guide/get_started/cli_commands.rst | 14 +- .../tutorials/base/how_to_train/detection.rst | 18 +-- .../detection/mobilenetv2_atss/__init__.py | 2 +- .../mobilenetv2_atss/data_pipeline.py | 2 +- .../detection/mobilenetv2_atss/deployment.py | 2 +- .../detection/mobilenetv2_atss/model.py | 2 +- .../mobilenetv2_atss/semisl/__init__.py | 2 +- .../mobilenetv2_atss/semisl/data_pipeline.py | 2 +- .../mobilenetv2_atss/semisl/model.py | 2 +- .../detection/mobilenetv2_atss/template.yaml | 4 +- .../mobilenetv2_atss/tile_pipeline.py | 2 +- .../compressed_model.yml | 0 tests/e2e/test_api_xai_sanity.py | 4 +- tests/integration/cli/test_cli.py | 7 +- tests/regression/regression_config.json | 40 +++--- .../detection/test_xai_detection_validity.py | 6 +- tests/unit/cli/manager/test_config_manager.py | 2 +- 18 files changed, 137 insertions(+), 101 deletions(-) rename tests/e2e/cli/detection/reference/{Custom_Object_Detection_Gen3_ATSS => Custom_Object_Detection_Gen3_MobileNetV2_ATSS}/compressed_model.yml (100%) 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 1cf89b52c5b..b744f255537 100644 --- a/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst +++ b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst @@ -71,51 +71,82 @@ Models We support the following ready-to-use model templates: -+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-----------------+ -| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | -+===========================================================================================================================================================================================+=========+=====================+=================+ -| `Custom_Object_Detection_YOLOX `_ | YOLOX | 6.5 | 20.4 | -+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-----------------+ -| `Custom_Object_Detection_Gen3_SSD `_ | SSD | 9.4 | 7.6 | -+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-----------------+ -| `Custom_Object_Detection_Gen3_ATSS `_ | ATSS | 20.6 | 9.1 | -+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------------+-----------------+ - -`ATSS `_ is a good medium-range model that works well and fast in most cases. ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++===========================================================================================================================================================================================+=====================+=====================+=================+ +| `Custom_Object_Detection_YOLOX `_ | YOLOX | 6.5 | 20.4 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_SSD `_ | SSD | 9.4 | 7.6 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ +| `Custom_Object_Detection_Gen3_ATSS `_ | MobileNetV2-ATSS | 20.6 | 9.1 | ++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+-----------------+ + +Above table can be found using the following command + +.. code-block:: + $ 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. YOLOX achieved the same accuracy as SSD, and even outperforms its inference on CPU 1.5 times, but requires 3 times more time for training due to `Mosaic augmentation `_, which is even more than for ATSS. So if you have resources for a long training, you can pick the YOLOX model. +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} + $ 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. That means that weights are undertrained for COCO dataset and don't achieve the best result. -That is because the purpose of pretrained models is to learn basic features from a such large and diverse dataset as COCO and to use these weights to get good results for other custom datasets right from the start. +For `COCO `__ dataset the accuracy of pretrained weights is shown, and we report official COCO mAP with AP50. +Except for COCO, we report AP50 as performance metric. -The results on `Pascal VOC `_, `BCCD `_, `MinneApple `_ and `WGISD `_ were obtained on our templates without any changes. -BCCD is an easy dataset with focused large objects, while MinneApple and WGISD have small objects that are hard to distinguish from the background. +5 datasets were selected as transfer learning datasets. +`BDD100K `_ is the largest dataset among we used. 70000 images are used as train images and 10000 images are used for validation. +`Brackish `_ and `Plantdoc `_ are datasets of medium size. They have around 10000 images for train and 1500 images for validation. +`BCCD `_ and `Chess pieces `_ are datasets of small size. They have around 300 images for train and 100 images for validation. +We used our own templates without any modification. For hyperparameters, please, refer to the related template. We trained each model with a single Nvidia GeForce RTX3090. -+-----------+------------+-----------+-----------+-----------+-----------+ -| Model name| COCO | PASCAL VOC| BCCD | MinneApple| WGISD | -+===========+============+===========+===========+===========+===========+ -| YOLOX | 32.0 | 66.6 | 60.3 | 24.5 | 44.1 | -+-----------+------------+-----------+-----------+-----------+-----------+ -| SSD | 13.5 | 50.0 | 54.2 | 31.2 | 45.9 | -+-----------+------------+-----------+-----------+-----------+-----------+ -| ATSS | 32.5 | 68.7 | 61.5 | 42.5 | 57.5 | -+-----------+------------+-----------+-----------+-----------+-----------+ - - ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| Model name | COCO(AP50) | BDD100K | Brackish | Plantdoc | BCCD | Chess pieces | ++============================+==================+===========+===========+===========+===========+==============+ +| YOLOX | 31.0 (48.2) | 24.8 | 96.3 | 51.5 | 88.5 | 99.2 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| SSD | 13.5 | 28.2 | 96.5 | 52.9 | 91.1 | 99.1 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| MobileNetV2-ATSS | 32.5 (49.5) | 40.2 | 99.1 | 63.4 | 93.4 | 99.1 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| ResNeXt101-ATSS | 45.1 (63.8) | 45.5 | 99.3 | 69.3 | 93.1 | 99.1 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| ResNet50-Deformable-DETR | 44.3 (63.2) | 44.8 | 97.7 | 60.7 | 93.4 | 99.2 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ +| ResNet50-DINO | 49.0 (66.4) | 47.2 | 99.5 | 62.9 | 93.5 | 99.1 | ++----------------------------+------------------+-----------+-----------+-----------+-----------+--------------+ ************************ Semi-supervised Learning @@ -142,26 +173,26 @@ In the table below the mAP on toy data sample from `COCO Date: Fri, 14 Jul 2023 16:52:06 +0900 Subject: [PATCH 020/146] Add visual prompting documentation (#2354) * (WIP) write docs * Add visual prompting documentation * Update CHANGELOG --------- Co-authored-by: sungchul.kim --- CHANGELOG.md | 1 + .../guide/explanation/algorithms/index.rst | 1 + .../algorithms/visual_prompting/index.rst | 101 ++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 docs/source/guide/explanation/algorithms/visual_prompting/index.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbe22f40e8..e2ab9e6698f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Add new visual prompting task: train/eval (https://github.com/openvinotoolkit/training_extensions/pull/2203) - Add new visual prompting task: export (https://github.com/openvinotoolkit/training_extensions/pull/2274) - Add new visual prompting task: deploy (https://github.com/openvinotoolkit/training_extensions/pull/2311) +- Add new visual prompting task: documentation (https://github.com/openvinotoolkit/training_extensions/pull/2354) - Add new visual prompting task: optimize (PTQ) (https://github.com/openvinotoolkit/training_extensions/pull/2318) - Add new object detector ResNeXt101-ATSS () diff --git a/docs/source/guide/explanation/algorithms/index.rst b/docs/source/guide/explanation/algorithms/index.rst index 1cfc9baa8e3..8202085affc 100644 --- a/docs/source/guide/explanation/algorithms/index.rst +++ b/docs/source/guide/explanation/algorithms/index.rst @@ -31,3 +31,4 @@ Contents segmentation/index anomaly/index action/index + visual_prompting/index diff --git a/docs/source/guide/explanation/algorithms/visual_prompting/index.rst b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst new file mode 100644 index 00000000000..d0843de8b8c --- /dev/null +++ b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst @@ -0,0 +1,101 @@ +Visual Prompting +================= + +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. + +This section examines the solutions for visual prompting offered by the OpenVINO Training Extensions library. +`Segment Anything (SAM) `_, is one of the most famous visual prompting methods and this model will be used to adapt a new dataset domain. +Because `SAM `_ was trained by using web-scale dataset and has huge backbone network, fine-tuning the whole network is difficult and lots of resources are required. +Therefore, in this section, we try to fine-tune only mask decoder only for several epochs to increase performance on the new dataset domain. +For fine-tuning `SAM `_, we use following algorithms components: + +.. _visual_prompting_finetuning_pipeline: + +- ``Pre-processing``: Resize an image according to the longest axis and pad the rest with zero. + +- ``Optimizer``: We use `Adam `_ optimizer. + +- ``Loss function``: We use standard loss combination, 20 * focal loss + dice loss + iou loss, used in `SAM `_ as it is. + +- ``Additional training techniques`` + - ``Early stopping``: To add adaptability to the training pipeline and prevent overfitting. Early stopping will be automatically applied. + + +.. note:: + + Currently, fine-tuning `SAM `_ with bounding boxes in the OpenVINO Training Extensions is only supported. + We will support fine-tuning with other prompts (points and texts) and continuous fine-tuning with predicted mask information in the near future. + +.. note:: + + Currently, Post-Training Quantization (PTQ) for `SAM `_ is only supported, not Quantization Aware Training (QAT). + + +************** +Dataset Format +************** +.. _visual_prompting_dataset: + +For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset Management Framework (Datumaro) `_. + +We support three dataset formats for visual prompting: + +- `Common Semantic Segmentation `_ for semantic segmentation + +- `COCO `_ for instance segmentation + +- `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 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. + + +****** +Models +****** +.. _visual_prompting_model: + +We support the following model templates in experimental phase: + ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------+---------------------+-----------------+ +| Template ID | Name | Complexity (GFLOPs) | Model size (MB) | ++======================================================================================================================================================================================+===========+=====================+=================+ +| `Visual_Prompting_SAM_ViT_B `_ | SAM_ViT_B | 487 | 374 | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------+---------------------+-----------------+ + +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 `_. The below table shows performance improvement after fine-tuning. + ++---------------------------------------------------------------+--------------------+--------+-------------------+ +| Dataset | #samples | Before | After fine-tuning | ++===============================================================+====================+========+===================+ +| `WGISD `_ | 110 | 92.32 | 92.46 (+0.14) | ++---------------------------------------------------------------+--------------------+--------+-------------------+ +| `Trashcan `_ | 100 | 79.61 | 83.92 (+4.31) | ++---------------------------------------------------------------+--------------------+--------+-------------------+ +| `FLARE22 `_ | 1 CT (=100 slices) | 91.48 | 91.68 (+0.20) | ++---------------------------------------------------------------+--------------------+--------+-------------------+ + +According to datasets, ``learning rate`` and ``batch size`` can be adjusted like below: + +.. code-block:: + + $ otx train \ + --train-data-roots \ + --val-data-roots \ + params \ + --learning_parameters.dataset.train_batch_size \ + --learning_parameters.optimizer.lr From ca5f74d58f8e367c9cf725de2541964916684460 Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Fri, 14 Jul 2023 17:11:02 +0900 Subject: [PATCH 021/146] Remove custom modelapi patch in visual prompting (#2359) * Remove custom modelapi patch * Update test --- .../openvino/model_wrappers/__init__.py | 1 - .../model_wrappers/openvino_adapters.py | 164 ------------------ .../visual_prompting/tasks/openvino.py | 7 +- .../model_wrappers/test_openvino_models.py | 5 +- .../visual_prompting/tasks/test_openvino.py | 6 +- 5 files changed, 6 insertions(+), 177 deletions(-) delete mode 100644 src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py 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 index d251a0ed64a..1c22c536057 100644 --- a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py @@ -14,5 +14,4 @@ # See the License for the specific language governing permissions # and limitations under the License. -from .openvino_adapters import VisualPromptingOpenvinoAdapter # noqa: F401 from .openvino_models import Decoder, ImageEncoder # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py deleted file mode 100644 index 6f0a9e402b4..00000000000 --- a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Openvino Adapter Wrappers of OTX Visual Prompting. - -There is a bug on fit_to_window resize module in model API. -VisualPromptingOpenvinoAdapter is temporarily implemented to use updated `fit_to_window` resize function. -When model API version in otx is upgraded, it can be removed. - -Issue: https://github.com/openvinotoolkit/model_api/issues/99 -Updated PR: https://github.com/openvinotoolkit/model_api/pull/100 -""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from functools import partial -from typing import Tuple - -import numpy as np -import openvino.runtime as ov -from openvino.model_api.adapters import OpenvinoAdapter -from openvino.preprocess import ColorFormat, PrePostProcessor -from openvino.runtime import Output, Type -from openvino.runtime import opset10 as opset -from openvino.runtime.utils.decorators import custom_preprocess_function - - -def resize_image_with_aspect_pad(input: Output, size, keep_aspect_ratio, interpolation, pad_value): - """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/utils.py#L273-L341.""" - h_axis = 1 - w_axis = 2 - w, h = size - - target_size = list(size) - target_size.reverse() - - image_shape = opset.shape_of(input, name="shape") - iw = opset.convert( - opset.gather(image_shape, opset.constant(w_axis), axis=0), - destination_type="f32", - ) - ih = opset.convert( - opset.gather(image_shape, opset.constant(h_axis), axis=0), - destination_type="f32", - ) - w_ratio = opset.divide(np.float32(w), iw) - h_ratio = opset.divide(np.float32(h), ih) - scale = opset.minimum(w_ratio, h_ratio) - nw = opset.convert(opset.round(opset.multiply(iw, scale), "half_to_even"), destination_type="i32") - nh = opset.convert(opset.round(opset.multiply(ih, scale), "half_to_even"), destination_type="i32") - new_size = opset.concat([opset.unsqueeze(nh, 0), opset.unsqueeze(nw, 0)], axis=0) - image = opset.interpolate( - input, - new_size, - scales=np.array([0.0, 0.0], dtype=np.float32), - axes=[h_axis, w_axis], - mode=interpolation, - shape_calculation_mode="sizes", - ) - - dx_border = opset.subtract(opset.constant(w, dtype=np.int32), nw) - dy_border = opset.subtract(opset.constant(h, dtype=np.int32), nh) - pads_begin = np.array([0, 0, 0, 0], np.int32) - pads_end = opset.concat( - [ - opset.constant([0], dtype=np.int32), - opset.unsqueeze(dy_border, 0), - opset.unsqueeze(dx_border, 0), - opset.constant([0], dtype=np.int32), - ], - axis=0, - ) - return opset.pad( - image, - pads_begin, - pads_end, - "constant", - opset.constant(pad_value, dtype=np.uint8), - ) - - -def resize_image_with_aspect(size, interpolation, pad_value): - """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/utils.py#L356-L365.""" - return custom_preprocess_function( - partial( - resize_image_with_aspect_pad, - size=size, - keep_aspect_ratio=True, - interpolation=interpolation, - pad_value=pad_value, - ) - ) - - -class VisualPromptingOpenvinoAdapter(OpenvinoAdapter): - """Openvino Adapter Wrappers of OTX Visual Prompting. - - This class is to use fixed `fit_to_window` resize module. - When model API version in otx is upgraded, it can be removed. - """ - - def embed_preprocessing( - self, - layout, - resize_mode: str, - interpolation_mode, - target_shape: Tuple[int], - pad_value, - dtype=type(int), - brg2rgb=False, - mean=None, - scale=None, - input_idx=0, - ): - """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/openvino_adapter.py#L340-L411.""" - ppp = PrePostProcessor(self.model) # type: ignore[has-type] - - # Change the input type to the 8-bit image - if dtype == type(int): - ppp.input(input_idx).tensor().set_element_type(Type.u8) - - ppp.input(input_idx).tensor().set_layout(ov.Layout("NHWC")).set_color_format(ColorFormat.BGR) - - INTERPOLATION_MODE_MAP = { - "LINEAR": "linear", - "CUBIC": "cubic", - "NEAREST": "nearest", - } - - RESIZE_MODE_MAP = {"fit_to_window": resize_image_with_aspect} - - # Handle resize - # Change to dynamic shape to handle various image size - # TODO: check the number of input channels and rank of input shape - if resize_mode and target_shape: - if resize_mode in RESIZE_MODE_MAP: - input_shape = [1, -1, -1, 3] - ppp.input(input_idx).tensor().set_shape(input_shape) - ppp.input(input_idx).preprocess().custom( - RESIZE_MODE_MAP[resize_mode]( - target_shape, - INTERPOLATION_MODE_MAP[interpolation_mode], - pad_value, - ) - ) - - else: - raise ValueError(f"Upsupported resize type in model preprocessing: {resize_mode}") - - # Handle layout - ppp.input(input_idx).model().set_layout(ov.Layout(layout)) - - # Handle color format - if brg2rgb: - ppp.input(input_idx).preprocess().convert_color(ColorFormat.RGB) - - ppp.input(input_idx).preprocess().convert_element_type(Type.f32) - - if mean: - ppp.input(input_idx).preprocess().mean(mean) - if scale: - ppp.input(input_idx).preprocess().scale(scale) - - self.model = ppp.build() - self.load_model() diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index f7d045f1e6c..9b87e1ca130 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -29,16 +29,13 @@ import numpy as np import openvino.runtime as ov from nncf.common.quantization.structs import QuantizationPreset -from openvino.model_api.adapters import create_core +from openvino.model_api.adapters import OpenvinoAdapter, create_core from openvino.model_api.models import Model from otx.algorithms.common.utils.ir import check_if_quantized from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.utils import get_default_async_reqs_num from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers -from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( - VisualPromptingOpenvinoAdapter, -) from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( OTXVisualPromptingDataset, get_transform, @@ -126,7 +123,7 @@ def __init__( }, } for name in ["image_encoder", "decoder"]: - model_adapter = VisualPromptingOpenvinoAdapter( + model_adapter = OpenvinoAdapter( core=create_core(), model=model_files.get(name), weights_path=weight_files.get(name, None), 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 index 437e4f9d326..716df9c70b4 100644 --- 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 @@ -10,12 +10,11 @@ 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 BooleanValue, NumericalValue +from openvino.model_api.models.types import NumericalValue from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( Decoder, ImageEncoder, - VisualPromptingOpenvinoAdapter, ) from otx.api.entities.label import LabelEntity from tests.test_suite.e2e_test_system import e2e_pytest_unit @@ -52,7 +51,7 @@ class TestDecoder: @pytest.fixture(autouse=True) def setup(self, mocker): mocker.patch.object(SegmentationModel, "__init__") - mocker_model_adapter = mocker.Mock(spec=VisualPromptingOpenvinoAdapter) + mocker_model_adapter = mocker.Mock(spec=OpenvinoAdapter) self.decoder = Decoder(mocker_model_adapter) self.decoder.image_size = 6 diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py index 8a8229a9bf9..805621eb378 100644 --- a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py +++ b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py @@ -58,8 +58,7 @@ def setup(self, mocker): labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], ) ] - # FIXME: change VisualPromptingOpenvinoAdapter to OpenvinoAdapter after model api version update - mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.VisualPromptingOpenvinoAdapter") + 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 @@ -231,8 +230,7 @@ def otx_model(self): @pytest.fixture(autouse=True) def setup(self, mocker, otx_model): """Load the OpenVINOVisualPromptingTask.""" - # FIXME: change VisualPromptingOpenvinoAdapter to OpenvinoAdapter after model api version update - mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.VisualPromptingOpenvinoAdapter") + 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) From a123ca777cf0846608ced795624b7decfb144f6e Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 17 Jul 2023 10:19:01 +0900 Subject: [PATCH 022/146] Fix graph metric order and label issues (#2356) * Fix graph metric going backward issue * Add license notice * Fix pre-commit issue * Add rename items & logic for metric --------- Signed-off-by: Songki Choi --- .../common/adapters/mmcv/hooks/logger_hook.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py index 051e8102950..acbddd846a8 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py @@ -1,4 +1,8 @@ """Logger hooks.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + from collections import defaultdict from typing import Any, Dict, Optional @@ -29,6 +33,19 @@ def __repr__(self): 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, @@ -43,12 +60,13 @@ def __init__( @master_only def log(self, runner: BaseRunner): """Log function for OTXLoggerHook.""" - tags = self.get_loggable_tags(runner, allow_text=False, tags_to_skip=()) + 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: @@ -57,6 +75,11 @@ def log(self, runner: BaseRunner): 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, From 5ca24e1450e0b4c46d758ef42bc40cb336fcb69e Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Mon, 17 Jul 2023 10:19:56 +0900 Subject: [PATCH 023/146] Update multi-label document and conversion script (#2358) Update docs, label convert script --- .../algorithms/classification/multi_label_classification.rst | 5 ++++- .../classification/utils/convert_coco_to_multilabel.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 4ae4e25a6a8..46840d0c955 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,7 @@ 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 `_ first. We provided a `script ` to help with conversion. +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:: @@ -35,6 +35,9 @@ To convert the COCO data format to our internal one, run this script in similar Please, refer to our :doc:`dedicated tutorial <../../../tutorials/base/how_to_train/classification>` for more information how to train, validate and optimize classification models. +.. note:: + For now, "___" is a symbol to distinguish the multi-label format. So, it must be included at the front of the label name. + ****** Models ****** diff --git a/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py b/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py index 108504feb04..a9a8e9dc567 100644 --- a/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py +++ b/src/otx/algorithms/classification/utils/convert_coco_to_multilabel.py @@ -61,7 +61,7 @@ def coco_to_datumaro_multilabel(ann_file_path: str, data_root_dir: str, output: overall_classes: List = coco_dataset.get_classes() for class_name in overall_classes: multilabel_ann_format["categories"]["label"]["label_groups"].append( - {"name": str(class_name), "group_type": "exclusive", "labels": [str(class_name)]} + {"name": f"___{str(class_name)}", "group_type": "exclusive", "labels": [str(class_name)]} ) multilabel_ann_format["categories"]["label"]["labels"].append( From c1c0463afca0f5774ed8dc335180318287ce0ca4 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 17 Jul 2023 18:52:19 +0900 Subject: [PATCH 024/146] Update third party programs (#2365) --- third-party-programs.txt | 207 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/third-party-programs.txt b/third-party-programs.txt index 5d9cdaf5350..e6ddfe9006c 100644 --- a/third-party-programs.txt +++ b/third-party-programs.txt @@ -828,3 +828,210 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------- +mmengine + +Apache-2.0 + + 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 [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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 895bd36061d7b4b0c8d031b325be6750ad466327 Mon Sep 17 00:00:00 2001 From: Dick Ameln Date: Mon, 17 Jul 2023 12:25:21 +0200 Subject: [PATCH 025/146] Make anomaly task compatible with older albumentations versions (#2363) * fix transforms export in metadata * wrap transform dict * add todo for updating to_dict call --- requirements/anomaly.txt | 1 - src/otx/algorithms/anomaly/tasks/inference.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/anomaly.txt b/requirements/anomaly.txt index 4716c2b33dc..97bbf4db2ba 100644 --- a/requirements/anomaly.txt +++ b/requirements/anomaly.txt @@ -1,6 +1,5 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Anomaly Requirements. -albumentations>=1.3.0 torchvision<0.15.1 torchtext<0.15.1 anomalib==0.5.1 diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 23afa946179..81240f31217 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -425,7 +425,8 @@ def _get_metadata_dict(self) -> Dict[str, Any]: else: transform = self.trainer.datamodule.train_dataloader().dataset.transform metadata = { - "transform": transform.to_dict(), + # 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), From 3d157ab335181c3598a459081d7eebe4648fbb63 Mon Sep 17 00:00:00 2001 From: Evgeny Tsykunov Date: Tue, 18 Jul 2023 01:58:36 +0200 Subject: [PATCH 026/146] Fixing detection saliency map for one class case (#2368) * fix softmax * fix validity tests --- .../adapters/mmdet/hooks/det_class_probability_map_hook.py | 2 +- .../classification/test_xai_classification_validity.py | 4 +++- .../unit/algorithms/detection/test_xai_detection_validity.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) 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 index 2613990d2e5..7931e234091 100644 --- 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 @@ -62,7 +62,7 @@ def func( # 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: + if self.use_cls_softmax and self._num_cls_out_channels > 1: cls_scores = [torch.softmax(t, dim=1) for t in cls_scores] batch_size, _, height, width = cls_scores[-1].size() diff --git a/tests/unit/algorithms/classification/test_xai_classification_validity.py b/tests/unit/algorithms/classification/test_xai_classification_validity.py index 1ec20d0c2c1..675cd53b89c 100644 --- a/tests/unit/algorithms/classification/test_xai_classification_validity.py +++ b/tests/unit/algorithms/classification/test_xai_classification_validity.py @@ -54,4 +54,6 @@ def test_saliency_map_cls(self, template): assert len(saliency_maps) == 2 assert saliency_maps[0].ndim == 3 assert saliency_maps[0].shape == (1000, 7, 7) - assert np.all(np.abs(saliency_maps[0][0][0] - self.ref_saliency_vals_cls[template.name]) <= 1) + actual_sal_vals = saliency_maps[0][0][0].astype(np.int8) + ref_sal_vals = self.ref_saliency_vals_cls[template.name].astype(np.int8) + assert np.all(np.abs(actual_sal_vals - ref_sal_vals) <= 1) diff --git a/tests/unit/algorithms/detection/test_xai_detection_validity.py b/tests/unit/algorithms/detection/test_xai_detection_validity.py index 6d61beed752..6f684376064 100644 --- a/tests/unit/algorithms/detection/test_xai_detection_validity.py +++ b/tests/unit/algorithms/detection/test_xai_detection_validity.py @@ -80,7 +80,9 @@ def test_saliency_map_det(self, template): assert len(saliency_maps) == 2 assert saliency_maps[0].ndim == 3 assert saliency_maps[0].shape == self.ref_saliency_shapes[template.name] - assert np.all(np.abs(saliency_maps[0][0][0] - self.ref_saliency_vals_det[template.name]) <= 1) + actual_sal_vals = saliency_maps[0][0][0].astype(np.int8) + ref_sal_vals = self.ref_saliency_vals_det[template.name].astype(np.int8) + 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) From edd1a8c9c4fdf095d3489d6d5bdeab2752906cb5 Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Tue, 18 Jul 2023 09:53:57 +0900 Subject: [PATCH 027/146] Add e2e test for visual prompting (#2360) * (WIP) otx optimize * pre-commit * (WIP) set e2e * Remove nncf config * Add visual prompting requirement * Add visual prompting in tox * Add visual prompting in setup.py * Fix typo * Delete unused configuration.yaml * Edit test_name * Add to limit activation range * Update from `vp` to `visprompt` * Fix about no returning the first label * pre-commit * (WIP) otx optimize * pre-commit * (WIP) set e2e * Remove nncf config * Add visual prompting requirement * Add visual prompting in tox * Add visual prompting in setup.py * Fix typo * pre-commit * Add actions * Update tests/e2e/cli/visual_prompting/test_visual_prompting.py Co-authored-by: Jaeguk Hyun * Skip PTQ e2e test * Change task name * Remove skipped tc --------- Co-authored-by: Jaeguk Hyun --- .github/workflows/daily.yml | 2 + .github/workflows/pre_merge.yml | 2 + requirements/visual_prompting.txt | 4 + setup.py | 5 + .../configs/configuration.yaml | 56 ------ .../configs/sam_vit_b/configuration.yaml | 173 ------------------ .../sam_vit_b/ptq_optimization_config.py | 23 +++ .../visual_prompting/tasks/openvino.py | 17 +- .../visual_prompting_dataset_adapter.py | 4 +- tests/e2e/cli/visual_prompting/__init__.py | 3 + .../compressed_model.yml | 3 + .../visual_prompting/test_visual_prompting.py | 123 +++++++++++++ tests/test_suite/run_test_command.py | 56 +++++- .../adapter/test_visual_prompting_adapter.py | 1 + tox.ini | 4 +- 15 files changed, 229 insertions(+), 247 deletions(-) create mode 100644 requirements/visual_prompting.txt delete mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml create mode 100644 src/otx/algorithms/visual_prompting/configs/sam_vit_b/ptq_optimization_config.py create mode 100644 tests/e2e/cli/visual_prompting/__init__.py create mode 100644 tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_model.yml create mode 100644 tests/e2e/cli/visual_prompting/test_visual_prompting.py diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 635a7d1fa55..37e3e1a5185 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -24,6 +24,8 @@ jobs: 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: diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index 8ba34d5b009..204cbbd1fcf 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -104,6 +104,8 @@ jobs: 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: diff --git a/requirements/visual_prompting.txt b/requirements/visual_prompting.txt new file mode 100644 index 00000000000..ea9c9b50de1 --- /dev/null +++ b/requirements/visual_prompting.txt @@ -0,0 +1,4 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Visual Prompting Requirements. +scikit-image +pytorch-lightning>=1.7.0,<1.10.0 diff --git a/setup.py b/setup.py index c240b991049..c0b280e507d 100644 --- a/setup.py +++ b/setup.py @@ -149,6 +149,10 @@ def _cython_modules(): "base", "openvino", "segmentation", ] ), + "visual_prompting": get_requirements(requirement_files=[ + "base", "openvino", "visual_prompting", + ] + ), "full": get_requirements(requirement_files=[ "base", "openvino", @@ -156,6 +160,7 @@ def _cython_modules(): "classification", "detection", "segmentation", + "visual_prompting", "action", ] ), diff --git a/src/otx/algorithms/visual_prompting/configs/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/configuration.yaml index 1949d14f2a3..8a867588912 100644 --- a/src/otx/algorithms/visual_prompting/configs/configuration.yaml +++ b/src/otx/algorithms/visual_prompting/configs/configuration.yaml @@ -85,62 +85,6 @@ learning_parameters: visible_in_ui: true warning: null auto_hpo_state: NOT_POSSIBLE -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 diff --git a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml deleted file mode 100644 index 8a867588912..00000000000 --- a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/configuration.yaml +++ /dev/null @@ -1,173 +0,0 @@ -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: 9223372036854775807 - 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 -type: CONFIGURABLE_PARAMETERS -visible_in_ui: true 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..c6fc4168219 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/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.MIXED diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index 9b87e1ca130..545439a5c2f 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -28,13 +28,14 @@ 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.common.utils.logger import get_logger -from otx.algorithms.common.utils.utils import get_default_async_reqs_num from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( OTXVisualPromptingDataset, @@ -447,13 +448,17 @@ def optimize( if optimization_parameters is not None: optimization_parameters.update_progress(10 * i + 35 * (i - 1), 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 + 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(45 * i, None) diff --git a/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py b/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py index 5b83798dfb9..d428dc6afad 100644 --- a/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py +++ b/src/otx/core/data/adapter/visual_prompting_dataset_adapter.py @@ -60,7 +60,7 @@ def get_otx_dataset(self) -> DatasetEntity: 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): + if (new_label := self.updated_label_id.get(ann.label, None)) is not None: ann.label = new_label else: continue @@ -70,7 +70,7 @@ def get_otx_dataset(self) -> DatasetEntity: # 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): + if (new_label := self.updated_label_id.get(d_polygon.label, None)) is not None: d_polygon.label = new_label else: continue diff --git a/tests/e2e/cli/visual_prompting/__init__.py b/tests/e2e/cli/visual_prompting/__init__.py new file mode 100644 index 00000000000..9c68be83ef0 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_model.yml b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_model.yml new file mode 100644 index 00000000000..2bdafb331d5 --- /dev/null +++ b/tests/e2e/cli/visual_prompting/reference/Visual_Prompting_SAM_ViT_B/compressed_model.yml @@ -0,0 +1,3 @@ +TestToolsVisualPrompting: + pot: + number_of_fakequantizers: 210 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..4aa7ac9a35e --- /dev/null +++ b/tests/e2e/cli/visual_prompting/test_visual_prompting.py @@ -0,0 +1,123 @@ +"""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.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, + pot_eval_testing, + pot_optimize_testing, + pot_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.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() + +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_experimental.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/visual_prompting", experimental=True) + .filter(task_type="VISUAL_PROMPTING") + .templates + ) + 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=False) + 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=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_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) diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index f908b260b86..f2f3a1baa2b 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -511,7 +511,7 @@ def otx_demo_deployment_testing(template, root, otx_dir, args): assert os.path.exists(os.path.join(deployment_dir, "output")) -def pot_optimize_testing(template, root, otx_dir, args): +def pot_optimize_testing(template, root, otx_dir, args, is_visual_prompting=False): template_work_dir = get_template_dir(template, root) command_line = [ "otx", @@ -521,15 +521,38 @@ def pot_optimize_testing(template, root, otx_dir, args): 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", - f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml", "--output", f"{template_work_dir}/pot_{template.model_template_id}", ] + if is_visual_prompting: + command_line.extend( + [ + "--load-weights", + f"{template_work_dir}/exported_{template.model_template_id}/visual_prompting_decoder.xml", + ] + ) + else: + command_line.extend( + [ + "--load-weights", + f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml", + ] + ) + command_line.extend(["--workspace", f"{template_work_dir}"]) check_run(command_line) - assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml") - assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/openvino.bin") + if is_visual_prompting: + assert os.path.exists( + f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_image_encoder.xml" + ) + assert os.path.exists( + f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_image_encoder.bin" + ) + assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_decoder.xml") + assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_decoder.bin") + else: + assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml") + assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/openvino.bin") assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/label_schema.json") @@ -550,14 +573,17 @@ def _validate_fq_in_xml(xml_path, path_to_ref_data, compression_type, test_name, def pot_validate_fq_testing(template, root, otx_dir, task_type, test_name): template_work_dir = get_template_dir(template, root) - xml_path = f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml" + if task_type == "visual_prompting": + xml_path = f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_image_encoder.xml" + else: + xml_path = f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml" 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, "pot", test_name) -def pot_eval_testing(template, root, otx_dir, args): +def pot_eval_testing(template, root, otx_dir, args, is_visual_prompting=False): template_work_dir = get_template_dir(template, root) command_line = [ "otx", @@ -565,11 +591,23 @@ def pot_eval_testing(template, root, otx_dir, args): template.model_template_path, "--test-data-roots", f'{os.path.join(otx_dir, args["--test-data-roots"])}', - "--load-weights", - f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml", "--output", f"{template_work_dir}/pot_{template.model_template_id}", ] + if is_visual_prompting: + command_line.extend( + [ + "--load-weights", + f"{template_work_dir}/pot_{template.model_template_id}/visual_prompting_decoder.xml", + ] + ) + else: + command_line.extend( + [ + "--load-weights", + f"{template_work_dir}/pot_{template.model_template_id}/openvino.xml", + ] + ) command_line.extend(["--workspace", f"{template_work_dir}"]) check_run(command_line) assert os.path.exists(f"{template_work_dir}/pot_{template.model_template_id}/performance.json") diff --git a/tests/unit/core/data/adapter/test_visual_prompting_adapter.py b/tests/unit/core/data/adapter/test_visual_prompting_adapter.py index 75f399c8169..767b7e57e44 100644 --- a/tests/unit/core/data/adapter/test_visual_prompting_adapter.py +++ b/tests/unit/core/data/adapter/test_visual_prompting_adapter.py @@ -54,6 +54,7 @@ def test_get_otx_dataset(self, data_format: str, use_mask: bool, expected_shape: 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) diff --git a/tox.ini b/tox.ini index 9cd0892fc8b..8c78bbfa7ad 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ test_dir = iseg: cli/instance_segmentation seg: cli/semantic_segmentation act: cli/action + visprompt: cli/visual_prompting deps = py38: torch @ https://download.pytorch.org/whl/cu117/torch-1.13.1%2Bcu117-cp38-cp38-linux_x86_64.whl py38: torchvision @ https://download.pytorch.org/whl/cu117/torchvision-0.14.1%2Bcu117-cp38-cp38-linux_x86_64.whl @@ -42,6 +43,7 @@ extras = cls: classification seg: segmentation iseg: detection + visprompt: visual_prompting [testenv:pre-commit-all-{py38,py39,py310}] deps = @@ -52,7 +54,7 @@ commands = pre-commit run --all-files -[testenv:tests-{all,ano,cls,det,iseg,seg,act}-{py38,py39,py310}] +[testenv:tests-{all,ano,cls,det,iseg,seg,act,visprompt}-{py38,py39,py310}] deps = {[testenv]deps} -r{toxinidir}/requirements/dev.txt From 66e8a67793cef8fe00da7c59fe599f1820df008b Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Tue, 18 Jul 2023 10:05:53 +0900 Subject: [PATCH 028/146] Fix e2e (#2366) * Change e2e reference name * Update openvino eval threshold for multiclass classification * Change comment message * Fix tiling e2e tests --------- Co-authored-by: GalyaZalesskaya --- .../algorithms/detection/adapters/mmdet/datasets/tiling.py | 4 ++-- tests/e2e/cli/classification/test_classification.py | 3 ++- .../compressed_model.yml | 0 3 files changed, 4 insertions(+), 3 deletions(-) rename tests/e2e/cli/detection/reference/{Custom_Object_Detection_Gen3_MobileNetV2_ATSS => Custom_Object_Detection_Gen3_ATSS}/compressed_model.yml (100%) diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py index 0764430b953..7da6355db0a 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py @@ -512,8 +512,8 @@ def merge_maps(self, saliency_maps: Union[List[List[np.ndarray]], List[np.ndarra for cl_map in map: # find first class map which is not None if cl_map is not None and dtype is None: - dtype = map[0].dtype - feat_h, feat_w = map[0].shape + dtype = cl_map.dtype + feat_h, feat_w = cl_map.shape break if dtype is not None: break diff --git a/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py index 15c6018057e..3560a7bb68e 100644 --- a/tests/e2e/cli/classification/test_classification.py +++ b/tests/e2e/cli/classification/test_classification.py @@ -180,7 +180,8 @@ def test_otx_explain_openvino(self, template, tmp_dir_path): @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) + # FIXME [Jaeguk] Revert threshold to 0.2 when model api supports resize and centercrop. + otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.5, half_precision=half_precision) @e2e_pytest_component @pytest.mark.skipif(TT_STABILITY_TESTS, reason="This is TT_STABILITY_TESTS") diff --git a/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_MobileNetV2_ATSS/compressed_model.yml b/tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml similarity index 100% rename from tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_MobileNetV2_ATSS/compressed_model.yml rename to tests/e2e/cli/detection/reference/Custom_Object_Detection_Gen3_ATSS/compressed_model.yml From d0e864c21f38293378c073e66431d0410b2f2e58 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Tue, 18 Jul 2023 13:57:29 +0900 Subject: [PATCH 029/146] Add Dino head unit tests (#2344) Recover DINO head unit tests --- .../adapters/mmdet/models/heads/__init__.py | 4 + .../models/heads/test_custom_dino_head.py | 212 ++++++++++++++++++ 2 files changed, 216 insertions(+) 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 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..d6b774f2244 --- /dev/null +++ b/tests/unit/algorithms/detection/adapters/mmdet/models/heads/test_custom_dino_head.py @@ -0,0 +1,212 @@ +"""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.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): + 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), + }, + ] + 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]) From c28c612e2e30df7ebe2fed88307a6d7f2f1d3ba2 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 18 Jul 2023 21:12:30 +0900 Subject: [PATCH 030/146] Update for release 1.4.0rc2 (#2370) * update for release 1.4.0rc2 * Add skip mark for unstable unit tests --------- Co-authored-by: jaegukhyun --- requirements/base.txt | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- .../adapters/mmdet/models/heads/test_custom_dino_head.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 02ea591ac61..384031b53d2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro==1.4.0rc2 +datumaro==1.4.0rc3 psutil scipy>=1.8 bayesian-optimization>=1.2.0 diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 86aa5654450..5b4fe7a3779 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx @ git+https://github.com/openvinotoolkit/training_extensions/@e4269e035bcaa3903c6b99044f46c42fcbf98f25#egg=otx +otx==1.4.0rc2 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime 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 index d6b774f2244..f2914a305fa 100644 --- 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 @@ -82,6 +82,7 @@ def setup(self): self.bbox_head.test_cfg = test_cfg @e2e_pytest_unit + @pytest.mark.skip("Test is unstable") def test_forward_train(self): inputs = [ torch.zeros([2, 256, 92, 95]), @@ -168,6 +169,7 @@ def test_forward_train(self): assert len(losses) == 39 @e2e_pytest_unit + @pytest.mark.skip("Test is unstable") def test_simple_test_bboxes(self): feats = [ torch.zeros([2, 256, 100, 134]), From 0c4be3ca67154ce2408c7ae7303d6e9f1cbdc21c Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Wed, 19 Jul 2023 02:58:07 +0200 Subject: [PATCH 031/146] Fix NNCF training on CPU (#2373) --- src/otx/algorithms/common/adapters/mmcv/nncf/runners.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py b/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py index 8a44337b40f..f9f6f3b111c 100644 --- a/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/runners.py @@ -111,7 +111,11 @@ def dump_checkpoint_fn(model, compression_ctrl, nncf_runner, save_dir): self._eval_hook._save_ckpt(self, nncf_runner.best_val_metric_value) return self._eval_hook.best_ckpt_path - uncompressed_model_accuracy = self.model.module.nncf._uncompressed_model_accuracy + 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, From 85bfe9f38b3bf9597420e44c20f64f8c4294f70b Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 19 Jul 2023 11:45:38 +0900 Subject: [PATCH 032/146] Align label order between Geti and OTX (#2369) * align label order * align with pre-commit * update CHANGELOG.md * deal with edge case * update type hint --- CHANGELOG.md | 1 + src/otx/api/entities/label_schema.py | 10 ++++---- tests/unit/api/entities/test_label_schema.py | 25 ++++++++++++++++++- ...test_prediction_to_annotation_converter.py | 1 - 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ab9e6698f..f775d1707e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ All notable changes to this project will be documented in this file. - Fix the bug that auto adapt batch size is unavailable with IterBasedRunner () - Fix the bug that learning rate isn't scaled when multi-GPU trianing is enabled() +- Fix the bug that label order is misaligned when model is deployed from Geti () ### Known issues diff --git a/src/otx/api/entities/label_schema.py b/src/otx/api/entities/label_schema.py index adca67154f9..2d69c5469ff 100644 --- a/src/otx/api/entities/label_schema.py +++ b/src/otx/api/entities/label_schema.py @@ -21,14 +21,14 @@ logger = logging.getLogger(__name__) -def natural_sort_label_id(target: Union[ID, LabelEntity, ScoredLabel]) -> List: +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[int]: A list of integers representing the numeric substrings in the ID + List[Union[int, str]]: A list of integers representing the numeric substrings in the ID in the order they appear. Example: @@ -41,9 +41,9 @@ def natural_sort_label_id(target: Union[ID, LabelEntity, ScoredLabel]) -> List: if isinstance(target, (LabelEntity, ScoredLabel)): target = target.id_ - if isinstance(target, int): - return [target] - return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", target)] + 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): diff --git a/tests/unit/api/entities/test_label_schema.py b/tests/unit/api/entities/test_label_schema.py index f24c96ea90d..bcfc30ccf50 100644 --- a/tests/unit/api/entities/test_label_schema.py +++ b/tests/unit/api/entities/test_label_schema.py @@ -5,7 +5,7 @@ # import pytest -from networkx.classes.reportviews import EdgeDataView, NodeView, OutMultiEdgeDataView +from networkx.classes.reportviews import NodeView, OutMultiEdgeDataView from otx.api.entities.color import Color from otx.api.entities.id import ID @@ -19,11 +19,34 @@ 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 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 index c76956b2a21..f88847fbe9c 100644 --- 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 @@ -750,7 +750,6 @@ def test_classification_to_annotation_init(self): ) label_schema = LabelSchemaEntity(label_groups=[label_group, other_label_group]) converter = ClassificationToAnnotationConverter(label_schema=label_schema) - assert converter.labels == non_empty_labels + other_non_empty_labels assert not converter.empty_label assert converter.label_schema == label_schema assert converter.hierarchical From 43eb8381b7092c06e56594538bbc4f3f2c5d7d3d Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Wed, 19 Jul 2023 18:25:26 +0900 Subject: [PATCH 033/146] Remove CenterCrop from Classification test pipeline and editing missing docs link (#2375) * Fix missing link for docs and removing centercrop for classification data pipeline * Revert the test threshold --- .../algorithms/segmentation/semantic_segmentation.rst | 4 ++-- .../classification/configs/base/data/data_pipeline.py | 5 ++--- tests/e2e/cli/classification/test_classification.py | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst b/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst index 07dd08df58c..80c04464f41 100644 --- a/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst +++ b/docs/source/guide/explanation/algorithms/segmentation/semantic_segmentation.rst @@ -42,7 +42,7 @@ Dataset Format For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset Management Framework (Datumaro) `_. -At this end we support `Common Semantic Segmentation `_ data format. +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:: @@ -278,4 +278,4 @@ It can be launched only with supervised (incremental) training type. .. Incremental Learning .. ******************** -.. To be added soon \ No newline at end of file +.. To be added soon diff --git a/src/otx/algorithms/classification/configs/base/data/data_pipeline.py b/src/otx/algorithms/classification/configs/base/data/data_pipeline.py index 1cfebc32c5f..2ac44214b01 100644 --- a/src/otx/algorithms/classification/configs/base/data/data_pipeline.py +++ b/src/otx/algorithms/classification/configs/base/data/data_pipeline.py @@ -20,7 +20,7 @@ __resize_target_size = 224 __train_pipeline = [ - dict(type="RandomResizedCrop", size=224, efficientnet_style=True), + 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"]), @@ -29,8 +29,7 @@ ] __test_pipeline = [ - dict(type="Resize", size=(256, -1)), - dict(type="CenterCrop", crop_size=224), + 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/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py index 3560a7bb68e..15c6018057e 100644 --- a/tests/e2e/cli/classification/test_classification.py +++ b/tests/e2e/cli/classification/test_classification.py @@ -180,8 +180,7 @@ def test_otx_explain_openvino(self, template, tmp_dir_path): @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" - # FIXME [Jaeguk] Revert threshold to 0.2 when model api supports resize and centercrop. - otx_eval_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.5, half_precision=half_precision) + 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") From 09068119378493644efdb5c28f87f0617aa2ef07 Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Thu, 20 Jul 2023 17:00:18 +0900 Subject: [PATCH 034/146] Fix H-label classification (#2377) * Fix h-labelissue * Update unit tests * Make black happy * Fix unittests * Make black happy * Fix update heades information func * Update the logic: consider the loss per batch --- .../adapters/mmcls/configurer.py | 5 ++++ .../adapters/mmcls/datasets/otx_datasets.py | 17 +++++++++-- .../custom_hierarchical_linear_cls_head.py | 30 +++++++++++-------- ...custom_hierarchical_non_linear_cls_head.py | 30 +++++++++++-------- .../classification/utils/cls_utils.py | 1 + .../adapters/mmcls/data/test_datasets.py | 25 +++++++++++++++- .../test_custom_hierarchical_cls_head.py | 27 ++++++++++++----- .../adapters/mmcls/test_configurer.py | 11 +++++++ 8 files changed, 108 insertions(+), 38 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index d0ecbfb4e2c..fe4529679a9 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -132,6 +132,11 @@ def configure_model(self, cfg, ir_options): # noqa: C901 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: diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py index 70a4500d1b5..7522be7ea33 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -309,9 +309,7 @@ def load_annotations(self): if item_labels: num_cls_heads = self.hierarchical_info["num_multiclass_heads"] - class_indices = [0] * ( - self.hierarchical_info["num_multiclass_heads"] + self.hierarchical_info["num_multilabel_classes"] - ) + 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: @@ -329,6 +327,19 @@ def load_annotations(self): 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.""" 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 index 5b3245a4f40..6776756bb61 100644 --- 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 @@ -87,22 +87,26 @@ def forward_train(self, cls_score, gt_label, **kwargs): 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"]): - 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() - head_logits = head_logits[valid_mask, :] - multiclass_loss = self.loss(head_logits, head_gt) - losses["loss"] += multiclass_loss + 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 self.hierarchical_info["num_multiclass_heads"] > 1: - losses["loss"] /= self.hierarchical_info["num_multiclass_heads"] + losses["loss"] /= num_effective_heads_in_batch if self.compute_multilabel_loss: head_gt = gt_label[:, self.hierarchical_info["num_multiclass_heads"] :] 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 index 4b2691157e1..5397818fbf3 100644 --- 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 @@ -117,22 +117,26 @@ def forward_train(self, cls_score, gt_label, **kwargs): 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"]): - 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() - head_logits = head_logits[valid_mask, :] - multiclass_loss = self.loss(head_logits, head_gt) - losses["loss"] += multiclass_loss + 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 self.hierarchical_info["num_multiclass_heads"] > 1: - losses["loss"] /= self.hierarchical_info["num_multiclass_heads"] + losses["loss"] /= num_effective_heads_in_batch if self.compute_multilabel_loss: head_gt = gt_label[:, self.hierarchical_info["num_multiclass_heads"] :] diff --git a/src/otx/algorithms/classification/utils/cls_utils.py b/src/otx/algorithms/classification/utils/cls_utils.py index 8bb2b9630f2..23dc1ba1fa6 100644 --- a/src/otx/algorithms/classification/utils/cls_utils.py +++ b/src/otx/algorithms/classification/utils/cls_utils.py @@ -62,6 +62,7 @@ def get_multihead_class_info(label_schema: LabelSchemaEntity): # pylint: disabl "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 diff --git a/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py b/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py index 41e6890e02d..b4719680125 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/data/test_datasets.py @@ -142,9 +142,32 @@ def test_metric_hierarchical_adapter(self): 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/models/heads/test_custom_hierarchical_cls_head.py b/tests/unit/algorithms/classification/adapters/mmcls/models/heads/test_custom_hierarchical_cls_head.py index 11f6e100996..8f8ec9b6550 100644 --- 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 @@ -24,13 +24,14 @@ def head_type(self) -> None: @pytest.fixture(autouse=True) def setup(self, head_type) -> None: - self.num_classes = 3 - self.head_dim = 5 + self.num_classes = 6 + self.head_dim = 10 self.cls_heads_info = { - "num_multiclass_heads": 1, - "num_multilabel_classes": 1, - "head_idx_to_logits_range": {"0": (0, 2)}, - "num_single_label_classes": 2, + "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") @@ -43,13 +44,23 @@ def setup(self, head_type) -> None: ) self.default_head.init_weights() self.default_input = torch.ones((2, self.head_dim)) - self.default_gt = torch.zeros((2, 2)) + 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 + 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: diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index 96e1efbf685..ae058a4d56d 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -23,6 +23,11 @@ def setup(self) -> None: self.model_cfg = MPAConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model.py")) self.data_cfg = MPAConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "data_pipeline.py")) + self.multilabel_model_cfg = MPAConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_multilabel.py")) + self.hierarchical_model_cfg = MPAConfig.fromfile( + os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_hierarchical.py") + ) + @e2e_pytest_unit def test_configure(self, mocker): mock_cfg_base = mocker.patch.object(ClassificationConfigurer, "configure_base") @@ -119,6 +124,12 @@ def test_configure_model(self): assert self.model_cfg.model_task assert self.model_cfg.model.head.in_channels == 960 + multilabel_model_cfg = self.multilabel_model_cfg + self.configurer.configure_model(multilabel_model_cfg, ir_options) + + h_label_model_cfg = self.hierarchical_model_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": ""}} From f1234d2d3ddc467c22cc134b5cd7f953a5161f90 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 21 Jul 2023 09:15:37 +0900 Subject: [PATCH 035/146] Update for release 1.4 (#2380) * updated for 1.4.0rc3 * update changelog & release note * bump datumaro version up --------- Co-authored-by: Songki Choi --- CHANGELOG.md | 23 +++++++++---------- docs/source/guide/release_notes/index.rst | 13 +++++++++++ requirements/base.txt | 2 +- src/otx/__init__.py | 2 +- .../exportable_code/demo/requirements.txt | 2 +- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f775d1707e7..63011d14033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,32 +8,31 @@ All notable changes to this project will be documented in this file. - Support encrypted dataset training () - Add custom max iou assigner to prevent CPU OOM when large annotations are used () -- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional (https://github.com/openvinotoolkit/training_extensions/pull/2195) -- Add per-class XAI saliency maps for Mask R-CNN model (https://github.com/openvinotoolkit/training_extensions/pull/2227) +- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional () +- Add per-class XAI saliency maps for Mask R-CNN model () - Add new object detector Deformable DETR () -- Add new object detector DINO() -- Add new visual prompting task: train/eval (https://github.com/openvinotoolkit/training_extensions/pull/2203) -- Add new visual prompting task: export (https://github.com/openvinotoolkit/training_extensions/pull/2274) -- Add new visual prompting task: deploy (https://github.com/openvinotoolkit/training_extensions/pull/2311) -- Add new visual prompting task: documentation (https://github.com/openvinotoolkit/training_extensions/pull/2354) -- Add new visual prompting task: optimize (PTQ) (https://github.com/openvinotoolkit/training_extensions/pull/2318) +- Add new object detector DINO () +- Add new visual prompting task (, , , , ) - Add new object detector ResNeXt101-ATSS () ### Enhancements - Introduce channel_last parameter to improve the performance () -- Decrease a time for making a workspace () +- Decrease time for making a workspace () - Set persistent_workers and pin_memory as True in detection task () -- New algorithm for Semi-SL semantic segmentation based on metric lerning via class prototypes (https://github.com/openvinotoolkit/training_extensions/pull/2156) -- Self-SL for classification now can recieve just folder with any images to start contrastive pretraining (https://github.com/openvinotoolkit/training_extensions/pull/2219) +- New algorithm for Semi-SL semantic segmentation based on metric learning via class prototypes () +- Self-SL for classification now can recieve just folder with any images to start contrastive pretraining () - Update OpenVINO version to 2023.0, and NNCF verion to 2.5 () -- Improve XAI saliency map generation for tiling detection and tiling instance segmentation (https://github.com/openvinotoolkit/training_extensions/pull/2240) +- Improve XAI saliency map generation for tiling detection and tiling instance segmentation () +- Remove CenterCrop from Classification test pipeline and editing missing docs link() ### Bug fixes - Fix the bug that auto adapt batch size is unavailable with IterBasedRunner () - Fix the bug that learning rate isn't scaled when multi-GPU trianing is enabled() - Fix the bug that label order is misaligned when model is deployed from Geti () +- Fix NNCF training on CPU () +- Fix H-label classification () ### Known issues diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 616d98a1837..ef0c1f6d721 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -1,6 +1,19 @@ Releases ======== +*************** +[v1.4.0] (3Q23) +*************** + +- Support encrypted dataset training +- Add custom max iou assigner to prevent CPU OOM when large annotations are used +- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional +- Add per-class XAI saliency maps for Mask R-CNN model +- Add new object detector Deformable DETR +- Add new object detector DINO +- Add new visual prompting task +- Add new object detector ResNeXt101-ATSS + *************** [v1.3.0] (2Q23) *************** diff --git a/requirements/base.txt b/requirements/base.txt index 384031b53d2..a901eb88599 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro==1.4.0rc3 +datumaro==1.4.0rc4 psutil scipy>=1.8 bayesian-optimization>=1.2.0 diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 0656f85ae5c..1a39b601165 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0rc2" +__version__ = "1.4.0rc3" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 5b4fe7a3779..040f57ee8b4 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.0rc2 +otx==1.4.0rc3 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 7319b208bed82b752198d5c5cfb8e8e689bac3ca Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Fri, 21 Jul 2023 02:30:35 +0200 Subject: [PATCH 036/146] Switch to PTQ for sseg (#2374) * Switch to PTQ for sseg * Update log messages --- src/otx/algorithms/action/adapters/openvino/task.py | 2 +- src/otx/algorithms/anomaly/tasks/openvino.py | 2 +- src/otx/algorithms/segmentation/adapters/openvino/task.py | 2 +- .../configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py | 1 - .../configs/ocr_lite_hrnet_s_mod2/ptq_optimization_config.py | 1 - .../configs/ocr_lite_hrnet_x_mod3/ptq_optimization_config.py | 1 - src/otx/algorithms/visual_prompting/tasks/openvino.py | 2 +- 7 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/otx/algorithms/action/adapters/openvino/task.py b/src/otx/algorithms/action/adapters/openvino/task.py index c1703d8cbbb..d22e43ee652 100644 --- a/src/otx/algorithms/action/adapters/openvino/task.py +++ b/src/otx/algorithms/action/adapters/openvino/task.py @@ -334,4 +334,4 @@ def optimize( if optimization_parameters is not None: optimization_parameters.update_progress(100, None) - logger.info("POT optimization completed") + logger.info("PTQ optimization completed") diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index 45d156ec9fe..cc65ef74294 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -333,7 +333,7 @@ def optimize( if optimization_parameters is not None: optimization_parameters.update_progress(100, None) - logger.info("POT optimization completed") + logger.info("PTQ optimization completed") def load_inferencer(self) -> OpenVINOInferencer: """Create the OpenVINO inferencer object. diff --git a/src/otx/algorithms/segmentation/adapters/openvino/task.py b/src/otx/algorithms/segmentation/adapters/openvino/task.py index 270216505c6..c25496a50e6 100644 --- a/src/otx/algorithms/segmentation/adapters/openvino/task.py +++ b/src/otx/algorithms/segmentation/adapters/openvino/task.py @@ -403,4 +403,4 @@ def optimize( if optimization_parameters is not None: optimization_parameters.update_progress(100, None) - logger.info("POT optimization completed") + logger.info("PTQ optimization completed") 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 index 7fbd8b81f06..1a2f9a8e589 100644 --- 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 @@ -18,7 +18,6 @@ statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 ), ), - backend_params={"use_pot": True}, ) preset = QuantizationPreset.MIXED 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 index 44890b47bc4..442c139c4a2 100644 --- 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 @@ -18,7 +18,6 @@ statistics_type=StatisticsType.QUANTILE, aggregator_type=AggregatorType.MAX, quantile_outlier_prob=1e-4 ), ), - backend_params={"use_pot": True}, ) preset = QuantizationPreset.MIXED 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 index 02f6d84e478..b678c2eeeb3 100644 --- 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 @@ -18,7 +18,6 @@ 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/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index 545439a5c2f..ce9f5b94512 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -487,4 +487,4 @@ def optimize( if optimization_parameters is not None: optimization_parameters.update_progress(100, None) - logger.info("POT optimization completed") + logger.info("PTQ optimization completed") From 22aaf2a9aba60e71f54ae12dde245f87579dc6b7 Mon Sep 17 00:00:00 2001 From: Harim Kang Date: Fri, 21 Jul 2023 13:38:20 +0900 Subject: [PATCH 037/146] Fix invalid import structures in otx.api (#2383) Update tiler.py --- src/otx/api/utils/tiler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/otx/api/utils/tiler.py b/src/otx/api/utils/tiler.py index 19ae38cd31b..a84e63a8734 100644 --- a/src/otx/api/utils/tiler.py +++ b/src/otx/api/utils/tiler.py @@ -13,14 +13,11 @@ from openvino.model_api.models.utils import DetectionResult -from otx.algorithms.common.utils.logger import get_logger 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 -logger = get_logger() - class Tiler: """Tile Image into (non)overlapping Patches. Images are tiled in order to efficiently process large images. @@ -78,8 +75,6 @@ def tile(self, image: np.ndarray) -> List[List[int]]: x2 = min(loc_j + self.tile_size, width) y2 = min(loc_i + self.tile_size, height) coords.append([loc_j, loc_i, x2, y2]) - logger.debug(f"------------------------> Num tiles: {len(coords)}") - logger.debug(f"------------------------> {height}x{width} ~ {self.tile_size}") return coords def filter_tiles_by_objectness( From 8690bf9c638e43b22c6b6d7cf15348bd837a13d6 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 21 Jul 2023 15:06:35 +0900 Subject: [PATCH 038/146] Update for 1.4.0rc4 (#2385) update for release 1.4.0rc4 --- CHANGELOG.md | 2 ++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63011d14033..f5f7a151d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file. - Update OpenVINO version to 2023.0, and NNCF verion to 2.5 () - Improve XAI saliency map generation for tiling detection and tiling instance segmentation () - Remove CenterCrop from Classification test pipeline and editing missing docs link() +- Switch to PTQ for sseg () ### Bug fixes @@ -33,6 +34,7 @@ All notable changes to this project will be documented in this file. - Fix the bug that label order is misaligned when model is deployed from Geti () - Fix NNCF training on CPU () - Fix H-label classification () +- Fix invalid import structures in otx.api () ### Known issues diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 1a39b601165..b978ac81519 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0rc3" +__version__ = "1.4.0rc4" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 040f57ee8b4..0e4f19f207c 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.0rc3 +otx==1.4.0rc4 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From efed624f08a706f1850ea6cec0090f32d43304ea Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Tue, 25 Jul 2023 16:46:13 +0300 Subject: [PATCH 039/146] [release 1.4.0] XAI: Return saliency maps for Mask RCNN IR async infer (#2395) * Return saliency maps for openvino async infer * add workaround to fix yapf importing error --------- Co-authored-by: eunwoosh --- src/otx/algorithms/detection/adapters/openvino/task.py | 2 +- src/otx/cli/utils/importing.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index 4ad7abba8fc..08b5423eef2 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -164,7 +164,7 @@ def _async_callback(self, request: Any, callback_args: tuple) -> None: else: features = ( copy.deepcopy(prediction["feature_vector"].reshape(-1)), - copy.deepcopy(prediction["saliency_map"][0]), + self.get_saliency_map(prediction, preprocessing_meta), ) result_handler(id, processed_prediciton, features) diff --git a/src/otx/cli/utils/importing.py b/src/otx/cli/utils/importing.py index 173cdd8bf89..5fbf45eaddf 100644 --- a/src/otx/cli/utils/importing.py +++ b/src/otx/cli/utils/importing.py @@ -20,6 +20,12 @@ 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 = { From 4fed654c232f9471d2fc026fd34077e850916ebf Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 26 Jul 2023 08:37:52 +0900 Subject: [PATCH 040/146] Update for release 1.4.0 (#2399) update version string Co-authored-by: Sungman Cho --- CHANGELOG.md | 1 + requirements/base.txt | 2 +- src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f7a151d8e..94f693f3f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to this project will be documented in this file. - Fix NNCF training on CPU () - Fix H-label classification () - Fix invalid import structures in otx.api () +- Add for async inference calculating saliency maps from predictions (Mask RCNN IR) () ### Known issues diff --git a/requirements/base.txt b/requirements/base.txt index a901eb88599..e01183cefac 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro==1.4.0rc4 +datumaro~=1.4.0 psutil scipy>=1.8 bayesian-optimization>=1.2.0 diff --git a/src/otx/__init__.py b/src/otx/__init__.py index b978ac81519..8d4abc10699 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0rc4" +__version__ = "1.4.0" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 0e4f19f207c..0cd74613981 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.0rc4 +otx==1.4.0 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 183c2cbad64b04e1c9abca75a993b83a11f60ec7 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 31 Jul 2023 13:48:18 +0900 Subject: [PATCH 041/146] Fix broken links in documentation (#2405) * fix docs links to datumaro's docs * fix docs links to otx's docs * bump version to 1.4.1 --- CHANGELOG.md | 8 ++++- README.md | 34 ++++++++++--------- docs/source/conf.py | 2 +- .../auto_configuration.rst | 8 ++--- .../additional_features/fast_data_loading.rst | 4 +-- .../object_detection/object_detection.rst | 8 ++--- .../algorithms/visual_prompting/index.rst | 6 ++-- .../source/guide/get_started/introduction.rst | 2 +- .../tutorials/base/how_to_train/detection.rst | 4 +-- src/otx/__init__.py | 2 +- .../exportable_code/demo/requirements.txt | 2 +- 11 files changed, 44 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f693f3f3b..9149890fdf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## \[v1.4.1\] + +### Bug fixes + +- Fix broken links in documentation () + ## \[v1.4.0\] ### New features @@ -245,7 +251,7 @@ All notable changes to this project will be documented in this file. - Enhance `find` command to find configurations of supported tasks / algorithms / models / backbones - Introduce `build` command to customize task or model configurations in isolated workspace - Auto-config feature to automatically select the right algorithm and default model for the `train` & `build` command by detecting the task type of given input dataset -- Improve [documentation](https://openvinotoolkit.github.io/training_extensions/stable/guide/get_started/introduction.html) +- Improve [documentation](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/introduction.html) - Improve training performance by introducing enhanced loss for the few-shot transfer ### Bug fixes diff --git a/README.md b/README.md index 296ce17eaae..8a15d7a590f 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ --- -[Key Features](#key-features) • -[Installation](https://openvinotoolkit.github.io/training_extensions/latest/guide/get_started/installation.html) • -[Documentation](https://openvinotoolkit.github.io/training_extensions/latest/index.html) • +[Key Features](#key-features) +[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/installation.html) +[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.1/index.html) [License](#license) [![PyPI](https://img.shields.io/pypi/v/otx)](https://pypi.org/project/otx) @@ -54,7 +54,7 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: - **Action recognition** including action classification and detection - **Anomaly recognition** tasks including anomaly classification, detection and segmentation -OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/algorithms/index.html): +OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/algorithms/index.html): - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks - **Semi-supervised learning** @@ -64,9 +64,9 @@ OpenVINO™ Training Extensions will provide the following features in coming re - **Distributed training** to accelerate the training process when you have multiple GPUs - **Half-precision training** to save GPUs memory and use larger batch sizes -- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. -- OpenVINO™ Training Extensions uses [Datumaro](https://openvinotoolkit.github.io/datumaro/stable/index.html) as the backend to hadle datasets. Thanks to that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We constantly working to extend supported formats to give more freedom of datasets format choice. -- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. +- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. +- OpenVINO™ Training Extensions uses [Datumaro](https://openvinotoolkit.github.io/datumaro/v1.4.1/index.html) as the backend to hadle datasets. Thanks to that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We constantly working to extend supported formats to give more freedom of datasets format choice. +- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. --- @@ -74,7 +74,7 @@ OpenVINO™ Training Extensions will provide the following features in coming re ### Installation -Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/latest/guide/get_started/installation.html). +Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/installation.html). Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. @@ -90,20 +90,22 @@ Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. - `otx demo` allows one to apply a trained model on the custom data or the online footage from a web camera and see how it will work in a real-life scenario. - `otx explain` runs explain algorithm on the provided data and outputs images with the saliency maps to show how your model makes predictions. -You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/latest/guide/get_started/cli_commands.html). +You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/cli_commands.html). --- ## Updates -### v1.3.0 (2Q23) +### v1.4.0 (3Q23) -- Support direct annotation input for COCO format () -- Action task supports multi GPU training. () -- Support storage cache in Apache Arrow using Datumaro for action tasks () -- Add a simplified greedy labels postprocessing for hierarchical classification (). -- Support auto adapting batch size () -- Support auto adapting num_workers () +- Support encrypted dataset training () +- Add custom max iou assigner to prevent CPU OOM when large annotations are used () +- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional () +- Add per-class XAI saliency maps for Mask R-CNN model () +- Add new object detector Deformable DETR () +- Add new object detector DINO () +- Add new visual prompting task (, , , , ) +- Add new object detector ResNeXt101-ATSS () ### Release History diff --git a/docs/source/conf.py b/docs/source/conf.py index b71e9c40771..4c46b04d59b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,7 @@ project = 'OpenVINO™ Training Extensions' copyright = '2023, OpenVINO™ Training Extensions Contributors' author = 'OpenVINO™ Training Extensions Contributors' -release = '1.4.0' +release = '1.4.1' # -- General configuration --------------------------------------------------- # diff --git a/docs/source/guide/explanation/additional_features/auto_configuration.rst b/docs/source/guide/explanation/additional_features/auto_configuration.rst index e3c895cc47c..5b824c01c0a 100644 --- a/docs/source/guide/explanation/additional_features/auto_configuration.rst +++ b/docs/source/guide/explanation/additional_features/auto_configuration.rst @@ -24,7 +24,7 @@ It will recognize the task by analyzing the dataset and if there is no splits fo .. note:: - Currently, Datumaro auto-split feature supports 3 formats: `Imagenet `_ (multi-class classification), `COCO `_ (detection) and `Cityscapes `_ (semantic segmentation). + 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. @@ -32,14 +32,14 @@ After dataset preparation, the training will be started with the middle-sized te Supported dataset formats for each task: - classification: `Imagenet `_, `COCO `_ (multi-label), :ref:`custom hierarchical ` -- object detection: `COCO `_, `Pascal-VOC `_, `YOLO `_ -- semantic segmentation: `Common Semantic Segmentation `_, `Pascal-VOC `_, `Cityscapes `_, `ADE20k `_ +- object detection: `COCO `_, `Pascal-VOC `_, `YOLO `_ +- semantic segmentation: `Common Semantic Segmentation `_, `Pascal-VOC `_, `Cityscapes `_, `ADE20k `_ - action classification: `CVAT `_ - action detection: `CVAT `_ - anomaly classification: `MVTec `_ - anomaly detection: `MVTec `_ - anomaly segmentation: `MVTec `_ -- instance segmentation: `COCO `_, `Pascal-VOC `_ +- 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 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: 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 46767e2c8bd..c219a5ba456 100644 --- a/docs/source/guide/explanation/additional_features/fast_data_loading.rst +++ b/docs/source/guide/explanation/additional_features/fast_data_loading.rst @@ -48,7 +48,7 @@ Storage Caching OpenVINO™ Training Extensions uses `Datumaro `_ under the hood for dataset managements. -Since Datumaro `supports `_ +Since Datumaro `supports `_ `Apache Arrow `_, OpenVINO™ Training Extensions can exploit fast data loading using memory-mapped arrow file at the expanse of storage consumtion. @@ -67,7 +67,7 @@ One could change it by modifying ``OTX_CACHE`` environment variable. $ OTX_CACHE=/path/to/cache otx train .. params --algo_backend.storage_cache_scheme JPEG/75 -Please refere `Datumaro document `_ +Please refere `Datumaro document `_ for available schemes to choose but we recommend ``JPEG/75`` for fast data loaidng. .. [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/algorithms/object_detection/object_detection.rst b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst index b744f255537..8e423e03025 100644 --- a/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst +++ b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst @@ -39,7 +39,7 @@ Dataset Format ************** At the current point we support `COCO `_ and -`Pascal-VOC `_ dataset formats. +`Pascal-VOC `_ dataset formats. Learn more about the formats by following the links above. Here is an example of expected format for COCO dataset: .. code:: @@ -103,7 +103,7 @@ In addition to these models, we supports experimental models for object detectio | `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. +`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. @@ -121,10 +121,10 @@ To see which public backbones are available for the task, the following command 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. +For `COCO `__ dataset the accuracy of pretrained weights is shown, and we report official COCO mAP with AP50. Except for COCO, we report AP50 as performance metric. -5 datasets were selected as transfer learning datasets. +5 datasets were selected as transfer learning datasets. `BDD100K `_ is the largest dataset among we used. 70000 images are used as train images and 10000 images are used for validation. `Brackish `_ and `Plantdoc `_ are datasets of medium size. They have around 10000 images for train and 1500 images for validation. `BCCD `_ and `Chess pieces `_ are datasets of small size. They have around 300 images for train and 100 images for validation. diff --git a/docs/source/guide/explanation/algorithms/visual_prompting/index.rst b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst index d0843de8b8c..d85e02e49ba 100644 --- a/docs/source/guide/explanation/algorithms/visual_prompting/index.rst +++ b/docs/source/guide/explanation/algorithms/visual_prompting/index.rst @@ -41,11 +41,11 @@ For the dataset handling inside OpenVINO™ Training Extensions, we use `Dataset We support three dataset formats for visual prompting: -- `Common Semantic Segmentation `_ for semantic segmentation +- `Common Semantic Segmentation `_ for semantic segmentation -- `COCO `_ for instance segmentation +- `COCO `_ for instance segmentation -- `Pascal VOC `_ for instance segmentation and semantic segmentation +- `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: diff --git a/docs/source/guide/get_started/introduction.rst b/docs/source/guide/get_started/introduction.rst index af3d6c21df2..d40a673b62f 100644 --- a/docs/source/guide/get_started/introduction.rst +++ b/docs/source/guide/get_started/introduction.rst @@ -39,7 +39,7 @@ OpenVINO™ Training Extensions will provide the :doc:`following features <../ex - **Distributed training** to accelerate the training process when you have multiple GPUs - **Half-precision training** to save GPUs memory and use larger batch sizes - Integrated, efficient :doc:`hyper-parameter optimization module <../explanation/additional_features/hpo>` (**HPO**). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. -- OpenVINO™ Training Extensions uses `Datumaro `_ as the backend to handle datasets. On account of that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. In the future there will be more supported formats available to give more freedom of datasets format choice. +- OpenVINO™ Training Extensions uses `Datumaro `_ as the backend to handle datasets. On account of that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. In the future there will be more supported formats available to give more freedom of datasets format choice. - Improved :doc:`auto-configuration functionality <../explanation/additional_features/auto_configuration>`. OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. ************ 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 dc36b7e66ab..726e44feaa4 100644 --- a/docs/source/guide/tutorials/base/how_to_train/detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/detection.rst @@ -52,8 +52,8 @@ Dataset preparation Currently, we support the following object detection dataset formats: - `COCO `_ - - `Pascal-VOC `_ - - `YOLO `_ + - `Pascal-VOC `_ + - `YOLO `_ 1. Clone a repository with `WGISD dataset `_. diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 8d4abc10699..8bfc6e5b9de 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0" +__version__ = "1.4.1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 0cd74613981..ec9bf79a597 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.0 +otx==1.4.1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 2858515e69bd234cd11f8e1127999472d59f5244 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Wed, 2 Aug 2023 02:59:44 +0300 Subject: [PATCH 042/146] Update exportable code README (#2411) --- src/otx/api/usecases/exportable_code/demo/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otx/api/usecases/exportable_code/demo/README.md b/src/otx/api/usecases/exportable_code/demo/README.md index 617aa6501db..89bd18528b7 100644 --- a/src/otx/api/usecases/exportable_code/demo/README.md +++ b/src/otx/api/usecases/exportable_code/demo/README.md @@ -4,6 +4,7 @@ Exportable code is a .zip archive that contains simple demo to get and visualize ## Structure of generated zip +- `README.md` - model - `model.xml` - `model.bin` @@ -12,7 +13,6 @@ Exportable code is a .zip archive that contains simple demo to get and visualize - model_wrappers (Optional) - `__init__.py` - model_wrappers required to run demo - - `README.md` - `LICENSE` - `demo.py` - `requirements.txt` From c80bfe66a0320742a6ba2b95b30bcc18331c1445 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 2 Aug 2023 17:08:24 +0900 Subject: [PATCH 043/146] Updated for release 1.4.1 (#2412) updated for release 1.4.1 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9149890fdf7..11aeb614a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## \[v1.4.1\] +### Enhancements + +- Update the README file in exportable code () + ### Bug fixes - Fix broken links in documentation () From 92006b6e73b6f9395a2c11fa255b907353c709bf Mon Sep 17 00:00:00 2001 From: Evgeny Tsykunov Date: Fri, 18 Aug 2023 03:40:43 +0200 Subject: [PATCH 044/146] Add workaround for the incorrect meta info M-RCNN (used for XAI) (#2437) Add workaround for the incorrect mata info --- .../adapters/openvino/model_wrappers/openvino_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py b/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py index aaa5baa3f42..2936197fa4f 100644 --- a/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py +++ b/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py @@ -136,8 +136,8 @@ def get_saliency_map_from_prediction(self, outputs, meta, num_classes): if classes.shape[0] == 1: classes = classes.squeeze(0) - scale_x = meta["resized_shape"][1] / meta["original_shape"][1] - scale_y = meta["resized_shape"][0] / meta["original_shape"][0] + scale_x = meta["resized_shape"][0] / meta["original_shape"][1] + scale_y = meta["resized_shape"][1] / meta["original_shape"][0] boxes[:, 0::2] /= scale_x boxes[:, 1::2] /= scale_y From 75040e36a11a2e7ba971f0174fc5cf1e39aae8d1 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 18 Aug 2023 18:07:51 +0900 Subject: [PATCH 045/146] Add model category attributes to model template (#2439) Add model category attributes to model template * Add model category & status fields in model template * Add is_default_for_task attr to model template * Update model templates with category attrs * Add integration tests for model templates consistency * Fix license & doc string * Fix typo * Refactor test cases * Refactor common tests by generator --------- Signed-off-by: Songki Choi --- .../classification/padim/template.yaml | 4 + .../classification/stfpm/template.yaml | 3 + .../configs/detection/padim/template.yaml | 4 + .../configs/detection/stfpm/template.yaml | 3 + .../configs/segmentation/padim/template.yaml | 4 + .../configs/segmentation/stfpm/template.yaml | 3 + .../efficientnet_b0_cls_incr/template.yaml | 4 + .../efficientnet_v2_s_cls_incr/template.yaml | 3 + .../template.yaml | 3 + .../detection/cspdarknet_yolox/template.yaml | 8 +- .../detection/mobilenetv2_atss/template.yaml | 9 +-- .../detection/mobilenetv2_ssd/template.yaml | 8 +- .../efficientnetb2b_maskrcnn/template.yaml | 3 + .../resnet50_maskrcnn/template.yaml | 4 + .../efficientnetb2b_maskrcnn/template.yaml | 3 + .../resnet50_maskrcnn/template.yaml | 4 + .../ocr_lite_hrnet_18_mod2/template.yaml | 4 + .../ocr_lite_hrnet_s_mod2/template.yaml | 3 + .../ocr_lite_hrnet_x_mod3/template.yaml | 3 + src/otx/api/entities/model_template.py | 32 +++++++- .../anomaly/test_anomaly_classification.py | 20 ++--- .../cli/anomaly/test_anomaly_detection.py | 20 ++--- .../cli/anomaly/test_anomaly_segmentation.py | 20 ++--- .../cli/classification/test_classification.py | 6 +- .../cli/detection/test_detection.py | 6 +- .../test_instance_segmentation.py | 6 +- .../test_rotated_detection.py | 25 +++++++ .../test_segmentation.py | 48 +++++++----- tests/test_suite/run_test_command.py | 52 ++++++++++--- .../unit/api/entities/test_model_template.py | 74 +++++++++++++++---- 30 files changed, 286 insertions(+), 103 deletions(-) create mode 100644 tests/integration/cli/instance_segmentation/test_rotated_detection.py diff --git a/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml b/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml index b63060a72d5..21c557530a9 100644 --- a/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml +++ b/src/otx/algorithms/anomaly/configs/classification/padim/template.yaml @@ -29,3 +29,7 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml index ccdffab89b9..42ef6129a32 100644 --- a/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml +++ b/src/otx/algorithms/anomaly/configs/classification/stfpm/template.yaml @@ -35,3 +35,6 @@ training_targets: # Computational Complexity gigaflops: 5.6 size: 21.1 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml b/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml index 20b0823f922..93bb00a0dfd 100644 --- a/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml +++ b/src/otx/algorithms/anomaly/configs/detection/padim/template.yaml @@ -29,3 +29,7 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml index da6075e438f..5d4d49832c0 100644 --- a/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml +++ b/src/otx/algorithms/anomaly/configs/detection/stfpm/template.yaml @@ -35,3 +35,6 @@ training_targets: # Computational Complexity gigaflops: 5.6 size: 21.1 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml b/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml index 65026654fae..8ac2fdb8a13 100644 --- a/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml +++ b/src/otx/algorithms/anomaly/configs/segmentation/padim/template.yaml @@ -29,3 +29,7 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml index 1c72b4ddad7..fa17fc6f07e 100644 --- a/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml +++ b/src/otx/algorithms/anomaly/configs/segmentation/stfpm/template.yaml @@ -35,3 +35,6 @@ training_targets: # Computational Complexity gigaflops: 5.6 size: 21.1 + +# Model spec +model_category: ACCURACY 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 index 129ba98a535..1781b32e82e 100644 --- a/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml +++ b/src/otx/algorithms/classification/configs/efficientnet_b0_cls_incr/template.yaml @@ -57,3 +57,7 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/classification/configs/efficientnet_v2_s_cls_incr/template.yaml index c31b29bf2d1..514815b1631 100644 --- 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 @@ -57,3 +57,6 @@ training_targets: # Stats. gigaflops: 5.76 size: 20.23 + +# Model spec +model_category: ACCURACY 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 index 4def2ca9b2d..60573d606c5 100644 --- 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 @@ -57,3 +57,6 @@ training_targets: # Stats. gigaflops: 0.44 size: 4.29 + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox/template.yaml b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox/template.yaml index f7cf03fcced..7b37b8773fe 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox/template.yaml @@ -57,8 +57,6 @@ training_targets: # Stats. gigaflops: 6.5 size: 20.4 -# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. -# inference_targets: -# - CPU -# - GPU -# - VPU + +# Model spec +model_category: SPEED diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml index 04d1a2f6e60..c13a6dd69e7 100644 --- a/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_atss/template.yaml @@ -57,8 +57,7 @@ training_targets: # Stats. gigaflops: 20.6 size: 9.1 -# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. -# inference_targets: -# - CPU -# - GPU -# - VPU + +# Model spec +model_category: ACCURACY +is_default_for_task: true diff --git a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml index fb5d0a954fd..d44ce0e283d 100644 --- a/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/mobilenetv2_ssd/template.yaml @@ -57,8 +57,6 @@ training_targets: # Stats. gigaflops: 9.4 size: 7.6 -# # Inference options. Defined by OpenVINO capabilities, not Algo Backend or Platform. -# inference_targets: -# - CPU -# - GPU -# - VPU + +# Model spec +model_category: BALANCE 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 index c1e40ce6beb..6c318d2ee54 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml @@ -60,3 +60,6 @@ training_targets: # Stats. gigaflops: 68.48 size: 13.27 + +# Model spec +model_category: SPEED 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 index 355aa291bed..985dce2a69a 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/template.yaml @@ -60,3 +60,7 @@ training_targets: # 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/efficientnetb2b_maskrcnn/template.yaml b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml index 62707ac4028..34891f7e14e 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/template.yaml @@ -60,3 +60,6 @@ training_targets: # Stats. gigaflops: 68.48 size: 13.27 + +# Model spec +model_category: SPEED 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 index 69cb5104514..31e74540bda 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/template.yaml @@ -60,3 +60,7 @@ training_targets: # Stats. gigaflops: 533.8 size: 177.9 + +# Model spec +model_category: ACCURACY +is_default_for_task: true 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 index ca8bd75be0a..37d39dffb86 100644 --- 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 @@ -58,3 +58,7 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_s_mod2/template.yaml index 6e3de18b4f9..ac2d7a8518f 100644 --- 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 @@ -59,3 +59,6 @@ training_targets: # 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/template.yaml b/src/otx/algorithms/segmentation/configs/ocr_lite_hrnet_x_mod3/template.yaml index 0d6d2e8f27c..8e24a5837af 100644 --- 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 @@ -59,3 +59,6 @@ training_targets: # Stats. gigaflops: 13.97 size: 6.4 + +# Model spec +model_category: ACCURACY diff --git a/src/otx/api/entities/model_template.py b/src/otx/api/entities/model_template.py index 03426cc64d8..fe7b100c7fb 100644 --- a/src/otx/api/entities/model_template.py +++ b/src/otx/api/entities/model_template.py @@ -1,6 +1,6 @@ """This file defines the ModelConfiguration, ModelEntity and Model classes.""" -# Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import copy @@ -459,6 +459,30 @@ class EntryPoints: 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: @@ -499,6 +523,9 @@ class ModelTemplate: 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 @@ -528,6 +555,9 @@ class ModelTemplate: 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.""" diff --git a/tests/integration/cli/anomaly/test_anomaly_classification.py b/tests/integration/cli/anomaly/test_anomaly_classification.py index 05bcd4f723a..c9173e08128 100644 --- a/tests/integration/cli/anomaly/test_anomaly_classification.py +++ b/tests/integration/cli/anomaly/test_anomaly_classification.py @@ -1,23 +1,13 @@ """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 +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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.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 ( @@ -28,6 +18,7 @@ otx_eval_testing, otx_export_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -44,6 +35,9 @@ 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) diff --git a/tests/integration/cli/anomaly/test_anomaly_detection.py b/tests/integration/cli/anomaly/test_anomaly_detection.py index 944f49aa318..0cb33a51fd8 100644 --- a/tests/integration/cli/anomaly/test_anomaly_detection.py +++ b/tests/integration/cli/anomaly/test_anomaly_detection.py @@ -1,23 +1,13 @@ """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 +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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.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 ( @@ -28,6 +18,7 @@ otx_eval_testing, otx_export_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -44,6 +35,9 @@ 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) diff --git a/tests/integration/cli/anomaly/test_anomaly_segmentation.py b/tests/integration/cli/anomaly/test_anomaly_segmentation.py index 3918291e25e..17483504515 100644 --- a/tests/integration/cli/anomaly/test_anomaly_segmentation.py +++ b/tests/integration/cli/anomaly/test_anomaly_segmentation.py @@ -1,23 +1,13 @@ """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 +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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.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 ( @@ -28,6 +18,7 @@ otx_eval_testing, otx_export_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -44,6 +35,9 @@ 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) diff --git a/tests/integration/cli/classification/test_classification.py b/tests/integration/cli/classification/test_classification.py index 45a98517284..5104af6b40b 100644 --- a/tests/integration/cli/classification/test_classification.py +++ b/tests/integration/cli/classification/test_classification.py @@ -1,5 +1,5 @@ """Tests for Classification with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -30,6 +30,7 @@ otx_hpo_testing, otx_resume_testing, otx_train_testing, + generate_model_template_testing, ) # Pre-train w/ 'label_0', 'label_1', 'label_2' classes @@ -82,6 +83,9 @@ 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) diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index e69810a9cf0..b9ba80011ba 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -1,5 +1,5 @@ """Tests for Class-Incremental Learning for object detection with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import copy @@ -28,6 +28,7 @@ otx_hpo_testing, otx_resume_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -81,6 +82,9 @@ 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) diff --git a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py index e5958fa2d04..c2ba3b9c3df 100644 --- a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py +++ b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py @@ -1,5 +1,5 @@ """Tests for Class-Incremental Learning for object detection with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import copy @@ -24,6 +24,7 @@ otx_hpo_testing, otx_resume_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -67,6 +68,9 @@ templates_ids_inc_convnext.extend([template_experimental.model_template_id]) +TestInstanceSegmentationModelTemplates = generate_model_template_testing(templates) + + class TestInstanceSegmentationCLI: @e2e_pytest_component @pytest.mark.parametrize("template", templates_inc_convnext, ids=templates_ids_inc_convnext) 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..6e89e7f67a8 --- /dev/null +++ b/tests/integration/cli/instance_segmentation/test_rotated_detection.py @@ -0,0 +1,25 @@ +"""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.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 generate_model_template_testing + + +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) diff --git a/tests/integration/cli/semantic_segmentation/test_segmentation.py b/tests/integration/cli/semantic_segmentation/test_segmentation.py index d558c39a15e..81884f948d8 100644 --- a/tests/integration/cli/semantic_segmentation/test_segmentation.py +++ b/tests/integration/cli/semantic_segmentation/test_segmentation.py @@ -1,5 +1,5 @@ """Tests for Semantic segmentation with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import copy @@ -9,6 +9,7 @@ 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, @@ -21,6 +22,7 @@ otx_hpo_testing, otx_resume_testing, otx_train_testing, + generate_model_template_testing, ) args = { @@ -71,8 +73,11 @@ default_template = parse_model_template( os.path.join("src/otx/algorithms/segmentation/configs", "ocr_lite_hrnet_18_mod2", "template.yaml") ) -templates = [default_template] -templates_ids = [default_template.model_template_id] +default_templates = [default_template] +default_templates_ids = [default_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] # add integration test for semi-sl with new SegNext model and prototype based approach # other tests will be updated accordingly after fully transfer to segnext templates @@ -83,22 +88,25 @@ templates_ids_inc_segnext = [segnext_experimental_template.model_template_id, default_template.model_template_id] +TestSemanticSegmentationModelTemplates = generate_model_template_testing(templates) + + class TestSegmentationCLI: @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) def test_otx_train_supcon(self, template, tmp_dir_path): 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) + @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", templates, ids=templates_ids) + @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) @@ -111,57 +119,57 @@ def test_otx_resume(self, template, tmp_dir_path): otx_resume_testing(template, tmp_dir_path, otx_dir, args1) @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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) @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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" if template.entrypoints.nncf is None: @@ -171,7 +179,7 @@ def test_nncf_optimize(self, template, tmp_dir_path): @e2e_pytest_component @pytest.mark.skipif(MULTI_GPU_UNAVAILABLE, reason="The number of gpu is insufficient") - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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) @@ -200,14 +208,14 @@ def test_otx_multi_gpu_train_semisl(self, template, tmp_dir_path): assert os.path.exists(f"{template_dir}/semisl") @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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", templates, ids=templates_ids) + @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) @@ -215,7 +223,7 @@ def test_otx_multi_gpu_train_selfsl(self, template, tmp_dir_path): otx_train_testing(template, tmp_dir_path, otx_dir, args_selfsl_multigpu) @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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) @@ -224,7 +232,7 @@ def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_ otx_train_testing(template, tmp_dir_path, otx_dir, adapting_bs_args) @e2e_pytest_component - @pytest.mark.parametrize("template", templates, ids=templates_ids) + @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"]) diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index f2f3a1baa2b..8554366e587 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -1,16 +1,7 @@ -# Copyright (C) 2021 Intel Corporation +"""Common test case and helpersi for OTX""" +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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 asyncio import json @@ -25,9 +16,11 @@ import pytest 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 def get_template_rel_dir(template): @@ -1083,3 +1076,38 @@ def otx_train_auto_config(root, otx_dir: str, args: Dict[str, str], use_output: 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 diff --git a/tests/unit/api/entities/test_model_template.py b/tests/unit/api/entities/test_model_template.py index 820d2d64a44..d00b4f5dc19 100644 --- a/tests/unit/api/entities/test_model_template.py +++ b/tests/unit/api/entities/test_model_template.py @@ -1,17 +1,7 @@ -# Copyright (C) 2020-2021 Intel Corporation +"""Tests for model template entity""" +# Copyright (C) 2020-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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 itertools from os import remove @@ -31,6 +21,8 @@ HyperParameterData, InstantiationType, ModelOptimizationMethod, + ModelCategory, + ModelStatus, ModelTemplate, NullModelTemplate, TargetDevice, @@ -97,6 +89,9 @@ def check_model_attributes(model: ModelTemplate, expected_values: dict): 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) @@ -726,6 +721,56 @@ def test_entrypoints(self): ) +@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 @@ -788,6 +833,9 @@ def optional_model_parameters(self): 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 From 6b09e65acf7610bfb27fab88a7417f0dcf964b80 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 21 Aug 2023 14:00:17 +0900 Subject: [PATCH 046/146] Update for 1.4.2rc1 (#2441) update for release 1.4.2rc1 --- CHANGELOG.md | 12 +++++++++++- README.md | 14 +++++++------- docs/source/conf.py | 2 +- src/otx/__init__.py | 2 +- .../usecases/exportable_code/demo/requirements.txt | 2 +- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11aeb614a0e..4c6aaebb02f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## \[v1.4.2\] + +### Enhancements + +- Add model category attributes to model template () + +### Bug fixes + +- Add workaround for the incorrect meta info M-RCNN (used for XAI) () + ## \[v1.4.1\] ### Enhancements @@ -255,7 +265,7 @@ All notable changes to this project will be documented in this file. - Enhance `find` command to find configurations of supported tasks / algorithms / models / backbones - Introduce `build` command to customize task or model configurations in isolated workspace - Auto-config feature to automatically select the right algorithm and default model for the `train` & `build` command by detecting the task type of given input dataset -- Improve [documentation](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/introduction.html) +- Improve [documentation](https://openvinotoolkit.github.io/training_extensions/1.0.0/guide/get_started/introduction.html) - Improve training performance by introducing enhanced loss for the few-shot transfer ### Bug fixes diff --git a/README.md b/README.md index 8a15d7a590f..a75f8436fa1 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ --- [Key Features](#key-features) -[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/installation.html) -[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.1/index.html) +[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html) +[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.2/index.html) [License](#license) [![PyPI](https://img.shields.io/pypi/v/otx)](https://pypi.org/project/otx) @@ -54,7 +54,7 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: - **Action recognition** including action classification and detection - **Anomaly recognition** tasks including anomaly classification, detection and segmentation -OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/algorithms/index.html): +OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/algorithms/index.html): - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks - **Semi-supervised learning** @@ -64,9 +64,9 @@ OpenVINO™ Training Extensions will provide the following features in coming re - **Distributed training** to accelerate the training process when you have multiple GPUs - **Half-precision training** to save GPUs memory and use larger batch sizes -- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. +- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. - OpenVINO™ Training Extensions uses [Datumaro](https://openvinotoolkit.github.io/datumaro/v1.4.1/index.html) as the backend to hadle datasets. Thanks to that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We constantly working to extend supported formats to give more freedom of datasets format choice. -- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. +- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. --- @@ -74,7 +74,7 @@ OpenVINO™ Training Extensions will provide the following features in coming re ### Installation -Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/installation.html). +Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html). Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. @@ -90,7 +90,7 @@ Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. - `otx demo` allows one to apply a trained model on the custom data or the online footage from a web camera and see how it will work in a real-life scenario. - `otx explain` runs explain algorithm on the provided data and outputs images with the saliency maps to show how your model makes predictions. -You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.1/guide/get_started/cli_commands.html). +You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/cli_commands.html). --- diff --git a/docs/source/conf.py b/docs/source/conf.py index 4c46b04d59b..32bc942b667 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,7 @@ project = 'OpenVINO™ Training Extensions' copyright = '2023, OpenVINO™ Training Extensions Contributors' author = 'OpenVINO™ Training Extensions Contributors' -release = '1.4.1' +release = '1.4.2' # -- General configuration --------------------------------------------------- # diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 8bfc6e5b9de..76a1873f9e7 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.1" +__version__ = "1.4.2rc1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index ec9bf79a597..da1676a7aed 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.1 +otx==1.4.2rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 4f9c2f1429659ee1097848c73d179cd97b0e5a43 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Mon, 21 Aug 2023 13:54:02 +0300 Subject: [PATCH 047/146] Fix label list order for h-label classification (#2440) * Fix label list for h-label cls * Fix unit tests --- .../classification/adapters/openvino/task.py | 17 +++++++++++++-- src/otx/algorithms/classification/task.py | 13 ++++++++++-- .../classification/utils/__init__.py | 2 ++ .../classification/utils/cls_utils.py | 21 +++++++++++++++++++ .../test_classification_openvino_task.py | 1 + 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/openvino/task.py b/src/otx/algorithms/classification/adapters/openvino/task.py index 741c9a6526a..ed21e928af0 100644 --- a/src/otx/algorithms/classification/adapters/openvino/task.py +++ b/src/otx/algorithms/classification/adapters/openvino/task.py @@ -36,6 +36,7 @@ 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 @@ -228,12 +229,18 @@ def add_prediction(id: int, predicted_scene: AnnotationSceneEntity, aux_data: tu if saliency_map is not None and repr_vector is not None: 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 + ) add_saliency_maps_to_dataset_item( dataset_item=dataset_item, saliency_map=saliency_map, model=self.model, - labels=self.task_environment.get_labels(), + labels=label_list, predicted_scored_labels=item_labels, explain_predicted_classes=explain_predicted_classes, process_saliency_maps=process_saliency_maps, @@ -284,6 +291,12 @@ def explain( 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): predicted_scene, _, saliency_map, _, _ = self.inferencer.predict(dataset_item.numpy) if saliency_map is None: @@ -298,7 +311,7 @@ def explain( dataset_item=dataset_item, saliency_map=saliency_map, model=self.model, - labels=self.task_environment.get_labels(), + labels=label_list, predicted_scored_labels=item_labels, explain_predicted_classes=explain_predicted_classes, process_saliency_maps=process_saliency_maps, diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index 3c74230dcab..bd0500a3ec1 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -28,6 +28,7 @@ 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, @@ -345,6 +346,10 @@ def _add_predictions_to_dataset( 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)): @@ -373,7 +378,7 @@ def _add_predictions_to_dataset( dataset_item=dataset_item, saliency_map=saliency_map, model=self._task_environment.model, - labels=self._labels, + labels=label_list, predicted_scored_labels=item_labels, explain_predicted_classes=explain_predicted_classes, process_saliency_maps=process_saliency_maps, @@ -436,13 +441,17 @@ def _add_explanations_to_dataset( ): """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=self._labels, + labels=label_list, predicted_scored_labels=item_labels, explain_predicted_classes=explain_predicted_classes, process_saliency_maps=process_saliency_maps, diff --git a/src/otx/algorithms/classification/utils/__init__.py b/src/otx/algorithms/classification/utils/__init__.py index 533b871de17..536cd56bff7 100644 --- a/src/otx/algorithms/classification/utils/__init__.py +++ b/src/otx/algorithms/classification/utils/__init__.py @@ -8,10 +8,12 @@ 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", diff --git a/src/otx/algorithms/classification/utils/cls_utils.py b/src/otx/algorithms/classification/utils/cls_utils.py index 23dc1ba1fa6..b1506ccc8e7 100644 --- a/src/otx/algorithms/classification/utils/cls_utils.py +++ b/src/otx/algorithms/classification/utils/cls_utils.py @@ -117,3 +117,24 @@ def get_cls_model_api_configuration(label_schema: LabelSchemaEntity, inference_c 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/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py b/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py index c34c9c6ccbc..504265d4fd9 100644 --- a/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py +++ b/tests/unit/algorithms/classification/tasks/test_classification_openvino_task.py @@ -182,6 +182,7 @@ def test_explain(self, mocker): self.fake_input, ), ) + self.cls_ov_task.inferencer.model.hierarchical = False updpated_dataset = self.cls_ov_task.explain(self.dataset) assert updpated_dataset is not None From 230f6f238b604a378572220a411352f360ce5ccd Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 22 Aug 2023 10:37:44 +0900 Subject: [PATCH 048/146] Modified fq numbers for lite HRNET (#2445) modified fq numbers for lite HRNET --- .../compressed_model.yml | 2 +- .../compressed_model.yml | 2 +- .../compressed_model.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 05285214ecb..f72ca6fca9d 100644 --- 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 @@ -2,4 +2,4 @@ TestToolsMPASegmentation: nncf: number_of_fakequantizers: 586 pot: - number_of_fakequantizers: 518 + number_of_fakequantizers: 494 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 index 7834c23fadd..99a7b525c57 100644 --- 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 @@ -2,4 +2,4 @@ TestToolsMPASegmentation: nncf: number_of_fakequantizers: 436 pot: - number_of_fakequantizers: 392 + 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 index 2d032f078b8..f3d5c0f32b2 100644 --- 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 @@ -2,4 +2,4 @@ TestToolsMPASegmentation: nncf: number_of_fakequantizers: 1138 pot: - number_of_fakequantizers: 978 + number_of_fakequantizers: 942 From 498bd85f96446431a3cb473aa7ae9c4165abd703 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Thu, 24 Aug 2023 10:59:11 +0200 Subject: [PATCH 049/146] Update PTQ ignored scope for hrnet 18 mod2 (#2449) Update ptq ignored scope for hrnet 18 mod2 --- .../configs/ocr_lite_hrnet_18_mod2/ptq_optimization_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 1a2f9a8e589..4e5ce69c89c 100644 --- 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 @@ -23,6 +23,7 @@ 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", @@ -102,5 +103,5 @@ "/aggregator/Add_1", "/aggregator/Add_2", "/backbone/stage2/stage2.1/Add", - ] + ], ) From 948881e51cc12ac2103c34f0ffbde0055ac35a12 Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Fri, 25 Aug 2023 12:24:29 +0200 Subject: [PATCH 050/146] Fix OpenVINO inference for legacy models (#2450) * bug fix for legacy openvino models * Add tests * Specific exceptions --------- --- src/otx/algorithms/anomaly/tasks/openvino.py | 42 ++++++++++-- src/otx/cli/utils/io.py | 4 ++ .../algorithms/anomaly/tasks/test_openvino.py | 66 ++++++++++++++++++- 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index cc65ef74294..7859cfbfb36 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -26,6 +26,7 @@ import numpy as np import openvino.runtime as ov from addict import Dict as ADDict +from anomalib.data.utils.transform import get_transforms from anomalib.deploy import OpenVINOInferencer from nncf.common.quantization.structs import QuantizationPreset from omegaconf import OmegaConf @@ -216,16 +217,47 @@ def get_metadata(self) -> Dict: """Get Meta Data.""" metadata = {} if self.task_environment.model is not None: - metadata = json.loads(self.task_environment.model.get_data("metadata").decode()) - 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() + 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 = { + "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. diff --git a/src/otx/cli/utils/io.py b/src/otx/cli/utils/io.py index 73941eb1a6c..3770fb279bf 100644 --- a/src/otx/cli/utils/io.py +++ b/src/otx/cli/utils/io.py @@ -51,6 +51,10 @@ "visual_prompting_image_encoder.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. ) diff --git a/tests/unit/algorithms/anomaly/tasks/test_openvino.py b/tests/unit/algorithms/anomaly/tasks/test_openvino.py index 58b6b2a8450..82d1174bb97 100644 --- a/tests/unit/algorithms/anomaly/tasks/test_openvino.py +++ b/tests/unit/algorithms/anomaly/tasks/test_openvino.py @@ -3,27 +3,40 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -import pytest +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. @@ -77,3 +90,54 @@ def test_openvino(self, tmpdir, setup_task_environment): # deploy openvino_task.deploy(output_model) assert output_model.exportable_code is not None + + @patch.multiple(OpenVINOTask, get_config=MagicMock(), load_inferencer=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) From 9ebdad61e753095e395096d50e57b6d0a80e1502 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 28 Aug 2023 08:52:07 +0900 Subject: [PATCH 051/146] Update for 1.4.2rc2 (#2455) update for release 1.4.2rc2 --- CHANGELOG.md | 2 ++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6aaebb02f..688b3a2c3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Bug fixes - Add workaround for the incorrect meta info M-RCNN (used for XAI) () +- Fix label list order for h-label classification () +- Modified fq numbers for lite HRNET e2e tests () ## \[v1.4.1\] diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 76a1873f9e7..fb7987c10a7 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2rc1" +__version__ = "1.4.2rc2" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index da1676a7aed..c3f9b07d92e 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2rc1 +otx==1.4.2rc2 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From c553c109ba855391dc33ceb3a182008baf476d34 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Mon, 28 Aug 2023 09:33:35 +0200 Subject: [PATCH 052/146] Prevent zero-sized saliency map in tiling if tile size is too big (#2452) * Prevent zero-sized saliency map in tiling if tile size is too big * Prevent zero-sized saliency in tiling (PyTorch) * Add unit tests for Tiler merge features methods --------- Co-authored-by: Galina --- .../detection/adapters/mmdet/datasets/tiling.py | 2 +- src/otx/api/utils/tiler.py | 3 +-- .../detection/tiling/test_tiling_tile_classifier.py | 10 +++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py index 7da6355db0a..59befcb791c 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/tiling.py @@ -527,8 +527,8 @@ def merge_maps(self, saliency_maps: Union[List[List[np.ndarray]], List[np.ndarra for orig_image in self.cached_results: img_idx = orig_image["index"] - ratios[img_idx] = np.array([feat_h, feat_w]) / self.tile_size 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]) diff --git a/src/otx/api/utils/tiler.py b/src/otx/api/utils/tiler.py index a84e63a8734..f645b2ace77 100644 --- a/src/otx/api/utils/tiler.py +++ b/src/otx/api/utils/tiler.py @@ -325,14 +325,13 @@ def merge_maps(self, features: List) -> np.ndarray: 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, feat_w]) / self.tile_size + 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]) diff --git a/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py b/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py index 0c20806146f..b014a533c5c 100644 --- a/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py +++ b/tests/unit/algorithms/detection/tiling/test_tiling_tile_classifier.py @@ -78,8 +78,12 @@ def test_openvino_sync(self, mocker): mocked_model.return_value = mocker.MagicMock(spec=OTXMaskRCNNModel, model_adapter=adapter_mock) 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"}) + 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), @@ -93,6 +97,10 @@ def test_openvino_sync(self, mocker): mock_predict = mocker.patch.object( ov_inferencer.tiler.classifier, "infer_sync", return_value={"tile_prob": 0.5} ) + 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) ov_task.inferencer.predict = partial(ov_task.inferencer.predict, mode="sync") From 250df8ebe784d6bf7aed5bc391231c919977d559 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 29 Aug 2023 13:06:00 +0900 Subject: [PATCH 053/146] Update pot fq reference number (#2456) update pot fq reference number to 15 --- .../compressed_model.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index f72ca6fca9d..aa6e9acdd15 100644 --- 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 @@ -2,4 +2,4 @@ TestToolsMPASegmentation: nncf: number_of_fakequantizers: 586 pot: - number_of_fakequantizers: 494 + number_of_fakequantizers: 15 From eac33725f1b0d830decbe216e1f9583de53176ce Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 1 Sep 2023 15:36:37 +0900 Subject: [PATCH 054/146] Bump datumaro version to 1.5.0rc0 (#2470) bump datumaro version to 1.5.0rc0 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index e01183cefac..0bb5aa8f044 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro~=1.4.0 +datumaro==1.5.0rc0 psutil scipy>=1.8 bayesian-optimization>=1.2.0 From 2fd7c3e0b203c4d07563f308bdcb282c7a414ee2 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 1 Sep 2023 17:30:32 +0900 Subject: [PATCH 055/146] Set tox version constraint (#2472) set tox version constraint - https://github.com/tox-dev/tox/issues/3110 --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index d52a27ce00a..d6648af378e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,5 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Development Requirements. # -tox==4.4.5 pre-commit==2.20.0 pylint pytest @@ -10,3 +9,4 @@ pytest-mock onnx==1.13.0 onnxruntime==1.14.1 pytest-csv +tox>=4.5.1.1 From 00a84df3d611b362dcc1b98588f4941d70fb60ea Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Tue, 5 Sep 2023 01:03:41 +0200 Subject: [PATCH 056/146] Bug fix for albumentations (#2467) * bug fix for legacy openvino models * Address albumentation issue --------- Co-authored-by: Ashwin Vaidya --- src/otx/algorithms/anomaly/tasks/openvino.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index 7859cfbfb36..3800b264e0d 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -242,7 +242,8 @@ def _populate_metadata_legacy(self, model: ModelEntity) -> Dict[str, Any]: to_tensor=True, ) metadata = { - "transform": transform.to_dict(), + # 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, From efcf62f2c27170c8bb94e53f140112ec01850566 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 6 Sep 2023 10:50:03 +0900 Subject: [PATCH 057/146] update for release 1.4.2rc3 --- CHANGELOG.md | 3 +++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688b3a2c3d9..55e1c8e7d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,13 @@ All notable changes to this project will be documented in this file. ### Enhancements +- Bump datumaro version to 1.5.0rc0 () +- Set tox version constraint () - Add model category attributes to model template () ### Bug fixes +- Bug fix for albumentations () - Add workaround for the incorrect meta info M-RCNN (used for XAI) () - Fix label list order for h-label classification () - Modified fq numbers for lite HRNET e2e tests () diff --git a/src/otx/__init__.py b/src/otx/__init__.py index fb7987c10a7..563346135fe 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2rc2" +__version__ = "1.4.2rc3" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index c3f9b07d92e..ad0d92c1429 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2rc2 +otx==1.4.2rc3 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 3ed1e9a9063d94a59d92d38b120dabf7d39b34e1 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Thu, 7 Sep 2023 06:12:12 +0200 Subject: [PATCH 058/146] Add a dummy hierarchical config required by MAPI (#2483) --- .../classification/adapters/openvino/task.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/otx/algorithms/classification/adapters/openvino/task.py b/src/otx/algorithms/classification/adapters/openvino/task.py index ed21e928af0..ac4ed4b874e 100644 --- a/src/otx/algorithms/classification/adapters/openvino/task.py +++ b/src/otx/algorithms/classification/adapters/openvino/task.py @@ -114,6 +114,16 @@ def __init__( 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, "otx_classification", self.configuration, preload=True) self.converter = ClassificationToAnnotationConverter(self.label_schema) From 98ad1244da9657c04ae41d7d3e75f0e57b4facfe Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 8 Sep 2023 12:08:57 +0900 Subject: [PATCH 059/146] bump version to 1.4.2rc4 --- src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 563346135fe..1eb8b4ec35d 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2rc3" +__version__ = "1.4.2rc4" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index ad0d92c1429..449be47c3ed 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2rc3 +otx==1.4.2rc4 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 5539c18be865babd521f6f19f7cdd93570abe196 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 18 Sep 2023 18:10:39 +0900 Subject: [PATCH 060/146] Bump datumaro version (#2502) * bump datumaro version * remove deprecated/reomved attribute usage of the datumaro --- requirements/base.txt | 2 +- tests/unit/core/data/test_helpers.py | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 0bb5aa8f044..80129e6edff 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro==1.5.0rc0 +datumaro~=1.5.0 psutil scipy>=1.8 bayesian-optimization>=1.2.0 diff --git a/tests/unit/core/data/test_helpers.py b/tests/unit/core/data/test_helpers.py index f2f47de6990..6bc973c4159 100644 --- a/tests/unit/core/data/test_helpers.py +++ b/tests/unit/core/data/test_helpers.py @@ -6,8 +6,12 @@ import os import cv2 -import datumaro as dm 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 @@ -107,7 +111,7 @@ def generate_datumaro_dataset_item( image_shape: np.array = np.array((5, 5, 3)), mask_shape: np.array = np.array((5, 5)), temp_dir: Optional[str] = None, -) -> dm.DatasetItem: +) -> DatasetItem: """Generate Datumaro DatasetItem. Args: @@ -119,20 +123,22 @@ def generate_datumaro_dataset_item( temp_dir (str): directory to save image data Returns: - dm.DatasetItem: Datumaro DatasetItem + DatasetItem: Datumaro DatasetItem """ ann_task_dict = { - "classification": dm.Label(label=0), - "detection": dm.Bbox(1, 2, 3, 4, label=0), - "segmentation": dm.Mask(np.zeros(mask_shape)), + "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 dm.DatasetItem(id=item_id, subset=subset, image=path, annotations=[ann_task_dict[task]]) + return DatasetItem(id=item_id, subset=subset, media=ImageFromFile(path), annotations=[ann_task_dict[task]]) - return dm.DatasetItem(id=item_id, subset=subset, image=np.ones(image_shape), 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( @@ -141,7 +147,7 @@ def generate_datumaro_dataset( num_data: int = 1, image_shape: np.array = np.array((5, 5, 3)), mask_shape: np.array = np.array((5, 5)), -) -> dm.Dataset: +) -> Dataset: """Generate Datumaro Dataset. Args: @@ -154,7 +160,7 @@ def generate_datumaro_dataset( Returns: dm.Dataset: Datumaro Dataset """ - dataset_items: dm.DatasetItem = [] + dataset_items: DatasetItem = [] for subset in subsets: for idx in range(num_data): dataset_items.append( @@ -166,4 +172,4 @@ def generate_datumaro_dataset( mask_shape=mask_shape, ) ) - return dm.Dataset.from_iterable(dataset_items, categories=["cat", "dog"]) + return Dataset.from_iterable(dataset_items, categories=["cat", "dog"]) From fae472b47634e7ba718b2dfc356080d81350ecbe Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Tue, 19 Sep 2023 03:14:13 +0200 Subject: [PATCH 061/146] Upgrade nncf version for 1.4 release (#2459) * Upgrade nncf version * Fix nncf interface warning * Set the exact nncf version * Update FQ refs after NNCF upgrade * Use NNCF from pypi --- requirements/openvino.txt | 2 +- src/otx/algorithms/common/tasks/nncf_task.py | 2 +- .../ote_anomaly_classification_padim/compressed_model.yml | 4 ++-- .../ote_anomaly_detection_padim/compressed_model.yml | 4 ++-- .../ote_anomaly_segmentation_padim/compressed_model.yml | 4 ++-- .../compressed_model.yml | 4 ++-- .../compressed_model.yml | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/requirements/openvino.txt b/requirements/openvino.txt index 41c6ecbfeb7..6424a9b6778 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -1,6 +1,6 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # OpenVINO Requirements. # -nncf==2.5.0 +nncf==2.6.0 onnx==1.13.0 openvino-model-api==0.1.3 openvino==2023.0 diff --git a/src/otx/algorithms/common/tasks/nncf_task.py b/src/otx/algorithms/common/tasks/nncf_task.py index 6a0848d6a08..1a95e84554c 100644 --- a/src/otx/algorithms/common/tasks/nncf_task.py +++ b/src/otx/algorithms/common/tasks/nncf_task.py @@ -245,7 +245,7 @@ def model_builder( if is_export: compression_ctrl.prepare_for_export() - model.disable_dynamic_graph_building() + model.nncf.disable_dynamic_graph_building() if return_compression_ctrl: return compression_ctrl, model 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 index d4837afc5d9..01460cc560c 100644 --- 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 @@ -1,5 +1,5 @@ TestToolsAnomalyClassification: nncf: - number_of_fakequantizers: 26 - pot: number_of_fakequantizers: 27 + pot: + number_of_fakequantizers: 28 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 index 6f5de7f1072..4bca1f02a5d 100644 --- 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 @@ -1,5 +1,5 @@ TestToolsAnomalyDetection: nncf: - number_of_fakequantizers: 26 - pot: number_of_fakequantizers: 27 + pot: + number_of_fakequantizers: 28 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 index 01ace253a51..1d41886dfd4 100644 --- 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 @@ -1,5 +1,5 @@ TestToolsAnomalySegmentation: nncf: - number_of_fakequantizers: 26 - pot: number_of_fakequantizers: 27 + pot: + number_of_fakequantizers: 28 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 index d56b1d4a537..72f1b3d9c6b 100644 --- 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 @@ -2,9 +2,9 @@ TestToolsMPAInstanceSegmentation: nncf: number_of_fakequantizers: 204 pot: - number_of_fakequantizers: 162 + number_of_fakequantizers: 137 TestToolsTilingInstanceSegmentation: nncf: number_of_fakequantizers: 204 pot: - number_of_fakequantizers: 162 + number_of_fakequantizers: 137 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 index 1e6a124b3e5..bbfb5aa8c6d 100644 --- 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 @@ -2,9 +2,9 @@ TestToolsMPAInstanceSegmentation: nncf: number_of_fakequantizers: 97 pot: - number_of_fakequantizers: 124 + number_of_fakequantizers: 99 TestToolsTilingInstanceSegmentation: nncf: number_of_fakequantizers: 97 pot: - number_of_fakequantizers: 124 + number_of_fakequantizers: 99 From 4ddbffc02b735262b0606ab1794ad598b2d084de Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 19 Sep 2023 11:37:15 +0900 Subject: [PATCH 062/146] Update version for release 1.4.2rc5 (#2507) update version for release 1.4.2rc5 --- docs/source/conf.py | 9 ++++++++- src/otx/__init__.py | 2 +- .../api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 32bc942b667..61741e262b6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,6 +11,13 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # +import os +import sys + +sys.path.insert(0, os.path.abspath("../../src")) + +from otx import __version__ + # ruff: noqa # -- Project information ----------------------------------------------------- # @@ -18,7 +25,7 @@ project = 'OpenVINO™ Training Extensions' copyright = '2023, OpenVINO™ Training Extensions Contributors' author = 'OpenVINO™ Training Extensions Contributors' -release = '1.4.2' +release = __version__ # -- General configuration --------------------------------------------------- # diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 1eb8b4ec35d..78cf2f8eb22 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2rc4" +__version__ = "1.4.2rc5" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 449be47c3ed..c6a0e4f6396 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2rc4 +otx==1.4.2rc5 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 44949555777f5dba05fe3679d268360ace0ff6f6 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 22 Sep 2023 10:22:01 +0900 Subject: [PATCH 063/146] Update for 1.4.2 (#2514) update for release 1.4.2 --- CHANGELOG.md | 3 +- docs/source/guide/release_notes/index.rst | 120 ++++++++++++++++-- src/otx/__init__.py | 2 +- .../exportable_code/demo/requirements.txt | 2 +- 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e1c8e7d50..17c963187e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file. ### Enhancements -- Bump datumaro version to 1.5.0rc0 () +- Upgrade nncf version to 2.6.0 () +- Bump datumaro version to 1.5.0 (, ) - Set tox version constraint () - Add model category attributes to model template () diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index ef0c1f6d721..7d9cf3b418d 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -1,9 +1,26 @@ Releases -======== +######## -*************** -[v1.4.0] (3Q23) -*************** +.. toctree:: + :maxdepth: 1 + +v1.4.2 (4Q23) +------------- + +- Upgrade nncf version to 2.6.0 +- Bump datumaro version to 1.5.0 +- Set tox version constraint +- Add model category attributes to model template +- Minor bug fixes + +v1.4.1 (3Q23) +------------- + +- Update the README file in exportable code +- Minor bug fixes + +v1.4.0 (3Q23) +------------- - Support encrypted dataset training - Add custom max iou assigner to prevent CPU OOM when large annotations are used @@ -13,10 +30,23 @@ Releases - Add new object detector DINO - Add new visual prompting task - Add new object detector ResNeXt101-ATSS +- Introduce channel_last parameter to improve the performance +- Decrease time for making a workspace +- Set persistent_workers and pin_memory as True in detection task +- New algorithm for Semi-SL semantic segmentation based on metric learning via class prototypes +- Self-SL for classification now can recieve just folder with any images to start contrastive pretraining +- Update OpenVINO version to 2023.0, and NNCF verion to 2.5 +- Improve XAI saliency map generation for tiling detection and tiling instance segmentation +- Remove CenterCrop from Classification test pipeline and editing missing docs link +- Switch to PTQ for sseg +- Minor bug fixes -*************** -[v1.3.0] (2Q23) -*************** +v1.3.1 (2Q23) +------------- +- Minor bug fixes + +v1.3.0 (2Q23) +------------- - Support direct annotation input for COCO format - Action task supports multi GPU training @@ -25,10 +55,38 @@ Releases - Support auto adapting batch size - Support auto adapting num_workers - Support noisy label detection for detection tasks +- Make semantic segmentation OpenVINO models compatible with ModelAPI +- Support label hierarchy through LabelTree in LabelSchema for classification task +- Enhance exportable code file structure, video inference and default value for demo +- Speedup OpenVINO inference in image classificaiton, semantic segmentation, object detection and instance segmentation tasks +- Refactoring of ONNX export functionality +- Minor bug fixes + +v1.2.4 (3Q23) +------------- +- Per-class saliency maps for M-RCNN +- Disable semantic segmentation soft prediction processing +- Update export and nncf hyperparameters +- Minor bug fixes + +v1.2.3 (2Q23) +------------- + +- Improve warning message for tiling configurable parameter +- Minor bug fixes + +v1.2.1 (2Q23) +------------- -************* -v1.2.0 (1Q23) -************* +- Upgrade mmdeploy==0.14.0 from official PyPI +- Integrate new ignored loss in semantic segmentation +- Optimize YOLOX data pipeline +- Tiling Spatial Concatenation for OpenVINO IR +- Optimize counting train & inference speed and memory consumption +- Minor bug fixes + +v1.2.0 (2Q23) +------------- - Add generating feature cli_report.log in output for otx training - Support multiple python versions up to 3.10 @@ -36,10 +94,30 @@ v1.2.0 (1Q23) - Add option to save images after inference in OTX CLI demo together with demo in exportable code - Support storage cache in Apache Arrow using Datumaro for cls, det, seg tasks - Add noisy label detection for multi-class classification task +- Clean up and refactor the output of the OTX CLI +- Enhance DetCon logic and SupCon for semantic segmentation +- Detection task refactoring +- Classification task refactoring +- Extend OTX explain CLI +- Segmentation task refactoring +- Action task refactoring +- Optimize data preprocessing time and enhance overall performance in semantic segmentation +- Support automatic batch size decrease when there is no enough GPU memory +- Minor bug fixes + +v1.1.2 (2Q23) +------------- + +- Minor bug fixes + + +v1.1.1 (1Q23) +------------- + +- Minor bug fixes -************* v1.1.0 (1Q23) -************* +------------- - Add FP16 IR export support - Add in-memory caching in dataloader @@ -50,10 +128,24 @@ v1.1.0 (1Q23) - Add embedding of inference configuration to IR for classification - Enable VOC dataset in OTX - Add mmcls.VisionTransformer backbone support +- Parametrize saliency maps dumping in export +- Bring mmdeploy to action recognition model export & Test optimization of action tasks +- Update backbone lists +- Add explanation for XAI & minor doc fixes +- Refactor phase#1: MPA modules + + +v1.0.1 (1Q23) +------------- + +- Refine documents by proof review +- Separate installation for each tasks +- Improve POT efficiency by setting stat_requests_number parameter to 1 +- Minor bug fixes + -************* v1.0.0 (1Q23) -************* +------------- - Installation through PyPI - Package will be renamed as OpenVINO™ Training Extensions diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 78cf2f8eb22..c21696d1ee0 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2rc5" +__version__ = "1.4.2" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index c6a0e4f6396..d6794af16ab 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2rc5 +otx==1.4.2 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 88deb2d655dc9215204d40828f8e7f8f118913d3 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 25 Sep 2023 22:04:28 +0900 Subject: [PATCH 064/146] create branch release/1.5.0 --- src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 0dc07f3e3b2..d602a09e885 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.5.0rc0" +__version__ = "1.5.0rc1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 1a01c373d15..ccf1180ba37 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.5 -otx @ git+https://github.com/openvinotoolkit/training_extensions/@e066a04834952257c1c9384a09f472d13b76b264#egg=otx +otx==1.5.0rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From b0eac196fd265b2fb0247b5565688f45be268f23 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Tue, 10 Oct 2023 10:13:29 +0900 Subject: [PATCH 065/146] Delete mem cache handler after training is done (#2535) release mem cache handler after training is done --- src/otx/algorithms/classification/task.py | 3 +++ src/otx/algorithms/detection/task.py | 3 +++ src/otx/algorithms/segmentation/task.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index 03da102188f..1a811ab7234 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -80,6 +80,7 @@ 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 logger = get_logger() RECIPE_TRAIN_TYPE = { @@ -215,6 +216,8 @@ def train( 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.") diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py index 9a3d9cca885..d87ce125f38 100644 --- a/src/otx/algorithms/detection/task.py +++ b/src/otx/algorithms/detection/task.py @@ -65,6 +65,7 @@ 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 logger = get_logger() @@ -231,6 +232,8 @@ def train( 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 diff --git a/src/otx/algorithms/segmentation/task.py b/src/otx/algorithms/segmentation/task.py index a62270bb13c..779bdd10edc 100644 --- a/src/otx/algorithms/segmentation/task.py +++ b/src/otx/algorithms/segmentation/task.py @@ -70,6 +70,7 @@ 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 logger = get_logger() RECIPE_TRAIN_TYPE = { @@ -171,6 +172,8 @@ def train( 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.") From 419a0f222904809f16cb12d3df1743039f044ee4 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 11 Oct 2023 09:15:03 +0900 Subject: [PATCH 066/146] Fix bug that auto batch size doesn't consider distributed training (#2533) * consider distributed training while searching batch size * update unit test * reveret gpu memory upper bound * fix typo * change allocated to reserved * add unit test for distributed training * align with pre-commit --- .../adapters/torch/utils/bs_search_algo.py | 39 +++++- .../torch/utils/test_bs_search_algo.py | 122 +++++++++++++++++- 2 files changed, 151 insertions(+), 10 deletions(-) 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 index 0e8b7343ac6..5b1457c6ede 100644 --- a/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py +++ b/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, Tuple import torch +import torch.distributed as dist from otx.algorithms.common.utils.logger import get_logger @@ -40,7 +41,7 @@ def __init__(self, train_func: Callable[[int], None], default_bs: int, max_bs: i def _try_batch_size(self, bs: int) -> Tuple[bool, int]: cuda_oom = False - torch.cuda.reset_max_memory_allocated(device=None) + torch.cuda.reset_max_memory_cached(device=None) torch.cuda.empty_cache() try: @@ -51,18 +52,42 @@ def _try_batch_size(self, bs: int) -> Tuple[bool, int]: else: raise e - max_memory_allocated = torch.cuda.max_memory_allocated(device=None) + 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_allocated + 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_allocated / self._total_mem}%" + f"GPU memory usage : {max_memory_reserved / self._total_mem}%" ) torch.cuda.empty_cache() - return cuda_oom, max_memory_allocated + return cuda_oom, max_memory_reserved @staticmethod def _get_even_center_val(val1: int, val2: int) -> int: @@ -82,10 +107,10 @@ def auto_decrease_batch_size(self) -> int: lowest_unavailable_bs = self._default_bs + 2 while True: - cuda_oom, max_memory_allocated = self._try_batch_size(current_bs) + 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_allocated > self._mem_upper_bound: + 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) 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 index d0649dc29bf..a347968dc5e 100644 --- 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 @@ -1,4 +1,7 @@ +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 @@ -11,6 +14,8 @@ class TestBsSearchAlgo: 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) @@ -35,11 +40,122 @@ def mock_train_func(batch_size): else: mem_usage = 8500 * batch_size / max_runnable_bs - self.mock_torch.cuda.max_memory_allocated.return_value = mem_usage + 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) @@ -91,7 +207,7 @@ def mock_train_func(batch_size): mem_usage = 9000 else: mem_usage = 1000 - self.mock_torch.cuda.max_memory_allocated.return_value = mem_usage + self.mock_torch.cuda.max_memory_reserved.return_value = mem_usage return mem_usage bs_search_algo = BsSearchAlgo(mock_train_func, 64, 1000) @@ -108,7 +224,7 @@ def mock_train_func(batch_size): mem_usage = 9000 else: mem_usage = 1000 + batch_size / 1000 - self.mock_torch.cuda.max_memory_allocated.return_value = mem_usage + self.mock_torch.cuda.max_memory_reserved.return_value = mem_usage return mem_usage bs_search_algo = BsSearchAlgo(mock_train_func, 64, 1000) From 39ec80b93f10216bedc11562c1d33df65466591e Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Wed, 11 Oct 2023 10:17:31 +0900 Subject: [PATCH 067/146] Apply fix progress hook to release 1.5.0 (#2539) * Fix hook's ordering issue. AdaptiveRepeatHook changes the runner.max_iters before the ProgressHook * Change the expression * Fix typo * Fix multi-label, h-label issue * Fix auto_bs issue * Apply suggestions from code review Co-authored-by: Eunwoo Shin * Reflecting reviews * Refactor the name of get_data_cfg * Revert adaptive hook sampler init * Refactor the function name: get_data_cfg -> get_subset_data_cfg * Fix unit test errors * Remove adding AdaptiveRepeatDataHook for autobs * Remove unused import * Fix detection and segmentation case in Geti scenario --------- Co-authored-by: Eunwoo Shin --- .../adapters/mmcls/configurer.py | 7 --- .../common/adapters/mmcv/clsincr_mixin.py | 8 ---- .../common/adapters/mmcv/configurer.py | 29 ++++++++---- .../mmcv/hooks/adaptive_repeat_data_hook.py | 40 ++++++++++++---- .../adapters/mmcv/hooks/task_adapt_hook.py | 13 ++++-- .../common/adapters/mmcv/utils/__init__.py | 2 - .../adapters/mmcv/utils/automatic_bs.py | 4 +- .../adapters/mmcv/utils/config_utils.py | 46 +++++++++++-------- .../dataloaders/samplers/balanced_sampler.py | 11 +++-- .../dataloaders/samplers/cls_incr_sampler.py | 7 +-- .../torch/dataloaders/samplers/otx_sampler.py | 34 +++++++------- .../detection/adapters/mmdet/configurer.py | 2 +- .../multilabel/incremental.yaml | 36 ++++++++++++++- .../classification/multilabel/train.yaml | 2 +- .../adapters/mmcls/test_configurer.py | 4 +- .../hooks/test_adaptive_repeat_data_hook.py | 3 +- .../adapters/mmcv/utils/test_config_utils.py | 22 +++++++-- .../dataloaders/samplers/test_otx_sampler.py | 6 +-- .../adapters/mmdet/test_configurer.py | 4 +- 19 files changed, 182 insertions(+), 98 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index 3d5be63a4f9..a77938d6b10 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -213,13 +213,6 @@ def get_sampler_type(self, cfg): sampler_type = "cls_incr" return sampler_type - def use_adaptive_repeat(self, cfg) -> bool: - """Return whether using adaptive repeat. - - Currently, only multi class classification supports adaptive repeat. - """ - return self._is_multiclass(cfg) - @staticmethod def _is_multiclass(cfg) -> bool: return not cfg.model.get("multilabel", False) and not cfg.model.get("hierarchical", False) diff --git a/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py b/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py index bdbdf35e080..02251a8f200 100644 --- a/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py +++ b/src/otx/algorithms/common/adapters/mmcv/clsincr_mixin.py @@ -36,7 +36,6 @@ def configure_task_adapt_hook(self, cfg): sampler_flag=sampler_flag, sampler_type=self.get_sampler_type(cfg), efficient_mode=cfg["task_adapt"].get("efficient_mode", False), - use_adaptive_repeat=self.use_adaptive_repeat(cfg), priority="NORMAL", ), ) @@ -50,10 +49,3 @@ def is_incremental(self) -> bool: def get_sampler_type(self, cfg) -> str: """Return sampler type.""" return "cls_incr" - - def use_adaptive_repeat(self, cfg) -> bool: - """Return whether using adaptive repeat. - - Currently, only multi class classification supports adaptive repeat. - """ - return False diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 6b1c1aaa4ef..149fd79f9f5 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -17,6 +17,7 @@ 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, @@ -111,8 +112,8 @@ def merge_configs(self, cfg, data_cfg, data_pipeline_path, hyperparams_from_otx, if data_cfg: for subset in data_cfg.data: if subset in cfg.data: - src_data_cfg = self.get_data_cfg(cfg, subset) - new_data_cfg = self.get_data_cfg(data_cfg, subset) + 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: @@ -198,13 +199,12 @@ def configure_samples_per_gpu( 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_data_cfg(cfg, subset) + 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) @@ -269,7 +269,7 @@ def configure_model(self, cfg, data_classes, model_classes, ir_options, **kwargs self.model_classes = model_classes self.data_classes = data_classes if data_classes is not None: - train_data_cfg = self.get_data_cfg(cfg, "train") + 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 @@ -413,6 +413,19 @@ def configure_hooks( 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): @@ -478,7 +491,7 @@ def get_model_meta(cfg): def get_data_classes(self, cfg): """Get data classes from train cfg.""" data_classes = [] - train_cfg = self.get_data_cfg(cfg, "train") + 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: @@ -486,7 +499,7 @@ def get_data_classes(self, cfg): return data_classes @staticmethod - def get_data_cfg(cfg, subset): + 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 @@ -512,7 +525,7 @@ def adapt_input_size_to_dataset( Tuple[int, int]: (width, height) or None """ - data_cfg = BaseConfigurer.get_data_cfg(cfg, "train") + data_cfg = BaseConfigurer.get_subset_data_cfg(cfg, "train") dataset = data_cfg.get("otx_dataset", None) if dataset is None: return None 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 index 042defcea15..310e7316ba6 100644 --- 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 @@ -6,6 +6,7 @@ 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.algorithms.common.utils.logger import get_logger @@ -17,38 +18,61 @@ 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, coef: float = -0.7, min_repeat: float = 1.0): + 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 - 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() sampler = OTXSampler( dataset=dataset, - samples_per_gpu=batch_size, - use_adaptive_repeats=True, - num_replicas=world_size, - rank=rank, + 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=batch_size, + batch_size=self.train_batch_size, sampler=sampler, num_workers=num_workers, collate_fn=collate_fn, 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 index 468fe0e23a3..193f156fafd 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py @@ -9,6 +9,7 @@ from otx.algorithms.common.adapters.torch.dataloaders.samplers import ( BalancedSampler, ClsIncrSampler, + OTXSampler, ) from otx.algorithms.common.utils.logger import get_logger @@ -36,7 +37,6 @@ def __init__( sampler_flag=False, sampler_type="cls_incr", efficient_mode=False, - use_adaptive_repeat=False, ): self.src_classes = src_classes self.dst_classes = dst_classes @@ -44,13 +44,11 @@ def __init__( self.sampler_flag = sampler_flag self.sampler_type = sampler_type self.efficient_mode = efficient_mode - self.use_adaptive_repeat = use_adaptive_repeat 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}") - logger.info(f"- Adaptive repeat: {self.use_adaptive_repeat}") def before_epoch(self, runner): """Produce a proper sampler for task-adaptation.""" @@ -61,6 +59,11 @@ def before_epoch(self, runner): 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, @@ -68,7 +71,7 @@ def before_epoch(self, runner): efficient_mode=self.efficient_mode, num_replicas=world_size, rank=rank, - use_adaptive_repeats=self.use_adaptive_repeat, + n_repeats=repeat, ) else: sampler = ClsIncrSampler( @@ -77,7 +80,7 @@ def before_epoch(self, runner): efficient_mode=self.efficient_mode, num_replicas=world_size, rank=rank, - use_adaptive_repeats=self.use_adaptive_repeat, + n_repeats=repeat, ) runner.data_loader = DataLoader( dataset, diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py b/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py index 4fd056b5725..d6c1ce5a3db 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/__init__.py @@ -12,7 +12,6 @@ InputSizeManager, OTXConfig, config_from_string, - get_data_cfg, get_dataset_configs, is_epoch_based_runner, patch_adaptive_interval_training, @@ -45,7 +44,6 @@ "patch_early_stopping", "patch_persistent_workers", "prepare_work_dir", - "get_data_cfg", "OTXConfig", "adapt_batch_size", "InputSizeManager", diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py index 9b95d58195f..3ccd2c81f3f 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py @@ -90,7 +90,6 @@ def train_func_single_iter(batch_size): ) default_bs = _get_batch_size(cfg) - bs_search_algo = BsSearchAlgo( train_func=train_func_single_iter, default_bs=default_bs, @@ -126,6 +125,9 @@ def _set_batch_size(cfg, batch_size: int): 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): diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py index eadb94b5f49..2b211890232 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -4,6 +4,7 @@ import copy import glob +import math import multiprocessing import os import os.path as osp @@ -517,15 +518,11 @@ def patch_from_hyperparams(config: Config, hyperparams, **kwargs): algo_backend = hyperparams.algo_backend warmup_iters = int(params.learning_rate_warmup_iters) - model_label_type = config.filename.split("/")[-1] - if "multilabel" in model_label_type: - lr_config = ConfigDict(max_lr=params.learning_rate, warmup=None) - else: - lr_config = ( - ConfigDict(warmup_iters=warmup_iters) - if warmup_iters > 0 - else ConfigDict(warmup_iters=warmup_iters, warmup=None) - ) + 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( @@ -599,14 +596,6 @@ def prepare_work_dir(config: Union[Config, ConfigDict]) -> str: return train_round_checkpoint_dir -def get_data_cfg(config: Union[Config, ConfigDict], subset: str = "train") -> Config: - """Return dataset configs.""" - data_cfg = config.data[subset] - while "dataset" in data_cfg: - data_cfg = data_cfg.dataset - return data_cfg - - class InputSizeManager: """Class for changing input size and getting input size value by checking data pipeline. @@ -899,7 +888,6 @@ def get_configured_input_size( if input_size == InputSizePreset.DEFAULT.value: return None - logger.info("Given model weight was trained with {} input size.".format(input_size)) else: @@ -984,3 +972,25 @@ def area(x): 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/torch/dataloaders/samplers/balanced_sampler.py b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py index 711f0f4729b..1ddaaab7884 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py @@ -4,6 +4,7 @@ # import math +from typing import Union import numpy as np from torch.utils.data import Dataset @@ -37,7 +38,7 @@ class BalancedSampler(OTXSampler): # pylint: disable=too-many-instance-attribut 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``. - use_adaptive_repeats (bool, optional): Flag about using adaptive repeats + n_repeats (Union[float, int, str], optional) : number of iterations for manual setting """ def __init__( @@ -48,14 +49,14 @@ def __init__( num_replicas: int = 1, rank: int = 0, drop_last: bool = False, - use_adaptive_repeats: 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, use_adaptive_repeats) + super().__init__(dataset, samples_per_gpu, n_repeats=n_repeats) self.img_indices = self.dataset.img_indices # type: ignore[attr-defined] self.num_cls = len(self.img_indices.keys()) @@ -74,7 +75,9 @@ def __init__( self.num_trials = int(self.data_length / self.num_cls) self.num_samples = self._calculate_num_samples() - logger.info(f"This sampler will select balanced samples {self.num_trials} times") + 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 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 index 6b03f8cdf93..58622a354f0 100644 --- 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 @@ -5,6 +5,7 @@ import math import random +from typing import Union import numpy as np from torch.utils.data import Dataset @@ -35,7 +36,7 @@ class ClsIncrSampler(OTXSampler): # pylint: disable=too-many-instance-attribute 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``. - use_adaptive_repeats (bool, optional): Flag about using adaptive repeats + n_repeats (Union[float, int, str], optional) : number of iterations for manual setting """ def __init__( @@ -46,14 +47,14 @@ def __init__( num_replicas: int = 1, rank: int = 0, drop_last: bool = False, - use_adaptive_repeats: 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, use_adaptive_repeats) + super().__init__(dataset, samples_per_gpu, n_repeats=n_repeats) if hasattr(self.dataset, "img_indices"): self.new_indices = self.dataset.img_indices["new"] 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 index 9f9a4cac2b6..b01f2aaef66 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py @@ -4,13 +4,14 @@ # import math -from typing import Optional +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.logger import get_logger from otx.algorithms.common.utils.task_adapt import unwrap_dataset @@ -32,7 +33,6 @@ class OTXSampler(Sampler): # pylint: disable=too-many-instance-attributes Args: dataset (Dataset): A built-up dataset samples_per_gpu (int): batch size of Sampling - use_adaptive_repeats (bool): Flag about using adaptive repeats num_replicas (int, optional): Number of processes participating in distributed training. By default, :attr:`world_size` is retrieved from the current distributed group. @@ -42,18 +42,22 @@ class OTXSampler(Sampler): # pylint: disable=too-many-instance-attributes 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, - use_adaptive_repeats: bool, 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, ): @@ -62,8 +66,15 @@ def __init__( self.num_replicas = num_replicas self.rank = rank self.shuffle = shuffle - self.repeat = self._get_proper_repeats(use_adaptive_repeats, coef, min_repeat) - + 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 @@ -73,19 +84,6 @@ def __init__( self.seed = seed self.epoch = 0 - def _get_proper_repeats(self, use_adaptive_repeats: bool, coef: float, min_repeat: float): - """Calculate the proper repeats with considering the number of iterations.""" - n_repeats = 1 - if use_adaptive_repeats: - # NOTE - # Currently, only support the integer type repeats. - # Will support the floating point repeats and large dataset cases. - n_iters_per_epoch = math.ceil(len(self.dataset) / self.samples_per_gpu) - n_repeats = math.floor(max(coef * math.sqrt(n_iters_per_epoch - 1) + 5, min_repeat)) - logger.info("OTX Sampler: adaptive repeats enabled") - logger.info(f"OTX will use {n_repeats} times larger dataset made by repeated sampling") - return n_repeats - def __iter__(self): """Iter.""" if self.shuffle: diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index 8df3ad66d50..876e05ca822 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -112,7 +112,7 @@ def _configure_eval_dataset(self, cfg): 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_data_cfg(cfg, "train") + 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): diff --git a/src/otx/recipes/stages/classification/multilabel/incremental.yaml b/src/otx/recipes/stages/classification/multilabel/incremental.yaml index 85b25d96aa9..03a87dc5591 100644 --- a/src/otx/recipes/stages/classification/multilabel/incremental.yaml +++ b/src/otx/recipes/stages/classification/multilabel/incremental.yaml @@ -19,9 +19,41 @@ optimizer: 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" -custom_hooks: - - type: ModelEmaV2Hook +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/train.yaml b/src/otx/recipes/stages/classification/multilabel/train.yaml index 5ff73be2aa9..4666a957e36 100644 --- a/src/otx/recipes/stages/classification/multilabel/train.yaml +++ b/src/otx/recipes/stages/classification/multilabel/train.yaml @@ -4,7 +4,7 @@ _base_: "../../_base_/logs/tensorboard_logger.py", "../../_base_/optimizers/sgd.py", "../../_base_/runners/epoch_runner_cancel.py", - "../../_base_/schedules/1cycle.py", + "../../_base_/schedules/plateau.py", ] optimizer: diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index 56f1ed27eeb..fd6f26d0805 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -266,11 +266,11 @@ def test_configure_compat_cfg(self): self.configurer.configure_compat_cfg(model_cfg) @e2e_pytest_unit - def test_get_data_cfg(self): + 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_data_cfg(config, "train") + assert [1, 2, 3] == self.configurer.get_subset_data_cfg(config, "train") class TestIncrClassificationConfigurer: 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 index 17afb05d007..550aea5f611 100644 --- 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 @@ -22,6 +22,7 @@ def __init__(self): def __len__(self): return 10 + self.mock_dataset = MockDataset() self.mock_data_loader = DataLoader( dataset=MockDataset(), batch_size=len(MockDataset()), @@ -30,7 +31,7 @@ def __len__(self): @e2e_pytest_unit def test_before_epoch(self) -> None: - hook = AdaptiveRepeatDataHook() + 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/utils/test_config_utils.py b/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py index 63d5faf887d..00405854eb9 100644 --- a/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py @@ -7,13 +7,14 @@ 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_data_cfg(workers_per_gpu: int = 2) -> dict: +def get_subset_data_cfg(workers_per_gpu: int = 2) -> dict: data_cfg = {} for subset in ["train", "val", "test", "unlabeled"]: data_cfg[subset] = "fake" @@ -25,7 +26,7 @@ def get_data_cfg(workers_per_gpu: int = 2) -> dict: @e2e_pytest_unit @pytest.mark.parametrize("workers_per_gpu", [0, 2]) def test_patch_persistent_workers(mocker, workers_per_gpu): - data_cfg = get_data_cfg(workers_per_gpu) + data_cfg = get_subset_data_cfg(workers_per_gpu) config = mocker.MagicMock() config.data = data_cfg @@ -43,7 +44,7 @@ def test_patch_persistent_workers(mocker, workers_per_gpu): @e2e_pytest_unit def test_patch_persistent_workers_dist_semisl(mocker): - data_cfg = get_data_cfg() + data_cfg = get_subset_data_cfg() config = mocker.MagicMock() config.data = data_cfg @@ -491,3 +492,18 @@ def test_adapt_input_size_to_dataset(self): 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/torch/dataloaders/samplers/test_otx_sampler.py b/tests/unit/algorithms/common/adapters/torch/dataloaders/samplers/test_otx_sampler.py index 8b72418d72e..6d10da154ab 100644 --- 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 @@ -1,5 +1,4 @@ import pytest -import math from torch.utils.data import Dataset from otx.algorithms.common.adapters.torch.dataloaders.samplers import OTXSampler @@ -20,9 +19,8 @@ def __len__(self): @e2e_pytest_unit @pytest.mark.parametrize("batch", [1, 2, 4, 8, 16]) - @pytest.mark.parametrize("use_adaptive_repeat", [True, False]) - def test_sampler_iter(self, batch, use_adaptive_repeat): - sampler = OTXSampler(self.mock_dataset, batch, use_adaptive_repeats=use_adaptive_repeat) + def test_sampler_iter(self, batch): + sampler = OTXSampler(self.mock_dataset, batch) sampler_iter = iter(sampler) count = 0 diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index 5d5637f81ef..c341d9d1b4e 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -323,12 +323,12 @@ def test_configure_compat_cfg(self): self.configurer.configure_compat_cfg(model_cfg) @e2e_pytest_unit - def test_get_data_cfg(self): + 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_data_cfg(config, "train") + assert [1, 2, 3] == self.configurer.get_subset_data_cfg(config, "train") class TestIncrDetectionConfigurer: From b152d9ee6823dac741c92ffa188fd3f56ab7b439 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Wed, 11 Oct 2023 13:20:27 +0900 Subject: [PATCH 068/146] Re introduce adaptive scheduling for training (#2541) * Re-introduce adaptive patience for training * Revert unit tests --- .../mmcv/hooks/adaptive_training_hook.py | 30 +++++++++++++++++-- .../hooks/test_adaptive_training_hooks.py | 4 +-- 2 files changed, 29 insertions(+), 5 deletions(-) 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 index 64a7be8f338..f9e6fb345ff 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py @@ -23,9 +23,15 @@ class AdaptiveTrainSchedulingHook(Hook): """Adaptive Training Scheduling Hook. - Depending on the size of iteration per epoch, adaptively update the validation interval. + 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. @@ -39,6 +45,10 @@ class AdaptiveTrainSchedulingHook(Hook): 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, @@ -47,6 +57,10 @@ def __init__( 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 @@ -84,13 +98,23 @@ def before_train_iter(self, runner): 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 - logger.info(f"Update LrUpdaterHook interval: {hook.interval} -> {adaptive_interval}") + hook.patience = patience + logger.info(f"Update LrUpdaterHook patience: {hook.patience} -> {patience}") elif isinstance(hook, EarlyStoppingHook): - logger.info(f"Update EarlyStoppingHook interval: {hook.interval} -> {adaptive_interval}") + 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 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 index 71716effd62..51a30756b97 100644 --- 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 @@ -86,7 +86,7 @@ def test_before_train_iter(self) -> None: assert hook._original_interval is None assert eval_hook.interval == 4 assert lr_hook.interval == 4 - assert lr_hook.patience == 1 + assert lr_hook.patience == 2 assert early_hook.interval == 4 - assert early_hook.patience == 1 + assert early_hook.patience == 3 assert ckpt_hook.interval == 4 From b7dd5706355ac33a334474e7c9447c728ccc57aa Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 11 Oct 2023 17:30:27 +0900 Subject: [PATCH 069/146] Update for release 1.4.3rc1 (#2542) --- CHANGELOG.md | 6 ++++++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c963187e5..fb9886a2edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## \[v1.4.3\] + +### Enhancements + +- Re-introduce adaptive scheduling for training () + ## \[v1.4.2\] ### Enhancements diff --git a/src/otx/__init__.py b/src/otx/__init__.py index c21696d1ee0..6e53681293f 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.2" +__version__ = "1.4.3rc1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index d6794af16ab..065f1522fd3 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.2 +otx==1.4.3rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From c8c5d22c9e9ae6d7ed68813387d50675fe50d1fc Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Thu, 12 Oct 2023 02:36:22 +0200 Subject: [PATCH 070/146] Mirror Anomaly ModelAPI changes (#2531) * Migrate anomaly exportable code to modelAPI (#2432) * Fix license in PR template * Migrate to modelAPI * Remove color conversion in streamer * Remove reverse_input_channels * Add float * Remove test as metadata is no longer used * Remove metadata from load method * remove anomalib openvino inferencer * fix signature * Support logacy OpenVINO model * Transform image * add configs --- .../anomalib/exportable_code/__init__.py | 12 -- .../exportable_code/anomaly_classification.py | 47 ------ .../exportable_code/anomaly_detection.py | 43 ----- .../exportable_code/anomaly_segmentation.py | 43 ----- .../adapters/anomalib/exportable_code/base.py | 47 ------ src/otx/algorithms/anomaly/tasks/inference.py | 2 +- src/otx/algorithms/anomaly/tasks/openvino.py | 150 ++++++++++++------ .../prediction_to_annotation_converter.py | 47 +++--- .../compressed_model.yml | 6 +- .../compressed_model.yml | 6 +- .../compressed_model.yml | 6 +- .../algorithms/anomaly/tasks/test_openvino.py | 2 +- ...test_prediction_to_annotation_converter.py | 74 +-------- 13 files changed, 143 insertions(+), 342 deletions(-) delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py deleted file mode 100644 index 5bcf71d66ca..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Exportable code for Anomaly tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from .anomaly_classification import AnomalyClassification -from .anomaly_detection import AnomalyDetection -from .anomaly_segmentation import AnomalySegmentation -from .base import AnomalyBase - -__all__ = ["AnomalyBase", "AnomalyClassification", "AnomalyDetection", "AnomalySegmentation"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py deleted file mode 100644 index bd61d0f3c05..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Classification tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalyClassification(AnomalyBase): - """Wrapper for anomaly classification task.""" - - __model__ = "anomaly_classification" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> float: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - float: Normalized anomaly score - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - pred_score = anomaly_map.reshape(-1).max() - - meta["image_threshold"] = self.metadata["image_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["image_threshold"], meta["min"], meta["max"]) - pred_score = self._normalize(pred_score, meta["image_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - meta["anomaly_map"] = result - - return np.array(pred_score) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py deleted file mode 100644 index 47e8b49697e..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Detection tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalyDetection(AnomalyBase): - """Wrapper for anomaly detection task.""" - - __model__ = "anomaly_detection" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> np.ndarray: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - np.ndarray: Detection Mask - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - - meta["pixel_threshold"] = self.metadata["pixel_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["pixel_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - return result diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py deleted file mode 100644 index 7335e914024..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Segmentation tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalySegmentation(AnomalyBase): - """Wrapper for anomaly segmentation task.""" - - __model__ = "anomaly_segmentation" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> np.ndarray: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - np.ndarray: Segmentation Mask - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - - meta["pixel_threshold"] = self.metadata["pixel_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["pixel_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - return result diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py deleted file mode 100644 index cf25d59fff2..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Union - -import numpy as np -from openvino.model_api.models import SegmentationModel -from openvino.model_api.models.types import DictValue, NumericalValue - - -class AnomalyBase(SegmentationModel): - """Wrapper for anomaly tasks.""" - - __model__ = "anomaly_base" - - @classmethod - def parameters(cls): - """Dictionary containing model parameters.""" - parameters = super().parameters() - parameters["resize_type"].update_default_value("standard") - parameters.update( - { - "metadata": DictValue(description="Metadata for inference"), - "threshold": NumericalValue(description="Threshold used to classify anomaly"), - } - ) - - return parameters - - @staticmethod - def _normalize( - targets: Union[np.ndarray, np.float32], - threshold: Union[np.ndarray, float], - min_val: Union[np.ndarray, float], - max_val: Union[np.ndarray, float], - ) -> np.ndarray: - """Apply min-max normalization and shift the values such that the threshold value is centered at 0.5.""" - normalized = ((targets - threshold) / (max_val - min_val)) + 0.5 - if isinstance(targets, (np.ndarray, np.float32)): - normalized = np.minimum(normalized, 1) - normalized = np.maximum(normalized, 0) - else: - raise ValueError(f"Targets must be either Tensor or Numpy array. Received {type(targets)}") - return normalized diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 0dc871c4cca..50c5a4b81f7 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -357,7 +357,7 @@ def _add_metadata_to_ir(self, model_file: str, export_type: ExportType) -> None: 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")] = True + 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" if export_type == ExportType.OPENVINO: diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index 3800b264e0d..5a5fffb3f02 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -19,7 +19,7 @@ import os import random import tempfile -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple, Union from zipfile import ZipFile import nncf @@ -27,14 +27,14 @@ import openvino.runtime as ov from addict import Dict as ADDict from anomalib.data.utils.transform import get_transforms -from anomalib.deploy import OpenVINOInferencer from nncf.common.quantization.structs import QuantizationPreset from omegaconf import OmegaConf +from openvino.model_api.models import AnomalyDetection, AnomalyResult -import otx.algorithms.anomaly.adapters.anomalib.exportable_code from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -75,22 +75,23 @@ logger = get_logger(__name__) -class OTXOpenVINOAnomalyDataloader: - """Dataloader for loading OTX dataset into OTX OpenVINO Inferencer. +class OTXNNCFAnomalyDataloader: + """Dataloader for loading OTX dataset for NNCF optimization. Args: dataset (DatasetEntity): OTX dataset entity - inferencer (OpenVINOInferencer): OpenVINO Inferencer + model: (AnomalyDetection) The modelAPI model used for fetching the transforms. + shuffle (bool, optional): Shuffle dataset. Defaults to True. """ def __init__( self, dataset: DatasetEntity, - inferencer: OpenVINOInferencer, + model: AnomalyDetection, shuffle: bool = True, ): self.dataset = dataset - self.inferencer = inferencer + self.model = model self.shuffler = None if shuffle: self.shuffler = list(range(len(dataset))) @@ -110,9 +111,12 @@ def __getitem__(self, index: int): image = self.dataset[index].numpy annotation = self.dataset[index].annotation_scene - inputs = self.inferencer.pre_process(image) - return (index, annotation), inputs + 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. @@ -135,7 +139,7 @@ def __init__(self, task_environment: TaskEnvironment) -> None: self.task_environment = task_environment self.task_type = self.task_environment.model_template.task_type self.config = self.get_config() - self.inferencer = self.load_inferencer() + 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] @@ -173,15 +177,13 @@ def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameter if inference_parameters is not None: update_progress_callback = inference_parameters.update_progress # type: ignore - # This always assumes that threshold is available in the task environment's model - meta_data = self.get_metadata() for idx, dataset_item in enumerate(dataset): - image_result = self.inferencer.predict(dataset_item.numpy, metadata=meta_data) + image_result: AnomalyResult = self.inference_model(dataset_item.numpy) # TODO: inferencer should return predicted label and mask - pred_label = image_result.pred_score >= 0.5 - pred_mask = (image_result.anomaly_map >= 0.5).astype(np.uint8) - probability = image_result.pred_score if pred_label else 1 - image_result.pred_score + 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: @@ -320,7 +322,7 @@ def optimize( ) logger.info("Starting PTQ optimization.") - data_loader = OTXOpenVINOAnomalyDataloader(dataset=dataset, inferencer=self.inferencer) + data_loader = OTXNNCFAnomalyDataloader(dataset=dataset, model=self.inference_model) quantization_dataset = nncf.Dataset(data_loader, lambda data: data[1]) with tempfile.TemporaryDirectory() as tempdir: @@ -355,34 +357,100 @@ def optimize( 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.set_data("metadata", self.task_environment.model.get_data("metadata")) output_model.model_format = ModelFormat.OPENVINO output_model.optimization_type = ModelOptimizationType.POT output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] output_model.precision = [ModelPrecision.INT8] self.task_environment.model = output_model - self.inferencer = self.load_inferencer() + 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 load_inferencer(self) -> OpenVINOInferencer: + def get_openvino_model(self) -> AnomalyDetection: """Create the OpenVINO inferencer object. Returns: - OpenVINOInferencer object + AnomalyDetection model """ if self.task_environment.model is None: raise Exception("task_environment.model is None. Cannot load weights.") - return OpenVINOInferencer( - path=( - self.task_environment.model.get_data("openvino.xml"), - self.task_environment.model.get_data("openvino.bin"), - ), - metadata=self.get_metadata(), - ) + 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. + """ + 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" + + 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 _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: @@ -412,19 +480,10 @@ def _get_openvino_configuration(self) -> Dict[str, Any]: if self.task_environment.model is None: raise Exception("task_environment.model is None. Cannot get configuration.") - configuration = { - "metadata": self.get_metadata(), + configuration: Dict[str, Any] = { "labels": LabelSchemaMapper.forward(self.task_environment.label_schema), - "threshold": 0.5, } - if "transforms" not in self.config.keys(): - configuration["mean_values"] = list(np.array([0.485, 0.456, 0.406]) * 255) - configuration["scale_values"] = list(np.array([0.229, 0.224, 0.225]) * 255) - else: - configuration["mean_values"] = self.config.transforms.mean - configuration["scale_values"] = self.config.transforms.std - return configuration def deploy(self, output_model: ModelEntity) -> None: @@ -446,7 +505,7 @@ def deploy(self, output_model: ModelEntity) -> None: task_type = str(self.task_type).lower() - parameters["type_of_model"] = task_type + parameters["type_of_model"] = "AnomalyDetection" parameters["converter_type"] = task_type.upper() parameters["model_parameters"] = self._get_openvino_configuration() zip_buffer = io.BytesIO() @@ -455,17 +514,6 @@ def deploy(self, output_model: ModelEntity) -> None: 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)) - # model_wrappers files - for root, _, files in os.walk( - os.path.dirname(otx.algorithms.anomaly.adapters.anomalib.exportable_code.__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("exportable_code/")[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")) 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 index d31964a80fe..7ea4c568b63 100644 --- a/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py +++ b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py @@ -11,10 +11,11 @@ import numpy as np from openvino.model_api.models import utils from openvino.model_api.models.utils import ( + AnomalyResult, ClassificationResult, + DetectionResult, ImageResultWithSoftPrediction, InstanceSegmentationResult, - DetectionResult, ) from otx.api.entities.annotation import ( @@ -26,16 +27,14 @@ 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.polygon import Point, Polygon 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.anomaly_utils import create_detection_annotation_from_anomaly_heatmap +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 -from otx.api.utils.detection_utils import detection2array - def convert_bbox_to_ellipse(x1, y1, x2, y2) -> Ellipse: """Convert bbox to ellipse.""" @@ -332,7 +331,7 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Convert predictions to OTX Annotation Scene using the metadata. Args: @@ -342,15 +341,14 @@ def convert_to_annotation(self, predictions: np.ndarray, metadata: Dict[str, Any Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_label = predictions >= metadata.get("threshold", 0.5) - - label = self.anomalous_label if pred_label else self.normal_label - probability = (1 - predictions) if predictions < 0.5 else predictions + 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(probability))], + labels=[ScoredLabel(label=label, probability=float(predictions.pred_score))], ) ] return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) @@ -369,7 +367,7 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Convert predictions to OTX Annotation Scene using the metadata. Args: @@ -379,9 +377,11 @@ def convert_to_annotation(self, predictions: np.ndarray, metadata: Dict[str, Any Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_mask = predictions >= 0.5 - mask = pred_mask.squeeze().astype(np.uint8) - annotations = create_annotation_from_segmentation_map(mask, predictions, self.label_map) + 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, self.label_map + ) if len(annotations) == 0: # TODO: add confidence to this label annotations = [ @@ -411,7 +411,7 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Convert predictions to OTX Annotation Scene using the metadata. Args: @@ -421,9 +421,18 @@ def convert_to_annotation(self, predictions: np.ndarray, metadata: Dict[str, Any Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_mask = predictions >= 0.5 - mask = pred_mask.squeeze().astype(np.uint8) - annotations = create_detection_annotation_from_anomaly_heatmap(mask, predictions, self.label_map) + 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 = [ 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 index 1c94650f275..d6d8e5a5f0d 100644 --- 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 @@ -1,7 +1,7 @@ TestToolsAnomalyClassification: nncf: - number_of_fakequantizers: 27 - ptq: - number_of_fakequantizers: 28 + number_of_fakequantizers: 26 pot: number_of_fakequantizers: 28 + ptq: + number_of_fakequantizers: 27 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 index c119a9d5f52..a54deb96c19 100644 --- 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 @@ -1,7 +1,7 @@ TestToolsAnomalyDetection: nncf: - number_of_fakequantizers: 27 - ptq: - number_of_fakequantizers: 28 + number_of_fakequantizers: 26 pot: number_of_fakequantizers: 28 + ptq: + number_of_fakequantizers: 27 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 index 217f636a463..6b1c3affdf9 100644 --- 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 @@ -1,7 +1,7 @@ TestToolsAnomalySegmentation: nncf: - number_of_fakequantizers: 27 - ptq: - number_of_fakequantizers: 28 + number_of_fakequantizers: 26 pot: number_of_fakequantizers: 28 + ptq: + number_of_fakequantizers: 27 diff --git a/tests/unit/algorithms/anomaly/tasks/test_openvino.py b/tests/unit/algorithms/anomaly/tasks/test_openvino.py index 82d1174bb97..8fb222189aa 100644 --- a/tests/unit/algorithms/anomaly/tasks/test_openvino.py +++ b/tests/unit/algorithms/anomaly/tasks/test_openvino.py @@ -91,7 +91,7 @@ def test_openvino(self, tmpdir, setup_task_environment): openvino_task.deploy(output_model) assert output_model.exportable_code is not None - @patch.multiple(OpenVINOTask, get_config=MagicMock(), load_inferencer=MagicMock()) + @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.""" 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 index 5a6518e8689..f48729b1775 100644 --- 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 @@ -6,9 +6,11 @@ import numpy as np import pytest -from openvino.model_api.models.utils import Detection -from openvino.model_api.models.utils import ClassificationResult -from openvino.model_api.models.utils import ImageResultWithSoftPrediction +from openvino.model_api.models.utils import ( + ClassificationResult, + Detection, + ImageResultWithSoftPrediction, +) from otx.api.entities.annotation import ( Annotation, @@ -943,69 +945,3 @@ def test_anomaly_classification_to_annotation_init( converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) assert converter.normal_label == non_empty_labels[0] assert converter.anomalous_label == non_empty_labels[2] - - @pytest.mark.priority_medium - @pytest.mark.unit - @pytest.mark.reqids(Requirements.REQ_1) - def test_anomaly_classification_to_annotation_convert( - self, - ): - """ - Description: - Check "AnomalyClassificationToAnnotationConverter" class "convert_to_annotation" method - - Input data: - "AnomalyClassificationToAnnotationConverter" class object, "predictions" array - - 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 - "metadata" dictionary with specified "threshold" key - 2. Check attributes of "AnnotationSceneEntity" object returned by "convert_to_annotation" method for - "metadata" dictionary without specified "threshold" key - """ - - def check_annotation(actual_annotation: Annotation, expected_labels: list): - assert isinstance(actual_annotation, Annotation) - assert actual_annotation.get_labels() == expected_labels - assert isinstance(actual_annotation.shape, Rectangle) - assert Rectangle.is_full_box(rectangle=actual_annotation.shape) - - non_empty_labels = [ - LabelEntity(name="Normal", domain=Domain.CLASSIFICATION, id=ID("1")), - LabelEntity( - name="Anomalous", - domain=Domain.CLASSIFICATION, - id=ID("2"), - is_anomalous=True, - ), - ] - label_group = LabelGroup(name="Anomaly classification labels group", labels=non_empty_labels) - label_schema = LabelSchemaEntity(label_groups=[label_group]) - converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) - predictions = np.array([0.7]) - # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" for "metadata" with - # specified "threshold" key - metadata = { - "non-required key": 1, - "other non-required key": 2, - "threshold": 0.8, - } - predictions_to_annotations = converter.convert_to_annotation(predictions=predictions, metadata=metadata) - check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) - check_annotation( - predictions_to_annotations.annotations[0], - expected_labels=[ScoredLabel(label=non_empty_labels[0], probability=0.7)], - ) - # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" for "metadata" without - # specified "threshold" key - metadata = {"non-required key": 1, "other non-required key": 2} - predictions_to_annotations = converter.convert_to_annotation(predictions=predictions, metadata=metadata) - check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) - check_annotation( - predictions_to_annotations.annotations[0], - expected_labels=[ScoredLabel(label=non_empty_labels[1], probability=0.7)], - ) From aec9f55880dbab6b5b4f644da14cede7f8461dbb Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Thu, 12 Oct 2023 10:46:33 +0900 Subject: [PATCH 071/146] Re-introduce adaptive training (#2543) * Re-introduce adaptive patience for training * Revert unit tests --- .../mmcv/hooks/adaptive_training_hook.py | 30 +++++++++++++++++-- .../hooks/test_adaptive_training_hooks.py | 4 +-- 2 files changed, 29 insertions(+), 5 deletions(-) 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 index 64a7be8f338..f9e6fb345ff 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py @@ -23,9 +23,15 @@ class AdaptiveTrainSchedulingHook(Hook): """Adaptive Training Scheduling Hook. - Depending on the size of iteration per epoch, adaptively update the validation interval. + 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. @@ -39,6 +45,10 @@ class AdaptiveTrainSchedulingHook(Hook): 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, @@ -47,6 +57,10 @@ def __init__( 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 @@ -84,13 +98,23 @@ def before_train_iter(self, runner): 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 - logger.info(f"Update LrUpdaterHook interval: {hook.interval} -> {adaptive_interval}") + hook.patience = patience + logger.info(f"Update LrUpdaterHook patience: {hook.patience} -> {patience}") elif isinstance(hook, EarlyStoppingHook): - logger.info(f"Update EarlyStoppingHook interval: {hook.interval} -> {adaptive_interval}") + 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 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 index 71716effd62..51a30756b97 100644 --- 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 @@ -86,7 +86,7 @@ def test_before_train_iter(self) -> None: assert hook._original_interval is None assert eval_hook.interval == 4 assert lr_hook.interval == 4 - assert lr_hook.patience == 1 + assert lr_hook.patience == 2 assert early_hook.interval == 4 - assert early_hook.patience == 1 + assert early_hook.patience == 3 assert ckpt_hook.interval == 4 From cb94427578ddb35f69d361bdee09c0ce4dd9a3c9 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Thu, 12 Oct 2023 10:56:57 +0900 Subject: [PATCH 072/146] Fix auto input size mismatch in eval & export (#2530) * Fix auto input size mismatch in eval & export * Re-enable E2E tests for Issue#2518 * Add input size check in export testing * Format float numbers in log * Fix NNCF export shape mismatch * Fix saliency map issue * Disable auto input size if tiling enabled --------- Signed-off-by: Songki Choi --- .../adapters/mmcls/configurer.py | 16 ++++---- .../adapters/mmcls/nncf/task.py | 18 +++------ .../classification/adapters/mmcls/task.py | 16 ++------ src/otx/algorithms/classification/task.py | 21 +++++----- .../common/adapters/mmcv/configurer.py | 15 +++++-- .../adapters/mmcv/utils/config_utils.py | 40 +++++++------------ src/otx/algorithms/common/utils/utils.py | 20 +++------- .../detection/adapters/mmdet/configurer.py | 20 +++++----- .../detection/adapters/mmdet/nncf/task.py | 17 ++------ .../detection/adapters/mmdet/task.py | 17 ++------ src/otx/algorithms/detection/task.py | 24 +++++------ .../segmentation/adapters/mmseg/configurer.py | 20 +++++----- .../segmentation/adapters/mmseg/nncf/task.py | 18 +++------ .../segmentation/adapters/mmseg/task.py | 17 ++------ src/otx/algorithms/segmentation/task.py | 21 +++++----- .../test_api_xai_sanity_classification.py | 8 ++-- .../test_api_xai_sanity_detection.py | 7 +++- tests/e2e/cli/detection/test_detection.py | 2 - .../cli/detection/test_tiling_detection.py | 2 - tests/test_suite/run_test_command.py | 15 +++++++ .../adapters/mmcls/test_configurer.py | 20 +++++----- .../adapters/mmcv/utils/test_config_utils.py | 38 +++++------------- .../adapters/mmdet/test_configurer.py | 22 +++++----- .../adapters/mmseg/test_mmseg_configurer.py | 20 +++++----- .../adapters/test_otx_segmentation_task.py | 1 + 25 files changed, 176 insertions(+), 259 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index a77938d6b10..397026d5760 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -1,9 +1,9 @@ """Base configurer for mmdet config.""" + # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -# -from typing import Optional +from typing import Optional, Tuple import torch from mmcv import build_from_cfg @@ -22,7 +22,6 @@ recursively_update_cfg, update_or_add_custom_hook, ) -from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.utils.logger import get_logger logger = get_logger() @@ -162,16 +161,19 @@ def configure_topk(cfg): @staticmethod def configure_input_size( - cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None + cfg, input_size=Optional[Tuple[int, int]], model_ckpt_path: Optional[str] = None, training=True ): """Change input size if necessary.""" - manager = InputSizeManager(cfg) - input_size = manager.get_configured_input_size(input_size_config, model_ckpt_path) if input_size is None: # InputSizePreset.DEFAULT return + manager = InputSizeManager(cfg) + if input_size == (0, 0): # InputSizePreset.AUTO - input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager) + 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 diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py index 31dcda81344..eefb7bf1de4 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py @@ -1,18 +1,7 @@ """NNCF Task 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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 from functools import partial from typing import List, Optional @@ -121,3 +110,6 @@ def _generate_training_metrics_group(self, learning_curves): 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/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 25b112e7d2a..55b7ea0dd3a 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -1,18 +1,7 @@ """Task of OTX Classification using mmclassification 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. +# SPDX-License-Identifier: Apache-2.0 import glob import os @@ -194,11 +183,12 @@ def configure( ir_options, data_classes, model_classes, - self._hyperparams.learning_parameters.input_size, + 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( diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index 1a811ab7234..9050e736f0c 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -1,18 +1,7 @@ """Task of OTX 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. +# SPDX-License-Identifier: Apache-2.0 import io import json @@ -34,6 +23,7 @@ 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 @@ -130,6 +120,12 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] 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 + 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. @@ -479,6 +475,7 @@ def save_model(self, output_model: ModelEntity): "model": model_ckpt, "config": hyperparams_str, "labels": labels, + "input_size": self._input_size, "VERSION": 1, } diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 149fd79f9f5..68e2ea6c35c 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -26,7 +26,6 @@ recursively_update_cfg, update_or_add_custom_hook, ) -from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.tasks.base_task import OnHookInitialized from otx.algorithms.common.utils import UncopiableDefaultDict, append_dist_rank_suffix from otx.algorithms.common.utils.data import compute_robust_dataset_statistics @@ -74,7 +73,7 @@ def configure( ir_options: Optional[Config] = None, data_classes: Optional[List[str]] = None, model_classes: Optional[List[str]] = None, - input_size: InputSizePreset = InputSizePreset.DEFAULT, + input_size: Optional[Tuple[int, int]] = None, **kwargs: Dict[Any, Any], ) -> Config: """Create MMCV-consumable config from given inputs.""" @@ -228,7 +227,7 @@ 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.configure_input_size(cfg, input_size, model_ckpt_path, self.training) def configure_recipe(self, cfg, **kwargs): """Configuration training recipe settings.""" @@ -533,7 +532,15 @@ def adapt_input_size_to_dataset( stat = compute_robust_dataset_statistics(dataset, use_annotations) if not stat: return None - logger.info(f"Dataset stat: {json.dumps(stat, indent=4)}") + + 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 diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py index 2b211890232..007c64dfa30 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -682,12 +682,12 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): self._set_pipeline_size_value(pipelines, resize_ratio) # Set model size - # - needed only for YOLOX 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.") - model_cfg["input_size"] = input_size @property def base_input_size(self) -> Union[Tuple[int, int], Dict[str, Tuple[int, int]]]: @@ -862,38 +862,28 @@ def _set_size_value(pipeline: Dict, attr: str, scale: Tuple[Union[int, float], U pipeline[attr] = (round(pipeline[attr][0] * scale[0]), round(pipeline[attr][1] * scale[1])) @staticmethod - def get_configured_input_size( - input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt: Optional[str] = None - ) -> Optional[Tuple[int, int]]: - """Get configurable input size configuration. If it doesn't exist, return None. + 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: - input_size_config (InputSizePreset, optional): Input size setting. Defaults to InputSizePreset.DEFAULT. 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. """ - input_size = None - if input_size_config == InputSizePreset.DEFAULT: - if model_ckpt is None: - return None - - model_info = torch.load(model_ckpt, map_location="cpu") - for key in ["config", "learning_parameters", "input_size", "value"]: - if key not in model_info: - return None - model_info = model_info[key] - input_size = model_info - - if input_size == InputSizePreset.DEFAULT.value: - return None - logger.info("Given model weight was trained with {} input size.".format(input_size)) + if model_ckpt is None: + return None - else: - input_size = input_size_config.value + model_info = torch.load(model_ckpt, map_location="cpu") + if model_info is None: + return None - return InputSizePreset.parse(input_size) + 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]]): diff --git a/src/otx/algorithms/common/utils/utils.py b/src/otx/algorithms/common/utils/utils.py index cd8bcd653b5..3eede66c23c 100644 --- a/src/otx/algorithms/common/utils/utils.py +++ b/src/otx/algorithms/common/utils/utils.py @@ -1,18 +1,7 @@ """Collections of Utils 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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 import importlib import inspect @@ -98,7 +87,7 @@ def get_arg_spec( # noqa: C901 # pylint: disable=too-many-branches return tuple(args) -def set_random_seed(seed, logger, deterministic=False): +def set_random_seed(seed, logger=None, deterministic=False): """Set random seed. Args: @@ -116,7 +105,8 @@ def set_random_seed(seed, logger, deterministic=False): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) os.environ["PYTHONHASHSEED"] = str(seed) - logger.info(f"Training seed was set to {seed} w/ deterministic={deterministic}.") + 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 diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index 876e05ca822..a176d64e3a3 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -1,9 +1,9 @@ """Base configurer for mmdet config.""" + # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -# -from typing import Optional +from typing import Optional, Tuple from mmcv.utils import ConfigDict @@ -13,7 +13,6 @@ from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, ) -from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.adapters.mmdet.utils import ( cluster_anchors, @@ -154,9 +153,12 @@ def configure_bbox_head(self, cfg): @staticmethod def configure_input_size( - cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None + 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") @@ -168,15 +170,13 @@ def configure_input_size( "test": (416, 416), "unlabeled": (992, 736), } - manager = InputSizeManager(cfg, base_input_size) - input_size = manager.get_configured_input_size(input_size_config, model_ckpt_path) - if input_size is None: # InputSizePreset.DEFAULT - return - if input_size == (0, 0): # InputSizePreset.AUTO - input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager, use_annotations=True) + 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 diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py index 25e4116d759..01b912d3791 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py @@ -1,19 +1,7 @@ """NNCF Task 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. - +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 from functools import partial from typing import Optional @@ -124,3 +112,4 @@ def _save_model_post_hook(self, modelinfo): 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 index 39b76b4ad9d..bf8079b15e1 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/task.py +++ b/src/otx/algorithms/detection/adapters/mmdet/task.py @@ -1,18 +1,7 @@ """Task of OTX Detection using mmdetection 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. +# SPDX-License-Identifier: Apache-2.0 import glob import io @@ -186,7 +175,7 @@ def configure(self, training=True, ir_options=None, train_dataset=None, export=F ir_options, data_classes, model_classes, - self._hyperparams.learning_parameters.input_size, + self._input_size, train_dataset=train_dataset, ) if should_cluster_anchors(self._recipe_cfg): @@ -195,6 +184,7 @@ def configure(self, training=True, ir_options=None, train_dataset=None, export=F 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 @@ -697,6 +687,7 @@ def save_model(self, output_model: ModelEntity): "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): diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py index d87ce125f38..a3bd184f690 100644 --- a/src/otx/algorithms/detection/task.py +++ b/src/otx/algorithms/detection/task.py @@ -1,18 +1,7 @@ """Task 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. +# SPDX-License-Identifier: Apache-2.0 import io import os @@ -24,6 +13,7 @@ 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, @@ -104,6 +94,15 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] else: 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 + 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. @@ -591,6 +590,7 @@ def save_model(self, output_model: ModelEntity): "config": hyperparams_str, "labels": labels, "confidence_threshold": self.confidence_threshold, + "input_size": self._input_size, "VERSION": 1, } torch.save(modelinfo, buffer) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py index f0f89cd22b6..b1c45dfab6c 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py @@ -1,11 +1,11 @@ """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 +from typing import Any, Optional, Tuple import torch from mmcv.runner import CheckpointLoader @@ -18,7 +18,6 @@ InputSizeManager, remove_custom_hook, ) -from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.utils import append_dist_rank_suffix from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.segmentation.adapters.mmseg.models.heads import otx_head_factory @@ -147,9 +146,12 @@ def patch_chkpt(ckpt_path: str, new_path: Optional[str] = None) -> str: @staticmethod def configure_input_size( - cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None + 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, @@ -157,15 +159,13 @@ def configure_input_size( "test": 544, "unlabeled": 512, } - manager = InputSizeManager(cfg, base_input_size) - input_size = manager.get_configured_input_size(input_size_config, model_ckpt_path) - if input_size is None: # InputSizePreset.DEFAULT - return - if input_size == (0, 0): # InputSizePreset.AUTO - input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager) + 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 diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py index 36fc0287fda..c4979e607d7 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py @@ -1,18 +1,7 @@ """NNCF Task 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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 from functools import partial from typing import List, Optional @@ -122,3 +111,6 @@ def _generate_training_metrics_group(self, learning_curves): 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 index 272ce57cd4d..0c671a06820 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/task.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/task.py @@ -1,18 +1,7 @@ """Task of OTX Segmentation using mmsegmentation 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. +# SPDX-License-Identifier: Apache-2.0 import glob import io @@ -162,9 +151,10 @@ def configure( ir_options, data_classes, model_classes, - self._hyperparams.learning_parameters.input_size, + self._input_size, ) self._config = cfg + self._input_size = cfg.model.pop("input_size", None) return cfg @@ -553,6 +543,7 @@ def save_model(self, output_model: ModelEntity): "model": model_ckpt, "config": hyperparams_str, "labels": labels, + "input_size": self._input_size, "VERSION": 1, } diff --git a/src/otx/algorithms/segmentation/task.py b/src/otx/algorithms/segmentation/task.py index 779bdd10edc..cca2befe81d 100644 --- a/src/otx/algorithms/segmentation/task.py +++ b/src/otx/algorithms/segmentation/task.py @@ -1,18 +1,7 @@ """Task of OTX 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. +# SPDX-License-Identifier: Apache-2.0 import io import os @@ -23,6 +12,7 @@ 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 ( @@ -110,6 +100,12 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] 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, @@ -323,6 +319,7 @@ def save_model(self, output_model: ModelEntity): "model": model_ckpt, "config": hyperparams_str, "labels": labels, + "input_size": self._input_size, "VERSION": 1, } diff --git a/tests/e2e/cli/classification/test_api_xai_sanity_classification.py b/tests/e2e/cli/classification/test_api_xai_sanity_classification.py index d44a9e1684e..39060672ee8 100644 --- a/tests/e2e/cli/classification/test_api_xai_sanity_classification.py +++ b/tests/e2e/cli/classification/test_api_xai_sanity_classification.py @@ -47,10 +47,12 @@ def saliency_maps_check( 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 ahs to be two-dimensional." + 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 sak map shape is incorrect." - assert metadata.data.numpy.dtype == np.uint8, "Sal map has to be uint8 dtype." + 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: diff --git a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py index 287c14325f6..02024d54bfd 100644 --- a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py +++ b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py @@ -8,6 +8,8 @@ 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 @@ -22,7 +24,7 @@ from tests.integration.api.detection.api_detection import DetectionTaskAPIBase, DEFAULT_DET_TEMPLATE_DIR from tests.test_suite.e2e_test_system import e2e_pytest_api -torch.manual_seed(0) +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." @@ -30,7 +32,7 @@ class TestOVDetXAIAPI(DetectionTaskAPIBase): ref_raw_saliency_shapes = { - "MobileNetV2-ATSS": (6, 8), + "MobileNetV2-ATSS": (4, 4), # Need to be adapted to configurable or adaptive input size } @e2e_pytest_api @@ -39,6 +41,7 @@ def test_inference_xai(self): 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) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py index a9692ac44d8..bb2e59da6ad 100644 --- a/tests/e2e/cli/detection/test_detection.py +++ b/tests/e2e/cli/detection/test_detection.py @@ -147,8 +147,6 @@ def test_otx_eval(self, template, tmp_dir_path): @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": - pytest.skip(reason="Issue#2518: YOLOX-L, Tiling-ATSS showed 0.0 after export") 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) diff --git a/tests/e2e/cli/detection/test_tiling_detection.py b/tests/e2e/cli/detection/test_tiling_detection.py index 76d3cbf0d90..b123f5dc502 100644 --- a/tests/e2e/cli/detection/test_tiling_detection.py +++ b/tests/e2e/cli/detection/test_tiling_detection.py @@ -128,8 +128,6 @@ def test_otx_eval(self, template, tmp_dir_path): @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 == "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_openvino_testing(template, tmp_dir_path, otx_dir, args, threshold=0.2, half_precision=half_precision) diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index bc2808b2893..9dc166947c9 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -8,6 +8,7 @@ import os import shutil import sys +import torch from pathlib import Path from typing import Dict import onnx @@ -244,6 +245,12 @@ def otx_export_testing(template, root, dump_features=False, half_precision=False 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 "Visual_Prompting" in template.model_template_id: assert os.path.exists(os.path.join(save_path, "visual_prompting_image_encoder.onnx")) @@ -661,6 +668,14 @@ def nncf_export_testing(template, root): 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): diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index fd6f26d0805..ab513913749 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -66,7 +66,7 @@ def test_configure(self, mocker): 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, InputSizePreset.DEFAULT, "") + 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) @@ -156,33 +156,31 @@ def test_configure_samples_per_gpu(self): @e2e_pytest_unit @pytest.mark.parametrize("input_size", [None, (0, 0), (128, 128)]) - def test_configure_input_size(self, mocker, input_size): + @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_configured_input_size.return_value = input_size + 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, InputSizePreset.DEFAULT, self.data_cfg) + 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): - mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + 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) - if input_size == (0, 0): - mock_input_manager.set_input_size = mocker.MagicMock() - mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = None - self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) - mock_input_manager.set_input_size.assert_not_called() - @e2e_pytest_unit def test_configure_fp16(self): model_cfg = copy.deepcopy(self.model_cfg) 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 index 00405854eb9..1f250eee3ff 100644 --- a/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_config_utils.py @@ -295,11 +295,11 @@ def get_mock_model_ckpt(case): if case == "none": return None if case == "no_input_size": - return {"config": {}} + return {} if case == "input_size_default": - return {"config": {"learning_parameters": {"input_size": {"value": "Default"}}}} + return {"input_size": None} if case == "input_size_exist": - return {"config": {"learning_parameters": {"input_size": {"value": "512x512"}}}} + return {"input_size": (512, 512)} @e2e_pytest_unit @@ -408,40 +408,20 @@ def test_get_input_size_from_cfg(self, test_case): assert input_size_manager.get_input_size_from_cfg("train") == input_size @e2e_pytest_unit - @pytest.mark.parametrize( - "input_size_config", [InputSizePreset.DEFAULT, InputSizePreset.AUTO, InputSizePreset._1024x1024] - ) @pytest.mark.parametrize("model_ckpt_case", ["none", "no_input_size", "input_size_default", "input_size_exist"]) - def test_get_configured_input_size(self, mocker, input_size_config, model_ckpt_case): + 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) - input_size_parser = re.compile("(\d+)x(\d+)") - - if input_size_config == InputSizePreset.DEFAULT: - if ( - model_ckpt_case == "none" - or model_ckpt_case == "no_input_size" - or model_ckpt_case == "input_size_default" - ): - expected_value = None - elif model_ckpt_case == "input_size_exist": - input_size = get_mock_model_ckpt(model_ckpt_case)["config"]["learning_parameters"]["input_size"][ - "value" - ] - pattern = input_size_parser.search(input_size) - expected_value = (int(pattern.group(1)), int(pattern.group(2))) - elif input_size_config == InputSizePreset.AUTO: - expected_value = (0, 0) + + if model_ckpt_case == "none" or model_ckpt_case == "no_input_size" or model_ckpt_case == "input_size_default": + expected_value = None else: - pattern = input_size_parser.search(input_size_config.value) - expected_value = (int(pattern.group(1)), int(pattern.group(2))) + expected_value = (512, 512) # check expected value is returned assert ( - InputSizeManager.get_configured_input_size( - input_size_config, None if model_ckpt_case == "none" else mocker.MagicMock() - ) + InputSizeManager.get_trained_input_size(None if model_ckpt_case == "none" else mocker.MagicMock()) == expected_value ) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index c341d9d1b4e..9df0626976d 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -76,9 +76,7 @@ def test_configure(self, mocker): ) 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, InputSizePreset.DEFAULT, "", train_dataset=self.det_dataset - ) + mock_cfg_data_pipeline.assert_called_once_with(model_cfg, None, "", train_dataset=self.det_dataset) mock_cfg_recipe.assert_called_once_with(model_cfg, train_dataset=self.det_dataset) 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) @@ -169,33 +167,31 @@ def test_configure_samples_per_gpu(self): @e2e_pytest_unit @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) - def test_configure_input_size_not_yolox(self, mocker, input_size): + @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_configured_input_size.return_value = input_size + 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, InputSizePreset.DEFAULT, self.data_cfg) + 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): - mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + 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) - if input_size == (0, 0): - mock_input_manager.set_input_size = mocker.MagicMock() - mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = None - self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) - mock_input_manager.set_input_size.assert_not_called() - @e2e_pytest_unit @pytest.mark.parametrize("is_yolox_tiny", [True, False]) def test_configure_input_size_yolox(self, mocker, is_yolox_tiny): diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py index 49c3b91d6e7..13a44846e15 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py @@ -67,7 +67,7 @@ def test_configure(self, mocker): 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, InputSizePreset.DEFAULT, "") + 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) @@ -156,33 +156,31 @@ def test_configure_samples_per_gpu(self): @e2e_pytest_unit @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) - def test_configure_input_size(self, mocker, input_size): + @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_configured_input_size.return_value = input_size + 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, InputSizePreset.DEFAULT, self.data_cfg) + 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): - mock_input_manager.set_input_size.assert_called_once_with((64, 64)) + 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) - if input_size == (0, 0): - mock_input_manager.set_input_size = mocker.MagicMock() - mock_base_configurer_cls.adapt_input_size_to_dataset.return_value = None - self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) - mock_input_manager.set_input_size.assert_not_called() - @e2e_pytest_unit def test_configure_fp16(self): model_cfg = copy.deepcopy(self.model_cfg) diff --git a/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py b/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py index 6f94c5c04c7..00e0cecb391 100644 --- a/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py +++ b/tests/unit/algorithms/segmentation/adapters/test_otx_segmentation_task.py @@ -80,6 +80,7 @@ def test_label_order(self, mocker): 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(): From 31f4e7c872590833b05e02433597bb12654b5490 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 12 Oct 2023 11:50:45 +0900 Subject: [PATCH 073/146] Update ref. fq number for anomaly e2e2 (#2547) --- .../ote_anomaly_classification_padim/compressed_model.yml | 6 ++---- .../ote_anomaly_detection_padim/compressed_model.yml | 6 ++---- .../ote_anomaly_segmentation_padim/compressed_model.yml | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) 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 index d6d8e5a5f0d..bf922ada0f0 100644 --- 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 @@ -1,7 +1,5 @@ TestToolsAnomalyClassification: nncf: - number_of_fakequantizers: 26 - pot: - number_of_fakequantizers: 28 - ptq: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 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 index a54deb96c19..aa1b8c764c0 100644 --- 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 @@ -1,7 +1,5 @@ TestToolsAnomalyDetection: nncf: - number_of_fakequantizers: 26 - pot: - number_of_fakequantizers: 28 - ptq: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 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 index 6b1c3affdf9..f476fb6f822 100644 --- 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 @@ -1,7 +1,5 @@ TestToolsAnomalySegmentation: nncf: - number_of_fakequantizers: 26 - pot: - number_of_fakequantizers: 28 - ptq: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 From 72ebf7a04b77f06c7f9c749e8868542ea3c4b547 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 12 Oct 2023 14:28:12 +0900 Subject: [PATCH 074/146] Skip e2e det tests by issue2548 (#2550) --- tests/e2e/cli/detection/test_detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py index bb2e59da6ad..cca27634b22 100644 --- a/tests/e2e/cli/detection/test_detection.py +++ b/tests/e2e/cli/detection/test_detection.py @@ -147,6 +147,8 @@ def test_otx_eval(self, template, tmp_dir_path): @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) From 2190a5163f20f6e61787ebbbd1dbb72a428fd10d Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 12 Oct 2023 16:07:38 +0900 Subject: [PATCH 075/146] Add skip to chained TC for issue #2548 (#2552) --- tests/e2e/cli/detection/test_detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py index cca27634b22..a5eb2ead206 100644 --- a/tests/e2e/cli/detection/test_detection.py +++ b/tests/e2e/cli/detection/test_detection.py @@ -221,6 +221,8 @@ def test_otx_deploy_openvino(self, template, tmp_dir_path): 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) From a5769629ba9ff0fce77e4f54d4538c32bee71370 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 12 Oct 2023 16:38:21 +0900 Subject: [PATCH 076/146] Update for release 1.4.3 (#2551) --- docs/source/guide/release_notes/index.rst | 5 +++++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 7d9cf3b418d..2699992177b 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -4,6 +4,11 @@ Releases .. toctree:: :maxdepth: 1 +v1.4.3 (4Q23) +------------- + +- Re introduce adaptive scheduling for training + v1.4.2 (4Q23) ------------- diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 6e53681293f..54213fd97ac 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.3rc1" +__version__ = "1.4.3" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 065f1522fd3..d0eecdea0e1 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.3 -otx==1.4.3rc1 +otx==1.4.3 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From f512025f8d1a020f652849f821749862b928f79e Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Wed, 18 Oct 2023 02:22:34 +0200 Subject: [PATCH 077/146] Update MAPI for 1.5 release (#2555) Upgrade MAPI to v 0.1.6 (#2529) * Upgrade MAPI * Update exp code demo commit * Fix MAPI imports --- requirements/openvino.txt | 2 +- .../action/adapters/openvino/model_wrappers/openvino_models.py | 3 +-- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/requirements/openvino.txt b/requirements/openvino.txt index ca71af3da87..4a1494bb460 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -2,7 +2,7 @@ # OpenVINO Requirements. # nncf==2.6.0 onnx==1.13.0 -openvino-model-api==0.1.5 +openvino-model-api==0.1.6 openvino==2023.0 openvino-dev==2023.0 openvino-telemetry>=2022.1.0 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 index 1f2089abb91..94b920900b1 100644 --- a/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py +++ b/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py @@ -20,12 +20,11 @@ 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 ( - RESIZE_TYPES, ClassificationResult, Detection, - InputTransform, ) from otx.api.entities.datasets import DatasetItemEntity diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index ccf1180ba37..05a2ec3dd6a 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 -openvino-model-api==0.1.5 +openvino-model-api==0.1.6 otx==1.5.0rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From ac8a7dda52114df93bbe09ac43b261c7d40be1e9 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Thu, 19 Oct 2023 02:52:50 +0200 Subject: [PATCH 078/146] Update ModelAPI configuration (#2564) * Update MAPI rt infor for detection * Upadte export info for cls, det and seg * Update unit tests --- .../algorithms/classification/utils/cls_utils.py | 8 ++++++-- src/otx/algorithms/detection/utils/utils.py | 13 ++++++++++--- src/otx/algorithms/segmentation/utils/metadata.py | 7 +++++-- .../algorithms/classification/utils/test_utils.py | 4 ++++ .../detection/utils/test_detection_utils.py | 4 ++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/otx/algorithms/classification/utils/cls_utils.py b/src/otx/algorithms/classification/utils/cls_utils.py index b1506ccc8e7..968586a7d5a 100644 --- a/src/otx/algorithms/classification/utils/cls_utils.py +++ b/src/otx/algorithms/classification/utils/cls_utils.py @@ -98,16 +98,20 @@ def get_cls_model_api_configuration(label_schema: LabelSchemaEntity, inference_c """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_labels = all_labels.strip() - mapi_config[("model_info", "labels")] = all_labels + 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) diff --git a/src/otx/algorithms/detection/utils/utils.py b/src/otx/algorithms/detection/utils/utils.py index 500ec1ad7cf..90c9a7e4476 100644 --- a/src/otx/algorithms/detection/utils/utils.py +++ b/src/otx/algorithms/detection/utils/utils.py @@ -110,16 +110,22 @@ def get_det_model_api_configuration( """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")] = "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" @@ -137,9 +143,10 @@ def get_det_model_api_configuration( for lbl in label_schema.get_labels(include_empty=False): all_labels += lbl.name.replace(" ", "_") + " " - all_labels = all_labels.strip() + all_label_ids += f"{lbl.id_} " - omz_config[("model_info", "labels")] = all_labels + omz_config[("model_info", "labels")] = all_labels.strip() + omz_config[("model_info", "label_ids")] = all_label_ids.strip() return omz_config diff --git a/src/otx/algorithms/segmentation/utils/metadata.py b/src/otx/algorithms/segmentation/utils/metadata.py index 9ecc4e320c2..4a4012a024e 100644 --- a/src/otx/algorithms/segmentation/utils/metadata.py +++ b/src/otx/algorithms/segmentation/utils/metadata.py @@ -12,13 +12,16 @@ 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_labels = all_labels.strip() + 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", "labels"): all_labels, + ("model_info", "labels"): all_labels.strip(), + ("model_info", "label_ids"): all_label_ids.strip(), + ("model_info", "task_type"): "segmentation", } diff --git a/tests/unit/algorithms/classification/utils/test_utils.py b/tests/unit/algorithms/classification/utils/test_utils.py index 009005f3cea..95dbf4883db 100644 --- a/tests/unit/algorithms/classification/utils/test_utils.py +++ b/tests/unit/algorithms/classification/utils/test_utils.py @@ -93,3 +93,7 @@ def test_get_cls_model_api_configuration(default_hierarchical_data): 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/detection/utils/test_detection_utils.py b/tests/unit/algorithms/detection/utils/test_detection_utils.py index 77c46a8c855..0a3a645e29e 100644 --- a/tests/unit/algorithms/detection/utils/test_detection_utils.py +++ b/tests/unit/algorithms/detection/utils/test_detection_utils.py @@ -34,3 +34,7 @@ def test_get_det_model_api_configuration(): 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()) From a5193b1a88f0479fc79b6003ed30df8d05104997 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Thu, 19 Oct 2023 14:48:25 +0200 Subject: [PATCH 079/146] Disable QAT for SegNexts (#2565) * Disable NNCF QAT for SegNext * Del obsolete pot configs * Move NNCF skip marks to test commands to avoid duplication --- .../pot_optimization_config.json | 14 ------ .../configs/ham_segnext_b/template.yaml | 1 - .../pot_optimization_config.json | 14 ------ .../configs/ham_segnext_s/template.yaml | 1 - .../pot_optimization_config.json | 14 ------ .../configs/ham_segnext_t/template.yaml | 1 - .../cli/classification/test_classification.py | 45 ------------------- .../test_segmentation.py | 15 ------- .../cli/classification/test_classification.py | 9 ---- .../test_segmentation.py | 3 -- tests/test_suite/run_test_command.py | 11 +++++ 11 files changed, 11 insertions(+), 117 deletions(-) delete mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_b/pot_optimization_config.json delete mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_s/pot_optimization_config.json delete mode 100644 src/otx/algorithms/segmentation/configs/ham_segnext_t/pot_optimization_config.json diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/pot_optimization_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_b/pot_optimization_config.json deleted file mode 100644 index f9b9b854e30..00000000000 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_b/pot_optimization_config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "algorithms": [ - { - "name": "DefaultQuantization", - "params": { - "preset": "mixed", - "target_device": "ANY", - "range_estimator": { - "preset": "quantile" - } - } - } - ] -} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml index bb0cf4a7e85..50ec187dd37 100644 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_b/template.yaml @@ -14,7 +14,6 @@ framework: OTXSegmentation v0.14.0 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: diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/pot_optimization_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_s/pot_optimization_config.json deleted file mode 100644 index f9b9b854e30..00000000000 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_s/pot_optimization_config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "algorithms": [ - { - "name": "DefaultQuantization", - "params": { - "preset": "mixed", - "target_device": "ANY", - "range_estimator": { - "preset": "quantile" - } - } - } - ] -} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml index d0ca51655cf..f28a01c3464 100644 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_s/template.yaml @@ -14,7 +14,6 @@ framework: OTXSegmentation v0.14.0 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: diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/pot_optimization_config.json b/src/otx/algorithms/segmentation/configs/ham_segnext_t/pot_optimization_config.json deleted file mode 100644 index f9b9b854e30..00000000000 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_t/pot_optimization_config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "algorithms": [ - { - "name": "DefaultQuantization", - "params": { - "preset": "mixed", - "target_device": "ANY", - "range_estimator": { - "preset": "quantile" - } - } - } - ] -} diff --git a/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml b/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml index c2403b3723b..ad041eea837 100644 --- a/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml +++ b/src/otx/algorithms/segmentation/configs/ham_segnext_t/template.yaml @@ -14,7 +14,6 @@ framework: OTXSegmentation v0.14.0 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: diff --git a/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py index 8479bb5c5d7..3252c596bcd 100644 --- a/tests/e2e/cli/classification/test_classification.py +++ b/tests/e2e/cli/classification/test_classification.py @@ -229,9 +229,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @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" - 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 @@ -239,9 +236,6 @@ def test_nncf_optimize(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_export_testing(template, tmp_dir_path) @e2e_pytest_component @@ -249,9 +243,6 @@ def test_nncf_export(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) @e2e_pytest_component @@ -259,9 +250,6 @@ def test_nncf_validate_fq(self, template, tmp_dir_path): @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" - 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 @@ -269,9 +257,6 @@ def test_nncf_eval(self, template, tmp_dir_path): @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" - 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 @@ -452,9 +437,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_m) @e2e_pytest_component @@ -462,9 +444,6 @@ def test_nncf_optimize(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_export_testing(template, tmp_dir_path) @e2e_pytest_component @@ -472,9 +451,6 @@ def test_nncf_export(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) @e2e_pytest_component @@ -482,9 +458,6 @@ def test_nncf_validate_fq(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_eval_testing(template, tmp_dir_path, otx_dir, args_m, threshold=0.01) @e2e_pytest_component @@ -492,9 +465,6 @@ def test_nncf_eval(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_m) @e2e_pytest_component @@ -630,9 +600,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_h) @e2e_pytest_component @@ -640,9 +607,6 @@ def test_nncf_optimize(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_export_testing(template, tmp_dir_path) @e2e_pytest_component @@ -650,9 +614,6 @@ def test_nncf_export(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_eval_testing(template, tmp_dir_path, otx_dir, args_h, threshold=0.01) @e2e_pytest_component @@ -660,9 +621,6 @@ def test_nncf_eval(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "classification", type(self).__name__) @e2e_pytest_component @@ -670,9 +628,6 @@ def test_nncf_validate_fq(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_eval_openvino_testing(template, tmp_dir_path, otx_dir, args_h) @e2e_pytest_component diff --git a/tests/e2e/cli/semantic_segmentation/test_segmentation.py b/tests/e2e/cli/semantic_segmentation/test_segmentation.py index 0315e3e051d..d85698a1394 100644 --- a/tests/e2e/cli/semantic_segmentation/test_segmentation.py +++ b/tests/e2e/cli/semantic_segmentation/test_segmentation.py @@ -188,9 +188,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @pytest.mark.parametrize("template", templates, ids=templates_ids) def test_nncf_optimize(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "segmentation" - 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 @@ -198,9 +195,6 @@ def test_nncf_optimize(self, template, tmp_dir_path): @pytest.mark.parametrize("template", templates, ids=templates_ids) def test_nncf_export(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "segmentation" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_export_testing(template, tmp_dir_path) @e2e_pytest_component @@ -208,9 +202,6 @@ def test_nncf_export(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_validate_fq_testing(template, tmp_dir_path, otx_dir, "semantic_segmentation", type(self).__name__) @e2e_pytest_component @@ -218,9 +209,6 @@ def test_nncf_validate_fq(self, template, tmp_dir_path): @pytest.mark.parametrize("template", templates, ids=templates_ids) def test_nncf_eval(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "segmentation" - 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 @@ -228,9 +216,6 @@ def test_nncf_eval(self, template, tmp_dir_path): @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" - 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 diff --git a/tests/integration/cli/classification/test_classification.py b/tests/integration/cli/classification/test_classification.py index 114b8936c39..9e927d8bfbb 100644 --- a/tests/integration/cli/classification/test_classification.py +++ b/tests/integration/cli/classification/test_classification.py @@ -224,9 +224,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @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" - 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 @@ -436,9 +433,6 @@ def test_otx_eval_deployment(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_m) @e2e_pytest_component @@ -576,7 +570,4 @@ def test_otx_eval_deployment(self, template, tmp_dir_path): @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" - if template.entrypoints.nncf is None: - pytest.skip("nncf entrypoint is none") - nncf_optimize_testing(template, tmp_dir_path, otx_dir, args_h) diff --git a/tests/integration/cli/semantic_segmentation/test_segmentation.py b/tests/integration/cli/semantic_segmentation/test_segmentation.py index 9a5cfad80d6..d4167c7a9ee 100644 --- a/tests/integration/cli/semantic_segmentation/test_segmentation.py +++ b/tests/integration/cli/semantic_segmentation/test_segmentation.py @@ -181,9 +181,6 @@ def test_otx_hpo(self, template, tmp_dir_path): @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" - 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 diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index 9dc166947c9..4da9d77063c 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -627,6 +627,9 @@ def ptq_eval_testing(template, root, otx_dir, args, is_visual_prompting=False): 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) command_line = [ "otx", @@ -649,6 +652,8 @@ def nncf_optimize_testing(template, root, otx_dir, args): 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) command_line = [ "otx", @@ -679,6 +684,8 @@ def nncf_export_testing(template, root): 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" path_to_ref_data = os.path.join( @@ -689,6 +696,8 @@ def nncf_validate_fq_testing(template, root, otx_dir, task_type, 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) command_line = [ "otx", @@ -717,6 +726,8 @@ def nncf_eval_testing(template, root, otx_dir, args, threshold=0.01): 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) command_line = [ "otx", From 8fafd40ad0879f7d4c0d480ee67eaae21c5bc23c Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Tue, 24 Oct 2023 02:58:20 +0200 Subject: [PATCH 080/146] Add Anomaly modelAPI changes to releases/1.4.0 (#2563) * bug fix for legacy openvino models * Apply otx anomaly 1.5 changes * Fix tests * Fix compression config * fix modelAPI imports * update integration tests * Edit config types * Update keys in deployed model --------- Co-authored-by: Ashwin Vaidya Co-authored-by: Kim, Sungchul --- requirements/openvino.txt | 2 +- .../model_wrappers/openvino_models.py | 7 +- .../anomalib/exportable_code/__init__.py | 12 -- .../exportable_code/anomaly_classification.py | 47 ----- .../exportable_code/anomaly_detection.py | 43 ----- .../exportable_code/anomaly_segmentation.py | 43 ----- .../adapters/anomalib/exportable_code/base.py | 47 ----- src/otx/algorithms/anomaly/tasks/inference.py | 51 ++++-- src/otx/algorithms/anomaly/tasks/openvino.py | 166 ++++++++++++------ src/otx/algorithms/anomaly/tasks/train.py | 2 +- src/otx/algorithms/common/utils/ir.py | 4 +- src/otx/algorithms/common/utils/utils.py | 16 +- .../configs/base/configuration.py | 8 +- .../demo/demo_package/model_container.py | 18 +- .../exportable_code/demo/requirements.txt | 2 +- .../prediction_to_annotation_converter.py | 44 +++-- .../compressed_model.yml | 2 + .../compressed_model.yml | 2 + .../compressed_model.yml | 2 + .../anomaly/test_anomaly_classification.py | 4 +- .../e2e/cli/anomaly/test_anomaly_detection.py | 4 +- .../cli/anomaly/test_anomaly_segmentation.py | 4 +- .../anomaly/test_anomaly_classification.py | 6 +- .../cli/anomaly/test_anomaly_detection.py | 6 +- .../cli/anomaly/test_anomaly_segmentation.py | 6 +- .../algorithms/anomaly/tasks/test_openvino.py | 2 +- ...test_prediction_to_annotation_converter.py | 66 ------- 27 files changed, 238 insertions(+), 378 deletions(-) delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py diff --git a/requirements/openvino.txt b/requirements/openvino.txt index 6424a9b6778..4a1494bb460 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -2,7 +2,7 @@ # OpenVINO Requirements. # nncf==2.6.0 onnx==1.13.0 -openvino-model-api==0.1.3 +openvino-model-api==0.1.6 openvino==2023.0 openvino-dev==2023.0 openvino-telemetry>=2022.1.0 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 index dcbfe71a1dc..63a1492c20e 100644 --- a/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py +++ b/src/otx/algorithms/action/adapters/openvino/model_wrappers/openvino_models.py @@ -20,12 +20,9 @@ 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 ( - RESIZE_TYPES, - Detection, - InputTransform, -) +from openvino.model_api.models.utils import Detection from otx.api.entities.datasets import DatasetItemEntity diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py deleted file mode 100644 index 5bcf71d66ca..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Exportable code for Anomaly tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from .anomaly_classification import AnomalyClassification -from .anomaly_detection import AnomalyDetection -from .anomaly_segmentation import AnomalySegmentation -from .base import AnomalyBase - -__all__ = ["AnomalyBase", "AnomalyClassification", "AnomalyDetection", "AnomalySegmentation"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py deleted file mode 100644 index bd61d0f3c05..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_classification.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Classification tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalyClassification(AnomalyBase): - """Wrapper for anomaly classification task.""" - - __model__ = "anomaly_classification" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> float: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - float: Normalized anomaly score - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - pred_score = anomaly_map.reshape(-1).max() - - meta["image_threshold"] = self.metadata["image_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["image_threshold"], meta["min"], meta["max"]) - pred_score = self._normalize(pred_score, meta["image_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - meta["anomaly_map"] = result - - return np.array(pred_score) diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py deleted file mode 100644 index 47e8b49697e..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_detection.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Detection tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalyDetection(AnomalyBase): - """Wrapper for anomaly detection task.""" - - __model__ = "anomaly_detection" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> np.ndarray: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - np.ndarray: Detection Mask - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - - meta["pixel_threshold"] = self.metadata["pixel_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["pixel_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - return result diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py deleted file mode 100644 index 7335e914024..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/anomaly_segmentation.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly Segmentation tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Any, Dict - -import cv2 -import numpy as np - -from .base import AnomalyBase - - -class AnomalySegmentation(AnomalyBase): - """Wrapper for anomaly segmentation task.""" - - __model__ = "anomaly_segmentation" - - def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> np.ndarray: - """Resize the outputs of the model to original image size. - - Args: - outputs (Dict[str, np.ndarray]): Raw outputs of the model after ``infer_sync`` is called. - meta (Dict[str, Any]): Metadata which contains values such as threshold, original image size. - - Returns: - np.ndarray: Segmentation Mask - """ - anomaly_map: np.ndarray = outputs[self.output_blob_name].squeeze() - - meta["pixel_threshold"] = self.metadata["pixel_threshold"] # pylint: disable=no-member - meta["min"] = self.metadata["min"] # pylint: disable=no-member - meta["max"] = self.metadata["max"] # pylint: disable=no-member - meta["threshold"] = self.threshold # pylint: disable=no-member - - anomaly_map = self._normalize(anomaly_map, meta["pixel_threshold"], meta["min"], meta["max"]) - - input_image_height = meta["original_shape"][0] - input_image_width = meta["original_shape"][1] - result = cv2.resize(anomaly_map, (input_image_width, input_image_height)) - - return result diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py b/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py deleted file mode 100644 index cf25d59fff2..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/exportable_code/base.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Wrapper for Open Model Zoo for Anomaly tasks.""" - -# Copyright (C) 2021-2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from typing import Union - -import numpy as np -from openvino.model_api.models import SegmentationModel -from openvino.model_api.models.types import DictValue, NumericalValue - - -class AnomalyBase(SegmentationModel): - """Wrapper for anomaly tasks.""" - - __model__ = "anomaly_base" - - @classmethod - def parameters(cls): - """Dictionary containing model parameters.""" - parameters = super().parameters() - parameters["resize_type"].update_default_value("standard") - parameters.update( - { - "metadata": DictValue(description="Metadata for inference"), - "threshold": NumericalValue(description="Threshold used to classify anomaly"), - } - ) - - return parameters - - @staticmethod - def _normalize( - targets: Union[np.ndarray, np.float32], - threshold: Union[np.ndarray, float], - min_val: Union[np.ndarray, float], - max_val: Union[np.ndarray, float], - ) -> np.ndarray: - """Apply min-max normalization and shift the values such that the threshold value is centered at 0.5.""" - normalized = ((targets - threshold) / (max_val - min_val)) + 0.5 - if isinstance(targets, (np.ndarray, np.float32)): - normalized = np.minimum(normalized, 1) - normalized = np.maximum(normalized, 0) - else: - raise ValueError(f"Targets must be either Tensor or Numpy array. Received {type(targets)}") - return normalized diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 81240f31217..50c5a4b81f7 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -22,7 +22,6 @@ import subprocess # nosec B404 import tempfile from glob import glob -from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from warnings import warn @@ -36,7 +35,6 @@ PostProcessingConfigurationCallback, ) from omegaconf import DictConfig, ListConfig -from openvino.runtime import Core, serialize from pytorch_lightning import Trainer from otx.algorithms.anomaly.adapters.anomalib.callbacks import ( @@ -47,6 +45,8 @@ from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -296,6 +296,8 @@ def export( 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: @@ -306,7 +308,7 @@ def export( 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) + self._add_metadata_to_ir(xml_file, export_type) with open(bin_file, "rb") as file: output_model.set_data("openvino.bin", file.read()) @@ -319,40 +321,51 @@ def export( 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, xml_file: str) -> None: - """Adds the metadata to the model IR. + 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. Remove this function when Anomalib is upgraded as the model graph will contain the required ops + # 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() - core = Core() - model = core.read_model(xml_file) + extra_model_data: Dict[Tuple[str, str], Any] = {} for key, value in metadata.items(): - if key == "transform": + if key in ("transform", "min", "max"): continue - model.set_rt_info(value, ["model_info", key]) + 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": - model.set_rt_info(self._serialize_list(transform_dict["mean"]), ["model_info", "mean_values"]) - model.set_rt_info(self._serialize_list(transform_dict["std"]), ["model_info", "scale_values"]) + 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": - model.set_rt_info(transform_dict["height"], ["model_info", "orig_height"]) - model.set_rt_info(transform_dict["width"], ["model_info", "orig_width"]) + 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") - model.set_rt_info("AnomalyDetection", ["model_info", "model_type"]) - tmp_xml_path = Path(Path(xml_file).parent) / "tmp.xml" - serialize(model, str(tmp_xml_path)) - tmp_xml_path.rename(xml_file) - Path(str(tmp_xml_path.parent / tmp_xml_path.stem) + ".bin").unlink() + # 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" + 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.""" diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index 3800b264e0d..f607a4a5d4e 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -19,7 +19,7 @@ import os import random import tempfile -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple, Union from zipfile import ZipFile import nncf @@ -27,14 +27,14 @@ import openvino.runtime as ov from addict import Dict as ADDict from anomalib.data.utils.transform import get_transforms -from anomalib.deploy import OpenVINOInferencer from nncf.common.quantization.structs import QuantizationPreset from omegaconf import OmegaConf +from openvino.model_api.models import AnomalyDetection, AnomalyResult -import otx.algorithms.anomaly.adapters.anomalib.exportable_code from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -75,22 +75,23 @@ logger = get_logger(__name__) -class OTXOpenVINOAnomalyDataloader: - """Dataloader for loading OTX dataset into OTX OpenVINO Inferencer. +class OTXNNCFAnomalyDataloader: + """Dataloader for loading OTX dataset for NNCF optimization. Args: dataset (DatasetEntity): OTX dataset entity - inferencer (OpenVINOInferencer): OpenVINO Inferencer + model: (AnomalyDetection) The modelAPI model used for fetching the transforms. + shuffle (bool, optional): Shuffle dataset. Defaults to True. """ def __init__( self, dataset: DatasetEntity, - inferencer: OpenVINOInferencer, + model: AnomalyDetection, shuffle: bool = True, ): self.dataset = dataset - self.inferencer = inferencer + self.model = model self.shuffler = None if shuffle: self.shuffler = list(range(len(dataset))) @@ -110,9 +111,12 @@ def __getitem__(self, index: int): image = self.dataset[index].numpy annotation = self.dataset[index].annotation_scene - inputs = self.inferencer.pre_process(image) - return (index, annotation), inputs + 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. @@ -135,7 +139,7 @@ def __init__(self, task_environment: TaskEnvironment) -> None: self.task_environment = task_environment self.task_type = self.task_environment.model_template.task_type self.config = self.get_config() - self.inferencer = self.load_inferencer() + 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] @@ -173,15 +177,13 @@ def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameter if inference_parameters is not None: update_progress_callback = inference_parameters.update_progress # type: ignore - # This always assumes that threshold is available in the task environment's model - meta_data = self.get_metadata() for idx, dataset_item in enumerate(dataset): - image_result = self.inferencer.predict(dataset_item.numpy, metadata=meta_data) + image_result: AnomalyResult = self.inference_model(dataset_item.numpy) # TODO: inferencer should return predicted label and mask - pred_label = image_result.pred_score >= 0.5 - pred_mask = (image_result.anomaly_map >= 0.5).astype(np.uint8) - probability = image_result.pred_score if pred_label else 1 - image_result.pred_score + 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: @@ -320,7 +322,7 @@ def optimize( ) logger.info("Starting PTQ optimization.") - data_loader = OTXOpenVINOAnomalyDataloader(dataset=dataset, inferencer=self.inferencer) + data_loader = OTXNNCFAnomalyDataloader(dataset=dataset, model=self.inference_model) quantization_dataset = nncf.Dataset(data_loader, lambda data: data[1]) with tempfile.TemporaryDirectory() as tempdir: @@ -355,34 +357,105 @@ def optimize( 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.set_data("metadata", self.task_environment.model.get_data("metadata")) output_model.model_format = ModelFormat.OPENVINO output_model.optimization_type = ModelOptimizationType.POT output_model.optimization_methods = [OptimizationMethod.QUANTIZATION] output_model.precision = [ModelPrecision.INT8] self.task_environment.model = output_model - self.inferencer = self.load_inferencer() + 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 load_inferencer(self) -> OpenVINOInferencer: + def get_openvino_model(self) -> AnomalyDetection: """Create the OpenVINO inferencer object. Returns: - OpenVINOInferencer object + AnomalyDetection model """ if self.task_environment.model is None: raise Exception("task_environment.model is None. Cannot load weights.") - return OpenVINOInferencer( - path=( - self.task_environment.model.get_data("openvino.xml"), - self.task_environment.model.get_data("openvino.bin"), - ), - metadata=self.get_metadata(), - ) + 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: @@ -412,18 +485,20 @@ def _get_openvino_configuration(self) -> Dict[str, Any]: if self.task_environment.model is None: raise Exception("task_environment.model is None. Cannot get configuration.") - configuration = { - "metadata": self.get_metadata(), + configuration: Dict[str, Any] = { "labels": LabelSchemaMapper.forward(self.task_environment.label_schema), - "threshold": 0.5, } - - if "transforms" not in self.config.keys(): - configuration["mean_values"] = list(np.array([0.485, 0.456, 0.406]) * 255) - configuration["scale_values"] = list(np.array([0.229, 0.224, 0.225]) * 255) - else: - configuration["mean_values"] = self.config.transforms.mean - configuration["scale_values"] = self.config.transforms.std + # 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 @@ -446,7 +521,7 @@ def deploy(self, output_model: ModelEntity) -> None: task_type = str(self.task_type).lower() - parameters["type_of_model"] = task_type + parameters["type_of_model"] = "AnomalyDetection" parameters["converter_type"] = task_type.upper() parameters["model_parameters"] = self._get_openvino_configuration() zip_buffer = io.BytesIO() @@ -455,17 +530,6 @@ def deploy(self, output_model: ModelEntity) -> None: 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)) - # model_wrappers files - for root, _, files in os.walk( - os.path.dirname(otx.algorithms.anomaly.adapters.anomalib.exportable_code.__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("exportable_code/")[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")) diff --git a/src/otx/algorithms/anomaly/tasks/train.py b/src/otx/algorithms/anomaly/tasks/train.py index 639d22c65a6..a1f4759ab1a 100644 --- a/src/otx/algorithms/anomaly/tasks/train.py +++ b/src/otx/algorithms/anomaly/tasks/train.py @@ -67,7 +67,7 @@ def train( if seed: logger.info(f"Setting seed to {seed}") seed_everything(seed, workers=True) - config.trainer.deterministic = deterministic + config.trainer.deterministic = "warn" if deterministic else deterministic logger.info("Training Configs '%s'", config) diff --git a/src/otx/algorithms/common/utils/ir.py b/src/otx/algorithms/common/utils/ir.py index 6bf54131b18..946ef9b40c0 100644 --- a/src/otx/algorithms/common/utils/ir.py +++ b/src/otx/algorithms/common/utils/ir.py @@ -19,7 +19,7 @@ def check_if_quantized(model: Any) -> bool: return False -def embed_ir_model_data(xml_file: str, data_items: Dict[Tuple[str], Any]) -> None: +def embed_ir_model_data(xml_file: str, data_items: Dict[Tuple[str, str], Any]) -> None: """Embeds serialized data to IR xml file. Args: @@ -34,6 +34,6 @@ def embed_ir_model_data(xml_file: str, data_items: Dict[Tuple[str], Any]) -> Non # workaround for CVS-110054 tmp_xml_path = Path(Path(xml_file).parent) / "tmp.xml" - serialize(model, tmp_xml_path) + serialize(model, str(tmp_xml_path)) 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/utils.py b/src/otx/algorithms/common/utils/utils.py index 24af86d48b9..cd8bcd653b5 100644 --- a/src/otx/algorithms/common/utils/utils.py +++ b/src/otx/algorithms/common/utils/utils.py @@ -21,9 +21,10 @@ import sys from collections import defaultdict from pathlib import Path -from typing import Callable, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple import numpy as np +import onnx import yaml from addict import Dict as adict @@ -153,3 +154,16 @@ def read_py_config(filename: str) -> adict: ) 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) diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration.py b/src/otx/algorithms/visual_prompting/configs/base/configuration.py index 63dc1e726a2..d9cdae0eaeb 100644 --- a/src/otx/algorithms/visual_prompting/configs/base/configuration.py +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration.py @@ -83,17 +83,17 @@ class __Postprocessing(ParameterGroup): affects_outcome_of=ModelLifecycle.INFERENCE, ) - orig_width = configurable_float( + orig_width = configurable_integer( header="Original width", description="Model input width before embedding processing.", - default_value=64.0, + default_value=64, affects_outcome_of=ModelLifecycle.INFERENCE, ) - orig_height = configurable_float( + orig_height = configurable_integer( header="Original height", description="Model input height before embedding processing.", - default_value=64.0, + default_value=64, affects_outcome_of=ModelLifecycle.INFERENCE, ) 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 index eda80faab7a..88725a07df3 100644 --- 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 @@ -15,8 +15,8 @@ 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.tiler import Tiler from otx.api.utils.detection_utils import detection2array +from otx.api.utils.tiler import Tiler from .utils import get_model_path, get_parameters @@ -49,7 +49,21 @@ def __init__(self, model_dir: Path, device="CPU") -> None: # labels for modelAPI wrappers can be empty, because unused in pre- and postprocessing self.model_parameters = self.parameters["model_parameters"] - self.model_parameters["labels"] = [] + + 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: + self.model_parameters["labels"] = [] self._initialize_wrapper() self.core_model = Model.create_model( diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index d0eecdea0e1..6eda94d0e5e 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 -openvino-model-api==0.1.3 +openvino-model-api==0.1.6 otx==1.4.3 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/prediction_to_annotation_converter.py b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py index b9931aed01e..90ba92a0c4e 100644 --- a/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py +++ b/src/otx/api/usecases/exportable_code/prediction_to_annotation_converter.py @@ -10,6 +10,7 @@ import cv2 import numpy as np from openvino.model_api.models import utils +from openvino.model_api.models.utils import AnomalyResult from otx.api.entities.annotation import ( Annotation, @@ -20,10 +21,9 @@ 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.polygon import Point, Polygon 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.anomaly_utils import create_detection_annotation_from_anomaly_heatmap 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 @@ -321,7 +321,7 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Convert predictions to OTX Annotation Scene using the metadata. Args: @@ -331,15 +331,14 @@ def convert_to_annotation(self, predictions: np.ndarray, metadata: Dict[str, Any Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_label = predictions >= metadata.get("threshold", 0.5) - - label = self.anomalous_label if pred_label else self.normal_label - probability = (1 - predictions) if predictions < 0.5 else predictions + 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(probability))], + labels=[ScoredLabel(label=label, probability=float(predictions.pred_score))], ) ] return AnnotationSceneEntity(kind=AnnotationSceneKind.PREDICTION, annotations=annotations) @@ -358,19 +357,21 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + 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. + predictions (AnomalyResult): Raw predictions from the model. metadata (Dict[str, Any]): Variable containing metadata information. Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_mask = predictions >= 0.5 - mask = pred_mask.squeeze().astype(np.uint8) - annotations = create_annotation_from_segmentation_map(mask, predictions, self.label_map) + 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, self.label_map + ) if len(annotations) == 0: # TODO: add confidence to this label annotations = [ @@ -400,7 +401,7 @@ def __init__(self, label_schema: LabelSchemaEntity): 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: np.ndarray, metadata: Dict[str, Any]) -> AnnotationSceneEntity: + def convert_to_annotation(self, predictions: AnomalyResult, metadata: Dict[str, Any]) -> AnnotationSceneEntity: """Convert predictions to OTX Annotation Scene using the metadata. Args: @@ -410,9 +411,18 @@ def convert_to_annotation(self, predictions: np.ndarray, metadata: Dict[str, Any Returns: AnnotationSceneEntity: OTX annotation scene entity object. """ - pred_mask = predictions >= 0.5 - mask = pred_mask.squeeze().astype(np.uint8) - annotations = create_detection_annotation_from_anomaly_heatmap(mask, predictions, self.label_map) + 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 = [ 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 index 01460cc560c..1c94650f275 100644 --- 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 @@ -1,5 +1,7 @@ TestToolsAnomalyClassification: nncf: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 pot: number_of_fakequantizers: 28 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 index 4bca1f02a5d..c119a9d5f52 100644 --- 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 @@ -1,5 +1,7 @@ TestToolsAnomalyDetection: nncf: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 pot: number_of_fakequantizers: 28 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 index 1d41886dfd4..217f636a463 100644 --- 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 @@ -1,5 +1,7 @@ TestToolsAnomalySegmentation: nncf: number_of_fakequantizers: 27 + ptq: + number_of_fakequantizers: 28 pot: number_of_fakequantizers: 28 diff --git a/tests/e2e/cli/anomaly/test_anomaly_classification.py b/tests/e2e/cli/anomaly/test_anomaly_classification.py index 7dade9a11c6..10da1239ea4 100644 --- a/tests/e2e/cli/anomaly/test_anomaly_classification.py +++ b/tests/e2e/cli/anomaly/test_anomaly_classification.py @@ -58,12 +58,12 @@ 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=False) + 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) + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) diff --git a/tests/e2e/cli/anomaly/test_anomaly_detection.py b/tests/e2e/cli/anomaly/test_anomaly_detection.py index 9068da92a10..10b008980ac 100644 --- a/tests/e2e/cli/anomaly/test_anomaly_detection.py +++ b/tests/e2e/cli/anomaly/test_anomaly_detection.py @@ -58,12 +58,12 @@ 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=False) + 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) + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) diff --git a/tests/e2e/cli/anomaly/test_anomaly_segmentation.py b/tests/e2e/cli/anomaly/test_anomaly_segmentation.py index 50e9d0da2a2..a3863debf25 100644 --- a/tests/e2e/cli/anomaly/test_anomaly_segmentation.py +++ b/tests/e2e/cli/anomaly/test_anomaly_segmentation.py @@ -58,12 +58,12 @@ 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=False) + 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) + otx_export_testing(template, tmp_dir_path, check_ir_meta=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) diff --git a/tests/integration/cli/anomaly/test_anomaly_classification.py b/tests/integration/cli/anomaly/test_anomaly_classification.py index c9173e08128..72ae511c8a9 100644 --- a/tests/integration/cli/anomaly/test_anomaly_classification.py +++ b/tests/integration/cli/anomaly/test_anomaly_classification.py @@ -11,6 +11,7 @@ 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 ( + generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, @@ -18,7 +19,6 @@ otx_eval_testing, otx_export_testing, otx_train_testing, - generate_model_template_testing, ) args = { @@ -42,7 +42,7 @@ 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=False) + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) @@ -57,7 +57,7 @@ def test_otx_export_fp16(self, template, tmp_dir_path): @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) + 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) diff --git a/tests/integration/cli/anomaly/test_anomaly_detection.py b/tests/integration/cli/anomaly/test_anomaly_detection.py index 0cb33a51fd8..795c64ab49d 100644 --- a/tests/integration/cli/anomaly/test_anomaly_detection.py +++ b/tests/integration/cli/anomaly/test_anomaly_detection.py @@ -11,6 +11,7 @@ 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 ( + generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, @@ -18,7 +19,6 @@ otx_eval_testing, otx_export_testing, otx_train_testing, - generate_model_template_testing, ) args = { @@ -42,7 +42,7 @@ 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=False) + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) @@ -57,7 +57,7 @@ def test_otx_export_fp16(self, template, tmp_dir_path): @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) + 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) diff --git a/tests/integration/cli/anomaly/test_anomaly_segmentation.py b/tests/integration/cli/anomaly/test_anomaly_segmentation.py index 17483504515..43366a80676 100644 --- a/tests/integration/cli/anomaly/test_anomaly_segmentation.py +++ b/tests/integration/cli/anomaly/test_anomaly_segmentation.py @@ -11,6 +11,7 @@ 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 ( + generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, @@ -18,7 +19,6 @@ otx_eval_testing, otx_export_testing, otx_train_testing, - generate_model_template_testing, ) args = { @@ -42,7 +42,7 @@ 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=False) + otx_train_testing(template, tmp_dir_path, otx_dir, args, deterministic=True) @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) @@ -57,7 +57,7 @@ def test_otx_export_fp16(self, template, tmp_dir_path): @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) + 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) diff --git a/tests/unit/algorithms/anomaly/tasks/test_openvino.py b/tests/unit/algorithms/anomaly/tasks/test_openvino.py index 82d1174bb97..8fb222189aa 100644 --- a/tests/unit/algorithms/anomaly/tasks/test_openvino.py +++ b/tests/unit/algorithms/anomaly/tasks/test_openvino.py @@ -91,7 +91,7 @@ def test_openvino(self, tmpdir, setup_task_environment): openvino_task.deploy(output_model) assert output_model.exportable_code is not None - @patch.multiple(OpenVINOTask, get_config=MagicMock(), load_inferencer=MagicMock()) + @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.""" 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 index f88847fbe9c..fdc81c94101 100644 --- 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 @@ -933,69 +933,3 @@ def test_anomaly_classification_to_annotation_init( converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) assert converter.normal_label == non_empty_labels[0] assert converter.anomalous_label == non_empty_labels[2] - - @pytest.mark.priority_medium - @pytest.mark.unit - @pytest.mark.reqids(Requirements.REQ_1) - def test_anomaly_classification_to_annotation_convert( - self, - ): - """ - Description: - Check "AnomalyClassificationToAnnotationConverter" class "convert_to_annotation" method - - Input data: - "AnomalyClassificationToAnnotationConverter" class object, "predictions" array - - 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 - "metadata" dictionary with specified "threshold" key - 2. Check attributes of "AnnotationSceneEntity" object returned by "convert_to_annotation" method for - "metadata" dictionary without specified "threshold" key - """ - - def check_annotation(actual_annotation: Annotation, expected_labels: list): - assert isinstance(actual_annotation, Annotation) - assert actual_annotation.get_labels() == expected_labels - assert isinstance(actual_annotation.shape, Rectangle) - assert Rectangle.is_full_box(rectangle=actual_annotation.shape) - - non_empty_labels = [ - LabelEntity(name="Normal", domain=Domain.CLASSIFICATION, id=ID("1")), - LabelEntity( - name="Anomalous", - domain=Domain.CLASSIFICATION, - id=ID("2"), - is_anomalous=True, - ), - ] - label_group = LabelGroup(name="Anomaly classification labels group", labels=non_empty_labels) - label_schema = LabelSchemaEntity(label_groups=[label_group]) - converter = AnomalyClassificationToAnnotationConverter(label_schema=label_schema) - predictions = np.array([0.7]) - # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" for "metadata" with - # specified "threshold" key - metadata = { - "non-required key": 1, - "other non-required key": 2, - "threshold": 0.8, - } - predictions_to_annotations = converter.convert_to_annotation(predictions=predictions, metadata=metadata) - check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) - check_annotation( - predictions_to_annotations.annotations[0], - expected_labels=[ScoredLabel(label=non_empty_labels[0], probability=0.7)], - ) - # Checking attributes of "AnnotationSceneEntity" returned by "convert_to_annotation" for "metadata" without - # specified "threshold" key - metadata = {"non-required key": 1, "other non-required key": 2} - predictions_to_annotations = converter.convert_to_annotation(predictions=predictions, metadata=metadata) - check_annotation_scene(annotation_scene=predictions_to_annotations, expected_length=1) - check_annotation( - predictions_to_annotations.annotations[0], - expected_labels=[ScoredLabel(label=non_empty_labels[1], probability=0.7)], - ) From 65ddbfacaf401ee447a9e3e4984ac8c483b0407d Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Tue, 24 Oct 2023 13:11:40 +0900 Subject: [PATCH 081/146] Fix the CustomNonLinearClsHead when the batch_size is set to 1 (#2571) Fix bn1d issue Co-authored-by: sungmanc --- .../adapters/mmcls/models/heads/custom_cls_head.py | 4 ++++ 1 file changed, 4 insertions(+) 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 index fcf2008e795..ec760df7f50 100644 --- 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 @@ -42,6 +42,10 @@ def forward(self, 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 From 09810359169027d925489e53fb976678899b8be9 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Tue, 24 Oct 2023 14:43:10 +0200 Subject: [PATCH 082/146] Update ModelAPI configuration (#2564 from 1.4) (#2568) Update ModelAPI configuration (#2564) * Update MAPI rt infor for detection * Upadte export info for cls, det and seg * Update unit tests --- .../algorithms/classification/utils/cls_utils.py | 8 ++++++-- src/otx/algorithms/detection/utils/utils.py | 13 ++++++++++--- src/otx/algorithms/segmentation/utils/metadata.py | 7 +++++-- .../algorithms/classification/utils/test_utils.py | 4 ++++ .../detection/utils/test_detection_utils.py | 4 ++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/otx/algorithms/classification/utils/cls_utils.py b/src/otx/algorithms/classification/utils/cls_utils.py index 88db1ed2ecb..67e3b1c5601 100644 --- a/src/otx/algorithms/classification/utils/cls_utils.py +++ b/src/otx/algorithms/classification/utils/cls_utils.py @@ -98,16 +98,20 @@ def get_cls_model_api_configuration(label_schema: LabelSchemaEntity, inference_c """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_labels = all_labels.strip() - mapi_config[("model_info", "labels")] = all_labels + 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) diff --git a/src/otx/algorithms/detection/utils/utils.py b/src/otx/algorithms/detection/utils/utils.py index 0297b148be5..5c68ecfa31e 100644 --- a/src/otx/algorithms/detection/utils/utils.py +++ b/src/otx/algorithms/detection/utils/utils.py @@ -110,16 +110,22 @@ def get_det_model_api_configuration( """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")] = "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" @@ -137,9 +143,10 @@ def get_det_model_api_configuration( for lbl in label_schema.get_labels(include_empty=False): all_labels += lbl.name.replace(" ", "_") + " " - all_labels = all_labels.strip() + all_label_ids += f"{lbl.id_} " - omz_config[("model_info", "labels")] = all_labels + omz_config[("model_info", "labels")] = all_labels.strip() + omz_config[("model_info", "label_ids")] = all_label_ids.strip() return omz_config diff --git a/src/otx/algorithms/segmentation/utils/metadata.py b/src/otx/algorithms/segmentation/utils/metadata.py index f234c3ad65e..0245d3de03c 100644 --- a/src/otx/algorithms/segmentation/utils/metadata.py +++ b/src/otx/algorithms/segmentation/utils/metadata.py @@ -12,14 +12,17 @@ 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_labels = all_labels.strip() + 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", "labels"): all_labels, ("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/tests/unit/algorithms/classification/utils/test_utils.py b/tests/unit/algorithms/classification/utils/test_utils.py index bbff0ccfdc3..f0f3819f009 100644 --- a/tests/unit/algorithms/classification/utils/test_utils.py +++ b/tests/unit/algorithms/classification/utils/test_utils.py @@ -93,3 +93,7 @@ def test_get_cls_model_api_configuration(default_hierarchical_data): 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/detection/utils/test_detection_utils.py b/tests/unit/algorithms/detection/utils/test_detection_utils.py index 77c46a8c855..0a3a645e29e 100644 --- a/tests/unit/algorithms/detection/utils/test_detection_utils.py +++ b/tests/unit/algorithms/detection/utils/test_detection_utils.py @@ -34,3 +34,7 @@ def test_get_det_model_api_configuration(): 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()) From 0ba7b6240dff10c62782a71ada10e6145502204f Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 25 Oct 2023 08:34:36 +0900 Subject: [PATCH 083/146] Update for 1.4.4rc1 (#2572) --- CHANGELOG.md | 7 +++++++ src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9886a2edd..88e1beda101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## \[v1.4.4\] + +### Enhancements + +- Update ModelAPI configuration() +- Add Anomaly modelAPI changes () + ## \[v1.4.3\] ### Enhancements diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 54213fd97ac..0730c04e7d7 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.3" +__version__ = "1.4.4rc1" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 6eda94d0e5e..26d5f57a346 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.6 -otx==1.4.3 +otx==1.4.4rc1 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 1285168f67cf1fb80496a325f089bc5aecffa7ae Mon Sep 17 00:00:00 2001 From: Vinnam Kim Date: Wed, 25 Oct 2023 17:02:16 +0900 Subject: [PATCH 084/146] Hotfix DatasetEntity.get_combined_subset function loop (#2577) Fix get_combined_subset function --- src/otx/api/entities/datasets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/otx/api/entities/datasets.py b/src/otx/api/entities/datasets.py index 1a37458ebe9..ff8660154ca 100644 --- a/src/otx/api/entities/datasets.py +++ b/src/otx/api/entities/datasets.py @@ -349,8 +349,9 @@ def get_combined_subset(self, subsets: List[Subset]) -> "DatasetEntity": Returns: DatasetEntity: DatasetEntity with items matching subsets """ + to_keep = set(subsets) dataset = DatasetEntity( - items=[item for item in self._items if item.subset in set(subsets)], + items=[item for item in self if item.subset in to_keep], purpose=self.purpose, ) return dataset From 7cacd94a73d24511797377ec6f723683d0f0fc9c Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 27 Oct 2023 16:20:56 +0900 Subject: [PATCH 085/146] Revert default input size to `Default` due to YOLOX perf regression (#2580) Signed-off-by: Songki Choi --- src/otx/algorithms/classification/configs/configuration.yaml | 5 +++-- .../detection/configs/detection/configuration.yaml | 5 +++-- .../configs/instance_segmentation/configuration.yaml | 5 +++-- .../detection/configs/rotated_detection/configuration.yaml | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index 099c20af4f8..cf4b7be35f8 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -277,11 +277,12 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Auto + 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. - Defaults to Auto, in which input size is automatically determined based on dataset statistics. + 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. diff --git a/src/otx/algorithms/detection/configs/detection/configuration.yaml b/src/otx/algorithms/detection/configs/detection/configuration.yaml index d36b0d941bc..ef3a46315bc 100644 --- a/src/otx/algorithms/detection/configs/detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -245,11 +245,12 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Auto + 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. - Defaults to Auto, in which input size is automatically determined based on dataset statistics. + 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. diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml index f0672ae5ff8..d18107fbb33 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -245,11 +245,12 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Auto + 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. - Defaults to Auto, in which input size is automatically determined based on dataset statistics. + 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. diff --git a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml index 52c232aaa7f..eb2cecbb289 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -245,11 +245,12 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Auto + 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. - Defaults to Auto, in which input size is automatically determined based on dataset statistics. + 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. From 803699484fbcc0a52924daf274290b92de55020e Mon Sep 17 00:00:00 2001 From: Sungman Cho Date: Tue, 31 Oct 2023 11:15:44 +0900 Subject: [PATCH 086/146] Fix for the degradation issue of the classification task (#2585) * Revert to sync with 1.4.0 * Remove repeat data * Convert to the RGB value * Fix color conversion logic * Fix precommit --- .../classification/adapters/mmcls/utils/config_utils.py | 3 --- src/otx/algorithms/classification/configs/configuration.yaml | 2 +- src/otx/recipes/stages/classification/incremental.yaml | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py index 4595e1482cc..59e405db542 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py @@ -20,7 +20,6 @@ from otx.algorithms.common.adapters.mmcv.utils import ( get_dataset_configs, - patch_color_conversion, ) from otx.algorithms.common.utils.logger import get_logger @@ -49,8 +48,6 @@ def patch_datasets( for cfg in cfgs: cfg.update(kwargs) - patch_color_conversion(config) - def patch_evaluation(config: Config, task: str): """Patch evaluation.""" diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index cf4b7be35f8..03c327f88b2 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -210,7 +210,7 @@ learning_parameters: warning: This is applied exclusively when early stopping is enabled. use_adaptive_interval: affects_outcome_of: TRAINING - default_value: false + 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 diff --git a/src/otx/recipes/stages/classification/incremental.yaml b/src/otx/recipes/stages/classification/incremental.yaml index 5835f4f5ba5..b40a5fdfd5b 100644 --- a/src/otx/recipes/stages/classification/incremental.yaml +++ b/src/otx/recipes/stages/classification/incremental.yaml @@ -42,10 +42,6 @@ custom_hooks: [ start: 3, min_delta_ratio: 0.01, priority: 75, - }, - { - type: AdaptiveRepeatDataHook, - priority: ABOVE_NORMAL } ] From 06775f221a252e66eba6e5454a6c29635cb189fd Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 31 Oct 2023 13:24:47 +0900 Subject: [PATCH 087/146] Bump datumaro version to 1.5.1rc3 (#2587) --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 80129e6edff..f4682fc6150 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort>=6.0.0 prettytable protobuf>=3.20.0 pyyaml -datumaro~=1.5.0 +datumaro==1.5.1rc3 psutil scipy>=1.8 bayesian-optimization>=1.2.0 From 8598b05f862c04278e9e684ceece72178da4f1f2 Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Tue, 31 Oct 2023 12:33:03 +0100 Subject: [PATCH 088/146] Add label ids to anomaly OpenVINO model xml (#2590) * Add label ids to model xml --------- --- src/otx/algorithms/anomaly/tasks/inference.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 50c5a4b81f7..aac3d525b5e 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -359,7 +359,20 @@ def _add_metadata_to_ir(self, model_file: str, export_type: ExportType) -> None: 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" + + 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: From 23a4644d8049bb609ba24be10197347b19f89fe5 Mon Sep 17 00:00:00 2001 From: Wonju Lee Date: Wed, 1 Nov 2023 09:26:02 +0900 Subject: [PATCH 089/146] Fix DeiT-Tiny model regression during class incremental training (#2594) * enable IBloss for DeiT-Tiny * update changelog * add docstring --- CHANGELOG.md | 1 + .../mmcls/models/heads/custom_vision_transformer_head.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 393a2f10560..d49a87410d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file. ### Bug fixes - Fix F1 auto-threshold to choose best largest confidence () +- Fix IBLoss enablement with DeiT-Tiny when class incremental training () ### Known issues 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 index b9ce9ef6c8f..38a2d704c2c 100644 --- 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 @@ -31,3 +31,10 @@ def loss(self, cls_score, gt_label, feature=None): 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 From a5d12b78715e80aad1bc4f460a62f74e4b2d1e80 Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Wed, 1 Nov 2023 02:04:32 +0100 Subject: [PATCH 090/146] Add label ids to model xml in release 1.5 (#2591) Add label ids to model xml --- src/otx/algorithms/anomaly/tasks/inference.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 50c5a4b81f7..aac3d525b5e 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -359,7 +359,20 @@ def _add_metadata_to_ir(self, model_file: str, export_type: ExportType) -> None: 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" + + 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: From ba1a30d3af3b89156f8239f33f6b15883fa67a7a Mon Sep 17 00:00:00 2001 From: Wonju Lee Date: Wed, 1 Nov 2023 12:50:23 +0900 Subject: [PATCH 091/146] Fix DeiT-Tiny regression test for release/1.4.0 (#2595) * Fix DeiT regression test * update changelog * temp --- CHANGELOG.md | 1 + .../mmcls/models/heads/custom_vision_transformer_head.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88e1beda101..25921b9d4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Update ModelAPI configuration() - Add Anomaly modelAPI changes () +- Fix IBLoss enablement with DeiT-Tiny when class incremental training () ## \[v1.4.3\] 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 index b9ce9ef6c8f..38a2d704c2c 100644 --- 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 @@ -31,3 +31,10 @@ def loss(self, cls_score, gt_label, feature=None): 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 From c22c683664a28b076187311cecaa8e7cc048b2ee Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 6 Nov 2023 15:48:18 +0900 Subject: [PATCH 092/146] Fix mmcls bug not wrapping model in DataParallel on CPUs (#2601) Wrap multi-label and h-label classification models by MMDataParallel in case of CPU training. --------- Signed-off-by: Songki Choi --- CHANGELOG.md | 4 ++++ src/otx/algorithms/classification/adapters/mmcls/task.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25921b9d4c8..5f7609c4e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,11 @@ All notable changes to this project will be documented in this file. - Update ModelAPI configuration() - Add Anomaly modelAPI changes () + +### Bug fixes + - Fix IBLoss enablement with DeiT-Tiny when class incremental training () +- Fix mmcls bug not wrapping model in DataParallel on CPUs () ## \[v1.4.3\] diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 01436571d08..312c968956c 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -381,6 +381,10 @@ def _train_model( # Model model = self.build_model(cfg, fp16=cfg.get("fp16", False)) + if not torch.cuda.is_available(): + # 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 cfg.distributed: From a4abbed43d5e83553cb0231e4ab673735d12d426 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 6 Nov 2023 18:51:18 +0900 Subject: [PATCH 093/146] Fix h-label loss normalization issue w/ exclusive label group of singe label (#2604) * Fix h-label loss normalization issue w/ exclusive label group with signle label * Fix non-linear version --------- Signed-off-by: Songki Choi --- CHANGELOG.md | 1 + .../mmcls/models/heads/custom_hierarchical_linear_cls_head.py | 2 +- .../models/heads/custom_hierarchical_non_linear_cls_head.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7609c4e18..5cee6309717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - Fix IBLoss enablement with DeiT-Tiny when class incremental training () - Fix mmcls bug not wrapping model in DataParallel on CPUs () +- Fix h-label loss normalization issue w/ exclusive label group of singe label () ## \[v1.4.3\] 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 index 6776756bb61..3e0de200be2 100644 --- 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 @@ -105,7 +105,7 @@ def forward_train(self, cls_score, gt_label, **kwargs): losses["loss"] += multiclass_loss num_effective_heads_in_batch += 1 - if self.hierarchical_info["num_multiclass_heads"] > 1: + if num_effective_heads_in_batch > 0: losses["loss"] /= num_effective_heads_in_batch if self.compute_multilabel_loss: 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 index 5397818fbf3..69ea7bb1476 100644 --- 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 @@ -135,7 +135,7 @@ def forward_train(self, cls_score, gt_label, **kwargs): losses["loss"] += multiclass_loss num_effective_heads_in_batch += 1 - if self.hierarchical_info["num_multiclass_heads"] > 1: + if num_effective_heads_in_batch > 0: losses["loss"] /= num_effective_heads_in_batch if self.compute_multilabel_loss: From 3ec4c9547a4dc548aabc5b7c75cc00a597ade874 Mon Sep 17 00:00:00 2001 From: Wonju Lee Date: Tue, 7 Nov 2023 11:02:57 +0900 Subject: [PATCH 094/146] Boost up Image numpy accessing speed through PIL (#2586) * boost up numpy accessing speed through PIL * update CHANGELOG * resolve precommit error * resolve precommit error * add fallback logic with PIL open * use convert instead of draft --- CHANGELOG.md | 1 + src/otx/api/entities/image.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cee6309717..bffb8601a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Update ModelAPI configuration() - Add Anomaly modelAPI changes () +- Update Image numpy access () ### Bug fixes diff --git a/src/otx/api/entities/image.py b/src/otx/api/entities/image.py index a241a9511bb..e841820c92d 100644 --- a/src/otx/api/entities/image.py +++ b/src/otx/api/entities/image.py @@ -10,6 +10,7 @@ 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 @@ -91,7 +92,12 @@ def numpy(self) -> np.ndarray: np.ndarray: NumPy representation of the image. """ if self.__data is None: - return cv2.cvtColor(cv2.imread(self.__file_path), cv2.COLOR_BGR2RGB) + 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 From ce9e858eecce9e8dbccb05eef4d5ec738a3be900 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 7 Nov 2023 13:07:38 +0900 Subject: [PATCH 095/146] Add missing import pathlib for cls e2e testing (#2610) --- tests/e2e/cli/classification/test_classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py index e48385ca9bf..48dd91ea464 100644 --- a/tests/e2e/cli/classification/test_classification.py +++ b/tests/e2e/cli/classification/test_classification.py @@ -5,6 +5,7 @@ import copy import os +from pathlib import Path import pytest import torch From 794a81463ed0fd8d61972b010acbea6a343294be Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 7 Nov 2023 13:27:40 +0900 Subject: [PATCH 096/146] Fix division by zero in class incremental learning for classification (#2606) * Add empty label to reproduce zero-division error Signed-off-by: Songki Choi * Fix minor typo Signed-off-by: Songki Choi * Fix empty label 4 -> 3 Signed-off-by: Songki Choi * Prevent division by zero Signed-off-by: Songki Choi * Update license Signed-off-by: Songki Choi * Update CHANGELOG.md Signed-off-by: Songki Choi * Fix inefficient sampling Signed-off-by: Songki Choi * Revert indexing Signed-off-by: Songki Choi * Fix minor typo Signed-off-by: Songki Choi --------- Signed-off-by: Songki Choi --- CHANGELOG.md | 1 + .../adapters/mmcls/configurer.py | 2 +- .../adapters/mmcls/datasets/otx_datasets.py | 7 +++++-- .../adapters/mmcls/models/losses/ib_loss.py | 4 ++-- src/otx/algorithms/classification/task.py | 2 +- .../dataloaders/samplers/balanced_sampler.py | 21 ++++++++++--------- .../3/.gitignore | 4 ++++ 7 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 tests/assets/classification_dataset_class_incremental/3/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index bffb8601a1e..0eabab2e816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Fix IBLoss enablement with DeiT-Tiny when class incremental training () - Fix mmcls bug not wrapping model in DataParallel on CPUs () - Fix h-label loss normalization issue w/ exclusive label group of singe label () +- Fix division by zero in class incremental learning for classification () ## \[v1.4.3\] diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index fe4529679a9..e752aebdff4 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -574,7 +574,7 @@ def _configure_dataloader(cfg): CLASS_INC_DATASET = [ "OTXClsDataset", "OTXMultilabelClsDataset", - "MPAHierarchicalClsDataset", + "OTXHierarchicalClsDataset", "ClsTVDataset", ] WEIGHT_MIX_CLASSIFIER = ["SAMImageClassifier"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py index 7522be7ea33..9d62cb48cee 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -1,6 +1,6 @@ """Base Dataset for Classification Task.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -176,7 +176,10 @@ def class_accuracy(self, results, gt_labels): for i in range(self.num_classes): cls_pred = pred_label == i cls_pred = cls_pred[gt_labels == i] - cls_acc = np.sum(cls_pred) / len(cls_pred) + 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 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 index d890c1a55ac..8d585d79b24 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py +++ b/src/otx/algorithms/classification/adapters/mmcls/models/losses/ib_loss.py @@ -1,5 +1,5 @@ """Module for defining IB Loss which alleviate effect of imbalanced dataset.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -48,7 +48,7 @@ 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) + 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) diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index bd0500a3ec1..3b0370e8fc9 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -495,7 +495,7 @@ def _generate_training_metrics(self, learning_curves): # pylint: disable=argume elif self._hierarchical: metric_key = "val/MHAcc" else: - metric_key = "val/accuracy_top-1" + metric_key = "val/accuracy (%)" # Learning curves best_acc = -1 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 index 5b07d07bc90..741429507cc 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py @@ -1,4 +1,7 @@ """Balanced sampler for imbalanced data.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + import math import numpy as np @@ -32,24 +35,22 @@ def __init__(self, dataset, batch_size, efficient_mode=True, num_replicas=1, ran self.dataset = dataset.dataset else: self.dataset = dataset - self.img_indices = self.dataset.img_indices + 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_replicas = num_replicas self.rank = rank self.drop_last = drop_last + self.num_trials = int(self.data_length / self.num_cls) if efficient_mode: # Reduce the # of sampling (sampling data for a single epoch) - self.num_tail = min(len(cls_indices) for cls_indices in self.img_indices.values()) - base = 1 - (1 / self.num_tail) - if base == 0: - raise ValueError("Required more than one sample per class") - self.num_trials = int(math.log(0.001, base)) - if int(self.data_length / self.num_cls) < self.num_trials: - self.num_trials = int(self.data_length / self.num_cls) - else: - self.num_trials = int(self.data_length / self.num_cls) + 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(f"This sampler will select balanced samples {self.num_trials} times") 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 From d240eba0671520b88f6db0b1f13bb5ed0e68a0b4 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 8 Nov 2023 15:19:06 +0900 Subject: [PATCH 097/146] Unify logger usage (#2612) * unify logger * align with pre-commit * unify anomaly logger to otx * change logger file path * align with pre-commit * change logger file path in missing file * configure logger after ConfigManager is initialized * configure logger when ConfigManager instance is initialized * update unit test code * move config_logger to each cli file * align with pre-commit * change part still using mmcv logger --- .../adapters/mmaction/data/det_dataset.py | 4 +- .../action/adapters/mmaction/task.py | 2 +- .../adapters/mmaction/utils/det_eval_utils.py | 9 +- .../action/adapters/openvino/task.py | 4 +- src/otx/algorithms/action/task.py | 2 +- .../action/tools/sample_classification.py | 5 +- .../action/tools/sample_detection.py | 5 +- .../adapters/anomalib/callbacks/inference.py | 4 +- .../anomaly/adapters/anomalib/data/data.py | 4 +- .../adapters/anomalib/logger/logger.py | 85 ------------------- src/otx/algorithms/anomaly/tasks/inference.py | 4 +- src/otx/algorithms/anomaly/tasks/nncf.py | 4 +- src/otx/algorithms/anomaly/tasks/openvino.py | 4 +- src/otx/algorithms/anomaly/tasks/train.py | 4 +- src/otx/algorithms/anomaly/tools/sample.py | 4 +- .../adapters/mmcls/configurer.py | 2 +- .../adapters/mmcls/datasets/otx_datasets.py | 2 +- .../adapters/mmcls/models/classifiers/byol.py | 2 +- .../classifiers/custom_image_classifier.py | 2 +- .../mmcls/models/classifiers/mixin.py | 2 +- .../models/classifiers/semisl_classifier.py | 2 +- .../semisl_multilabel_classifier.py | 2 +- .../adapters/mmcls/nncf/builder.py | 4 +- .../adapters/mmcls/nncf/task.py | 2 +- .../classification/adapters/mmcls/task.py | 2 +- .../adapters/mmcls/utils/builder.py | 6 +- .../adapters/mmcls/utils/config_utils.py | 2 +- .../adapters/mmcls/utils/exporter.py | 2 +- .../classification/adapters/openvino/task.py | 4 +- src/otx/algorithms/classification/task.py | 2 +- .../tools/classification_sample.py | 4 +- .../common/adapters/mmcv/configurer.py | 2 +- .../mmcv/hooks/adaptive_repeat_data_hook.py | 2 +- .../mmcv/hooks/adaptive_training_hook.py | 2 +- .../common/adapters/mmcv/hooks/cancel_hook.py | 2 +- .../mmcv/hooks/composed_dataloaders_hook.py | 2 +- .../mmcv/hooks/custom_model_ema_hook.py | 2 +- .../mmcv/hooks/dual_model_ema_hook.py | 2 +- .../mmcv/hooks/early_stopping_hook.py | 2 +- .../adapters/mmcv/hooks/force_train_hook.py | 2 +- .../common/adapters/mmcv/hooks/logger_hook.py | 2 +- .../mmcv/hooks/loss_dynamics_tracking_hook.py | 2 +- .../adapters/mmcv/hooks/mean_teacher_hook.py | 2 +- .../adapters/mmcv/hooks/model_ema_v2_hook.py | 2 +- .../adapters/mmcv/hooks/no_bias_decay_hook.py | 2 +- .../adapters/mmcv/hooks/progress_hook.py | 2 +- .../adapters/mmcv/hooks/task_adapt_hook.py | 2 +- .../mmcv/hooks/two_crop_transform_hook.py | 2 +- .../mmcv/models/backbones/efficientnet.py | 2 +- .../mmcv/models/backbones/efficientnetv2.py | 2 +- .../mmcv/models/backbones/mobilenetv3.py | 2 +- .../common/adapters/mmcv/nncf/utils.py | 2 +- .../common/adapters/mmcv/tasks/exporter.py | 2 +- .../adapters/mmcv/utils/automatic_bs.py | 2 +- .../adapters/mmcv/utils/config_utils.py | 2 +- .../torch/dataloaders/composed_dataloader.py | 2 +- .../dataloaders/samplers/balanced_sampler.py | 2 +- .../torch/dataloaders/samplers/otx_sampler.py | 2 +- .../adapters/torch/utils/bs_search_algo.py | 2 +- src/otx/algorithms/common/tasks/base_task.py | 2 +- src/otx/algorithms/common/tasks/nncf_task.py | 2 +- src/otx/algorithms/common/utils/data.py | 4 +- src/otx/algorithms/common/utils/task_adapt.py | 2 +- .../detection/adapters/mmdet/configurer.py | 2 +- .../adapters/mmdet/datasets/dataset.py | 2 +- .../mmdet/models/backbones/imgclsmob.py | 7 +- .../mmdet/models/dense_heads/mmov_rpn_head.py | 2 +- .../models/detectors/custom_atss_detector.py | 2 +- .../custom_deformable_detr_detector.py | 2 +- .../models/detectors/custom_dino_detector.py | 2 +- .../models/detectors/custom_lite_dino.py | 2 +- .../detectors/custom_maskrcnn_detector.py | 2 +- .../detectors/custom_single_stage_detector.py | 2 +- .../detectors/custom_two_stage_detector.py | 2 +- .../models/detectors/custom_vfnet_detector.py | 2 +- .../models/detectors/custom_yolox_detector.py | 2 +- .../models/detectors/loss_dynamics_mixin.py | 2 +- .../mmdet/models/detectors/mean_teacher.py | 2 +- .../detection/adapters/mmdet/nncf/builder.py | 4 +- .../detection/adapters/mmdet/nncf/task.py | 2 +- .../detection/adapters/mmdet/task.py | 2 +- .../detection/adapters/mmdet/utils/builder.py | 6 +- .../adapters/mmdet/utils/config_utils.py | 2 +- .../adapters/mmdet/utils/exporter.py | 2 +- .../detection/adapters/openvino/task.py | 2 +- src/otx/algorithms/detection/task.py | 2 +- .../detection/tools/detection_sample.py | 4 +- .../tools/detection_semisl_sample.py | 4 +- .../tools/instance_segmentation_sample.py | 4 +- src/otx/algorithms/detection/utils/data.py | 2 +- .../segmentation/adapters/mmseg/configurer.py | 2 +- .../mmseg/models/backbones/litehrnet.py | 5 +- .../mmseg/models/segmentors/detcon.py | 2 +- .../segmentors/mean_teacher_segmentor.py | 2 +- .../models/segmentors/otx_encoder_decoder.py | 5 +- .../adapters/mmseg/nncf/builder.py | 5 +- .../segmentation/adapters/mmseg/nncf/task.py | 2 +- .../segmentation/adapters/mmseg/task.py | 2 +- .../adapters/mmseg/utils/data_utils.py | 2 +- .../adapters/mmseg/utils/exporter.py | 2 +- .../segmentation/adapters/openvino/task.py | 2 +- src/otx/algorithms/segmentation/task.py | 2 +- .../segmentation/tools/segmentation_sample.py | 4 +- .../config/visual_prompting_config.py | 2 +- .../pytorch_lightning/datasets/dataset.py | 2 +- .../visual_prompting/tasks/inference.py | 2 +- .../visual_prompting/tasks/openvino.py | 2 +- .../visual_prompting/tasks/train.py | 2 +- src/otx/api/entities/dataset_item.py | 4 +- src/otx/api/entities/datasets.py | 3 +- src/otx/api/entities/label_schema.py | 4 +- src/otx/api/entities/metrics.py | 5 +- src/otx/api/usecases/evaluation/accuracy.py | 4 +- src/otx/api/usecases/evaluation/f_measure.py | 4 +- .../reporting/time_monitor_callback.py | 4 +- src/otx/cli/manager/config_manager.py | 6 +- src/otx/cli/tools/build.py | 2 + src/otx/cli/tools/deploy.py | 2 + src/otx/cli/tools/eval.py | 2 + src/otx/cli/tools/explain.py | 3 +- src/otx/cli/tools/export.py | 2 + src/otx/cli/tools/optimize.py | 2 + src/otx/cli/tools/train.py | 2 + src/otx/cli/utils/experiment.py | 5 +- src/otx/cli/utils/hpo.py | 4 +- src/otx/cli/utils/multi_gpu.py | 4 +- .../adapter/segmentation_dataset_adapter.py | 2 +- .../core/data/caching/mem_cache_handler.py | 2 +- src/otx/core/ov/graph/graph.py | 2 +- .../ov/graph/parsers/cls/cls_base_parser.py | 2 +- src/otx/core/ov/graph/utils.py | 2 +- src/otx/core/ov/models/ov_model.py | 2 +- src/otx/core/ov/models/parser_mixin.py | 2 +- src/otx/core/ov/ops/infrastructures.py | 2 +- src/otx/core/ov/ops/utils.py | 4 - src/otx/hpo/hpo_base.py | 4 +- src/otx/hpo/hpo_runner.py | 4 +- src/otx/hpo/hyperband.py | 4 +- src/otx/hpo/resource_manager.py | 4 +- src/otx/hpo/search_space.py | 4 +- .../anomalib/logger => utils}/__init__.py | 8 +- .../{algorithms/common => }/utils/logger.py | 0 .../visual_prompting/tasks/test_inference.py | 2 +- 143 files changed, 205 insertions(+), 277 deletions(-) delete mode 100644 src/otx/algorithms/anomaly/adapters/anomalib/logger/logger.py rename src/otx/{algorithms/anomaly/adapters/anomalib/logger => utils}/__init__.py (82%) rename src/otx/{algorithms/common => }/utils/logger.py (100%) diff --git a/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py b/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py index 7ee87e89f39..038e296e938 100644 --- a/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py +++ b/src/otx/algorithms/action/adapters/mmaction/data/det_dataset.py @@ -26,7 +26,6 @@ from mmaction.datasets.ava_dataset import AVADataset from mmaction.datasets.builder import DATASETS from mmaction.datasets.pipelines import Compose -from mmaction.utils import get_root_logger from mmcv.utils import print_log from otx.algorithms.action.adapters.mmaction.data.pipelines import RawFrameDecode @@ -36,8 +35,9 @@ 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_root_logger() +root_logger = get_logger() # pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals, super-init-not-called diff --git a/src/otx/algorithms/action/adapters/mmaction/task.py b/src/otx/algorithms/action/adapters/mmaction/task.py index 4e07e7ebc3a..66a5e981e35 100644 --- a/src/otx/algorithms/action/adapters/mmaction/task.py +++ b/src/otx/algorithms/action/adapters/mmaction/task.py @@ -57,7 +57,6 @@ 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.algorithms.common.utils.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.inference_parameters import InferenceParameters from otx.api.entities.model import ModelPrecision @@ -66,6 +65,7 @@ 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() 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 index 1127a6eb958..9653158a609 100644 --- a/src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py +++ b/src/otx/algorithms/action/adapters/mmaction/utils/det_eval_utils.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions # and limitations under the License. -import logging import time from collections import defaultdict @@ -25,6 +24,10 @@ 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): @@ -62,7 +65,7 @@ def det_eval(predictions, result_type, labels, video_infos, exclude_file, verbos start = time.time() for image_key in gt_boxes: if verbose and image_key in excluded_keys: - logging.info("Found excluded timestamp in detections: %s. It will be ignored.", image_key) + 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, @@ -77,7 +80,7 @@ def det_eval(predictions, result_type, labels, video_infos, exclude_file, verbos start = time.time() for image_key in boxes: if verbose and image_key in excluded_keys: - logging.info("Found excluded timestamp in detections: %s. It will be ignored.", image_key) + logger.info("Found excluded timestamp in detections: %s. It will be ignored.", image_key) continue pascal_evaluator.add_single_detected_image_info( image_key, diff --git a/src/otx/algorithms/action/adapters/openvino/task.py b/src/otx/algorithms/action/adapters/openvino/task.py index 85f3480b64c..01c55538ac6 100644 --- a/src/otx/algorithms/action/adapters/openvino/task.py +++ b/src/otx/algorithms/action/adapters/openvino/task.py @@ -16,7 +16,6 @@ import io import json -import logging import os import random import tempfile @@ -72,8 +71,9 @@ IOptimizationTask, OptimizationType, ) +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() # TODO: refactoring to Sphinx style. diff --git a/src/otx/algorithms/action/task.py b/src/otx/algorithms/action/task.py index f5e3e0980ad..5622da6c6aa 100644 --- a/src/otx/algorithms/action/task.py +++ b/src/otx/algorithms/action/task.py @@ -29,7 +29,6 @@ InferenceProgressCallback, TrainingProgressCallback, ) -from otx.algorithms.common.utils.logger import get_logger 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 @@ -66,6 +65,7 @@ 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() diff --git a/src/otx/algorithms/action/tools/sample_classification.py b/src/otx/algorithms/action/tools/sample_classification.py index 931e923838a..36e86cefcd3 100644 --- a/src/otx/algorithms/action/tools/sample_classification.py +++ b/src/otx/algorithms/action/tools/sample_classification.py @@ -22,8 +22,6 @@ os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" -from mmcv.utils import get_logger - from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create from otx.api.entities.inference_parameters import InferenceParameters @@ -36,8 +34,9 @@ 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(name="sample") +logger = get_logger() def parse_args(): diff --git a/src/otx/algorithms/action/tools/sample_detection.py b/src/otx/algorithms/action/tools/sample_detection.py index d39e197b214..1caf67c7ba0 100644 --- a/src/otx/algorithms/action/tools/sample_detection.py +++ b/src/otx/algorithms/action/tools/sample_detection.py @@ -22,8 +22,6 @@ os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1" -from mmcv.utils import get_logger - from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create from otx.api.entities.inference_parameters import InferenceParameters @@ -34,8 +32,9 @@ 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(name="sample") +logger = get_logger() def parse_args(): diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py index d4d28741763..fdf5ddbd9bc 100644 --- a/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py +++ b/src/otx/algorithms/anomaly/adapters/anomalib/callbacks/inference.py @@ -23,7 +23,6 @@ from pytorch_lightning.callbacks import Callback from torch import Tensor -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger from otx.api.entities.annotation import Annotation from otx.api.entities.datasets import DatasetEntity from otx.api.entities.label import LabelEntity @@ -32,8 +31,9 @@ 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(__name__) +logger = get_logger() class AnomalyInferenceCallback(Callback): diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py index b037434f40c..5a026e47e2d 100644 --- a/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/data.py @@ -25,7 +25,6 @@ from torch import Tensor from torch.utils.data import DataLoader, Dataset -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.model_template import TaskType from otx.api.entities.shapes.polygon import Polygon @@ -36,8 +35,9 @@ 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(__name__) +logger = get_logger() class OTXAnomalyDataset(Dataset): diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/logger/logger.py b/src/otx/algorithms/anomaly/adapters/anomalib/logger/logger.py deleted file mode 100644 index 0504c37767f..00000000000 --- a/src/otx/algorithms/anomaly/adapters/anomalib/logger/logger.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Logging.""" - -# Copyright (c) OpenMMLab. All rights reserved. -import logging -from logging import FileHandler, Handler, Logger, StreamHandler -from typing import Dict, List, Optional - -import torch.distributed as dist - -logger_initialized: Dict[str, bool] = {} - - -def get_logger( - name: str, log_file: Optional[str] = None, log_level: int = logging.INFO, file_mode: str = "w" -) -> Logger: - """Get logger. - - If the logger has not been initialized, this method will initialize the - logger by adding one or two handlers, otherwise the initialized logger will - be directly returned. During initialization, a StreamHandler will always be - added. If `log_file` is specified and the process rank is 0, a FileHandler - will also be added. - - Args: - name (str): Logger name. - log_file (str | None): The log filename. If specified, a FileHandler - will be added to the logger. - log_level (int): The logger level. Notx that only the process of - rank 0 is affected, and other processes will set the level to - "Error" thus be silent most of the time. - file_mode (str): The file mode used in opening log file. - Defaults to 'w'. - - Returns: - logging.Logger: The expected logger. - """ - logger = logging.getLogger(name) - if name in logger_initialized: - return logger - # handle hierarchical names - # e.g., logger "a" is initialized, then logger "a.b" will skip the - # initialization since it is a child of "a". - for logger_name in logger_initialized: - if name.startswith(logger_name): - return logger - - # handle duplicate logs to the console - # Starting in 1.8.0, PyTorch DDP attaches a StreamHandler (NOTSET) - # to the root logger. As logger.propagate is True by default, this root - # level handler causes logging messages from rank>0 processes to - # unexpectedly show up on the console, creating much unwanted clutter. - # To fix this issue, we set the root logger's StreamHandler, if any, to log - # at the ERROR level. - for handler in logger.root.handlers: # type: ignore - if isinstance(handler, StreamHandler): - handler.setLevel(logging.ERROR) - - handlers: List[Handler] = [StreamHandler()] - - if dist.is_available() and dist.is_initialized(): - rank = dist.get_rank() - else: - rank = 0 - - # only rank 0 will add a FileHandler - if rank == 0 and log_file is not None: - # Here, the default behaviour of the official logger is 'a'. Thus, we - # provide an interface to change the file mode to the default - # behaviour. - handlers.append(FileHandler(log_file, file_mode)) - - formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s") - for handler in handlers: - handler.setFormatter(formatter) - handler.setLevel(log_level) - logger.addHandler(handler) - - if rank == 0: - logger.setLevel(log_level) - else: - logger.setLevel(logging.ERROR) - - logger_initialized[name] = True - - return logger diff --git a/src/otx/algorithms/anomaly/tasks/inference.py b/src/otx/algorithms/anomaly/tasks/inference.py index 50c5a4b81f7..7f09c69b248 100644 --- a/src/otx/algorithms/anomaly/tasks/inference.py +++ b/src/otx/algorithms/anomaly/tasks/inference.py @@ -43,7 +43,6 @@ ) from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -69,8 +68,9 @@ 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(__name__) +logger = get_logger() # pylint: disable=too-many-instance-attributes diff --git a/src/otx/algorithms/anomaly/tasks/nncf.py b/src/otx/algorithms/anomaly/tasks/nncf.py index d8152d042ec..006ca11afad 100644 --- a/src/otx/algorithms/anomaly/tasks/nncf.py +++ b/src/otx/algorithms/anomaly/tasks/nncf.py @@ -41,7 +41,6 @@ from otx.algorithms.anomaly.adapters.anomalib.callbacks import ProgressCallback from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.model import ( ModelEntity, @@ -56,10 +55,11 @@ IOptimizationTask, OptimizationType, ) +from otx.utils.logger import get_logger from .inference import InferenceTask -logger = get_logger(__name__) +logger = get_logger() class NNCFTask(InferenceTask, IOptimizationTask): diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index 5a5fffb3f02..3488cb9c18e 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -32,7 +32,6 @@ from openvino.model_api.models import AnomalyDetection, AnomalyResult from otx.algorithms.anomaly.adapters.anomalib.config import get_anomalib_config -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -71,8 +70,9 @@ ) 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(__name__) +logger = get_logger() class OTXNNCFAnomalyDataloader: diff --git a/src/otx/algorithms/anomaly/tasks/train.py b/src/otx/algorithms/anomaly/tasks/train.py index a1f4759ab1a..9e2f57f249f 100644 --- a/src/otx/algorithms/anomaly/tasks/train.py +++ b/src/otx/algorithms/anomaly/tasks/train.py @@ -29,15 +29,15 @@ from otx.algorithms.anomaly.adapters.anomalib.callbacks import ProgressCallback from otx.algorithms.anomaly.adapters.anomalib.data import OTXAnomalyDataModule -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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(__name__) +logger = get_logger() class TrainingTask(InferenceTask, ITrainingTask): diff --git a/src/otx/algorithms/anomaly/tools/sample.py b/src/otx/algorithms/anomaly/tools/sample.py index 1cf7057b2ef..a8433defb51 100644 --- a/src/otx/algorithms/anomaly/tools/sample.py +++ b/src/otx/algorithms/anomaly/tools/sample.py @@ -29,7 +29,6 @@ AnomalyDetectionDataset, AnomalySegmentationDataset, ) -from otx.algorithms.anomaly.adapters.anomalib.logger import get_logger 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 @@ -46,8 +45,9 @@ 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(__name__) +logger = get_logger() # pylint: disable=too-many-instance-attributes diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index 397026d5760..873a6efdbe8 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -22,7 +22,7 @@ recursively_update_cfg, update_or_add_custom_hook, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py index 6c0ec0ab2a9..b430457f828 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -18,10 +18,10 @@ from torch.utils.data import Dataset from otx.algorithms.common.utils import get_cls_img_indices, get_old_new_img_indices -from otx.algorithms.common.utils.logger import get_logger 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() diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py index 9f8a2cb0dba..82e994a7a96 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/byol.py @@ -18,7 +18,7 @@ from mmcls.models.builder import CLASSIFIERS, build_backbone, build_head, build_neck from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 65e31d6bc53..d2e23dadda5 100644 --- 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 @@ -12,8 +12,8 @@ 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.logger import get_logger from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger from .mixin import ClsLossDynamicsTrackingMixin, SAMClassifierMixin diff --git a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py index 1674a2d182b..99d899356e1 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/mixin.py @@ -10,12 +10,12 @@ import numpy as np import pandas as pd -from otx.algorithms.common.utils.logger import get_logger 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() 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 index 33850bff708..a4eddee15b8 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py +++ b/src/otx/algorithms/classification/adapters/mmcls/models/classifiers/semisl_classifier.py @@ -6,7 +6,7 @@ import torch from mmcls.models.builder import CLASSIFIERS -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from .custom_image_classifier import CustomImageClassifier 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 index 1a773054384..aac9ed9688b 100644 --- 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 @@ -5,7 +5,7 @@ from mmcls.models.builder import CLASSIFIERS -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from .custom_image_classifier import CustomImageClassifier diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py index b8db070fb85..0b4f7b7fe28 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/builder.py @@ -7,7 +7,6 @@ from typing import Optional, Union import torch -from mmcls.utils import get_root_logger from mmcv.parallel import DataContainer from mmcv.runner import CheckpointLoader from mmcv.utils import Config, ConfigDict @@ -20,8 +19,9 @@ ) 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_root_logger() +logger = get_logger() def build_nncf_classifier( # pylint: disable=too-many-locals,too-many-statements diff --git a/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py index eefb7bf1de4..987a9438b43 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/nncf/task.py @@ -13,7 +13,6 @@ ) from otx.algorithms.classification.adapters.mmcls.task import MMClassificationTask from otx.algorithms.common.tasks.nncf_task import NNCFBaseTask -from otx.algorithms.common.utils.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.metrics import ( CurveMetric, @@ -26,6 +25,7 @@ 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() diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 425ddc6153d..9660895642c 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -53,7 +53,6 @@ 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.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.explain_parameters import ExplainParameters from otx.api.entities.inference_parameters import InferenceParameters @@ -61,6 +60,7 @@ 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, diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py index 36974ce3851..2ee660f270b 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py @@ -8,11 +8,11 @@ import torch from mmcv.runner import load_checkpoint -from mmcv.utils import Config, ConfigDict, get_logger +from mmcv.utils import Config, ConfigDict -from otx.algorithms.common.utils.logger import LEVEL +from otx.utils.logger import LEVEL, get_logger -logger = get_logger("mmcls") +logger = get_logger() def build_classifier( diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py index 4595e1482cc..6ed70d694bc 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/config_utils.py @@ -22,7 +22,7 @@ get_dataset_configs, patch_color_conversion, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py b/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py index f0406d6cdb6..ecc4dbfe35e 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/exporter.py @@ -11,7 +11,7 @@ from otx.algorithms.common.adapters.mmdeploy.utils.utils import ( sync_batchnorm_2_batchnorm, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/classification/adapters/openvino/task.py b/src/otx/algorithms/classification/adapters/openvino/task.py index 91cfb3de5fa..0fd2d822414 100644 --- a/src/otx/algorithms/classification/adapters/openvino/task.py +++ b/src/otx/algorithms/classification/adapters/openvino/task.py @@ -16,7 +16,6 @@ import io import json -import logging import os import tempfile import time @@ -79,8 +78,9 @@ OptimizationType, ) from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() # TODO: refactoring to Sphinx style. diff --git a/src/otx/algorithms/classification/task.py b/src/otx/algorithms/classification/task.py index 9050e736f0c..e52ae4626d0 100644 --- a/src/otx/algorithms/classification/task.py +++ b/src/otx/algorithms/classification/task.py @@ -27,7 +27,6 @@ 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.logger import get_logger 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 @@ -71,6 +70,7 @@ 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 = { diff --git a/src/otx/algorithms/classification/tools/classification_sample.py b/src/otx/algorithms/classification/tools/classification_sample.py index c82636f5fbe..45104ddc19b 100644 --- a/src/otx/algorithms/classification/tools/classification_sample.py +++ b/src/otx/algorithms/classification/tools/classification_sample.py @@ -12,7 +12,6 @@ import numpy as np import torch -from mmcv.utils import get_logger from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create @@ -33,6 +32,7 @@ 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) @@ -42,7 +42,7 @@ torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False -logger = get_logger(name="mmcls") +logger = get_logger() parser = argparse.ArgumentParser(description="Sample showcasing the new API") parser.add_argument("template_file_path", help="path to template file") diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 68e2ea6c35c..f2a786f8fb3 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -29,9 +29,9 @@ from otx.algorithms.common.tasks.base_task import OnHookInitialized from otx.algorithms.common.utils import UncopiableDefaultDict, append_dist_rank_suffix from otx.algorithms.common.utils.data import compute_robust_dataset_statistics -from otx.algorithms.common.utils.logger import get_logger 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() 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 index 310e7316ba6..a04657fd324 100644 --- 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 @@ -8,7 +8,7 @@ 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.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index f9e6fb345ff..747b638fb3a 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/adaptive_training_hook.py @@ -12,7 +12,7 @@ from otx.algorithms.common.adapters.mmcv.hooks.early_stopping_hook import ( EarlyStoppingHook, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py index c2300184f42..9f92c73caa0 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/cancel_hook.py @@ -10,7 +10,7 @@ from mmcv.runner import BaseRunner, EpochBasedRunner from mmcv.runner.hooks import HOOKS, Hook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index ab2e32414c6..a70a01b68b1 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/composed_dataloaders_hook.py @@ -9,7 +9,7 @@ from torch.utils.data import DataLoader from otx.algorithms.common.adapters.torch.dataloaders import ComposedDL -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index b73fe48bae1..b30b080c052 100644 --- 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 @@ -10,7 +10,7 @@ from mmcv.runner import HOOKS, BaseRunner, Hook from mmcv.runner.hooks.ema import EMAHook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index f1c02adc658..9708c8cb05a 100644 --- 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 @@ -9,7 +9,7 @@ from mmcv.parallel import is_module_wrapper from mmcv.runner import HOOKS, Hook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 3d55ab15fbc..4be96b9516c 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/early_stopping_hook.py @@ -10,7 +10,7 @@ from mmcv.runner.hooks import HOOKS, Hook from mmcv.utils import print_log -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 0d47e67841c..b79c7b89a2e 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/force_train_hook.py @@ -16,7 +16,7 @@ from mmcv.runner.hooks import HOOKS, Hook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py index a7ce6a35f48..6889db20b5d 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/logger_hook.py @@ -10,7 +10,7 @@ from mmcv.runner.dist_utils import master_only from mmcv.runner.hooks import HOOKS, Hook, LoggerHook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_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 index 0623f13621d..041445bf22b 100644 --- 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 @@ -12,9 +12,9 @@ from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( update_or_add_custom_hook, ) -from otx.algorithms.common.utils.logger import get_logger 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() 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 index 01be81b842f..e34b36fc7e6 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/mean_teacher_hook.py @@ -8,7 +8,7 @@ from otx.algorithms.common.adapters.mmcv.hooks.dual_model_ema_hook import ( DualModelEMAHook, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index b22f7989776..f9ef09f69df 100644 --- 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 @@ -9,7 +9,7 @@ from mmcv.runner import HOOKS, Hook from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 06e1e06e485..b930029a1ca 100644 --- 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 @@ -6,7 +6,7 @@ from mmcv.runner import HOOKS, Hook from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py b/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py index 1725d855438..03115f32f5b 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/progress_hook.py @@ -19,8 +19,8 @@ from mmcv.runner import BaseRunner from mmcv.runner.hooks import HOOKS, Hook -from otx.algorithms.common.utils.logger import get_logger from otx.api.usecases.reporting.time_monitor_callback import TimeMonitorCallback +from otx.utils.logger import get_logger logger = get_logger() 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 index 193f156fafd..d3e4f9ad68b 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/task_adapt_hook.py @@ -11,7 +11,7 @@ ClsIncrSampler, OTXSampler, ) -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 138ae1bf6c7..d225e3b44e4 100644 --- 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 @@ -4,7 +4,7 @@ from mmcv.runner import BaseRunner from mmcv.runner.hooks import HOOKS, Hook -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py index 13ca6eb1c3c..ce341d13a87 100644 --- a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnet.py @@ -22,7 +22,7 @@ from torch import nn from torch.nn import init -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..builder import BACKBONES diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py index 11fbf4c04f5..631e2fc612f 100644 --- a/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/efficientnetv2.py @@ -17,7 +17,7 @@ from mmcv.runner import load_checkpoint from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..builder import BACKBONES diff --git a/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py b/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py index 8d5630a1a51..cd63173afc1 100644 --- a/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py +++ b/src/otx/algorithms/common/adapters/mmcv/models/backbones/mobilenetv3.py @@ -19,7 +19,7 @@ from mmcv.runner import load_checkpoint from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..builder import BACKBONES diff --git a/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py b/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py index cb82d26cb26..08bd33a97fa 100644 --- a/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/nncf/utils.py @@ -26,7 +26,7 @@ no_nncf_trace, ) from otx.algorithms.common.utils import get_arg_spec -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py b/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py index 969e5ffdf81..995f46d5399 100644 --- a/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py +++ b/src/otx/algorithms/common/adapters/mmcv/tasks/exporter.py @@ -6,7 +6,7 @@ import os import traceback -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py index 3ccd2c81f3f..cfc4b6eb07d 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py @@ -11,7 +11,7 @@ from torch.cuda import is_available as cuda_available from otx.algorithms.common.adapters.torch.utils import BsSearchAlgo -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py index 007c64dfa30..3930fc22bb6 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -25,8 +25,8 @@ from mmcv.utils.path import check_file_exist from otx.algorithms.common.configs.configuration_enums import InputSizePreset -from otx.algorithms.common.utils.logger import get_logger 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 diff --git a/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py b/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py index 5fb30db8ead..463f5771333 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/composed_dataloader.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() 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 index 1ddaaab7884..a1d2bb62ad3 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/balanced_sampler.py @@ -9,7 +9,7 @@ import numpy as np from torch.utils.data import Dataset -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from .otx_sampler import OTXSampler 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 index b01f2aaef66..ada7250c65c 100644 --- a/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py +++ b/src/otx/algorithms/common/adapters/torch/dataloaders/samplers/otx_sampler.py @@ -12,8 +12,8 @@ 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.logger import get_logger from otx.algorithms.common.utils.task_adapt import unwrap_dataset +from otx.utils.logger import get_logger logger = get_logger() 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 index 5b1457c6ede..eaf8c1116e6 100644 --- a/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py +++ b/src/otx/algorithms/common/adapters/torch/utils/bs_search_algo.py @@ -8,7 +8,7 @@ import torch import torch.distributed as dist -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/tasks/base_task.py b/src/otx/algorithms/common/tasks/base_task.py index f390c5fd260..92ec3d8b182 100644 --- a/src/otx/algorithms/common/tasks/base_task.py +++ b/src/otx/algorithms/common/tasks/base_task.py @@ -30,7 +30,6 @@ 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.algorithms.common.utils.logger import get_logger from otx.api.entities.datasets import DatasetEntity from otx.api.entities.explain_parameters import ExplainParameters from otx.api.entities.inference_parameters import InferenceParameters @@ -46,6 +45,7 @@ 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: ".", diff --git a/src/otx/algorithms/common/tasks/nncf_task.py b/src/otx/algorithms/common/tasks/nncf_task.py index a3810e80370..30d11750b04 100644 --- a/src/otx/algorithms/common/tasks/nncf_task.py +++ b/src/otx/algorithms/common/tasks/nncf_task.py @@ -37,7 +37,6 @@ 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.algorithms.common.utils.logger import get_logger from otx.api.configuration import cfg_helper from otx.api.configuration.helper.utils import ids_to_strings from otx.api.entities.datasets import DatasetEntity @@ -58,6 +57,7 @@ IOptimizationTask, OptimizationType, ) +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index 75fa6f2a201..0297700045c 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -5,7 +5,6 @@ # pylint: disable=invalid-name import glob -import logging import os import random from typing import Any, Dict, List, Optional, Union @@ -19,8 +18,9 @@ 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 = logging.getLogger(__name__) +logger = get_logger() def get_unlabeled_filename(base_root: str, file_list_path: str): diff --git a/src/otx/algorithms/common/utils/task_adapt.py b/src/otx/algorithms/common/utils/task_adapt.py index 1f726a3f1c1..b720de811cb 100644 --- a/src/otx/algorithms/common/utils/task_adapt.py +++ b/src/otx/algorithms/common/utils/task_adapt.py @@ -5,7 +5,7 @@ import numpy as np -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index a176d64e3a3..e21947ea19c 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -13,12 +13,12 @@ from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, ) -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.adapters.mmdet.utils import ( cluster_anchors, patch_tiling, should_cluster_anchors, ) +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py b/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py index cce2b5dda77..7f1fb146311 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py +++ b/src/otx/algorithms/detection/adapters/mmdet/datasets/dataset.py @@ -276,7 +276,7 @@ def evaluate( # pylint: disable=too-many-branches 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}') + 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) diff --git a/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py index 7bfdd26f55c..e9c93e1fa84 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py +++ b/src/otx/algorithms/detection/adapters/mmdet/models/backbones/imgclsmob.py @@ -8,15 +8,18 @@ from mmcv.cnn import build_activation_layer, build_norm_layer from mmcv.runner import get_dist_info from mmdet.models.builder import BACKBONES -from mmdet.utils.logger import get_root_logger 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.""" @@ -95,8 +98,6 @@ def init_weights(self, pretrained=True): def generate_backbones(): """Generate backbones of pytorchcv funtion.""" - logger = get_root_logger() - for model_name, model_getter in _models.items(): def closure(model_name, model_getter): 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 index 853840d7f6e..7befbb7ca7a 100644 --- 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 @@ -11,8 +11,8 @@ from mmdet.models.builder import HEADS from mmdet.models.dense_heads.rpn_head import RPNHead -from otx.algorithms.common.utils.logger import get_logger from otx.core.ov.models.mmov_model import MMOVModel +from otx.utils.logger import get_logger logger = get_logger() 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 index 789aa146e32..63d94f894d9 100644 --- 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 @@ -13,12 +13,12 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger 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 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 index f5a5ee33c7b..4a9a18c312d 100644 --- 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 @@ -14,8 +14,8 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.task_adapt import map_class_names +from otx.utils.logger import get_logger logger = get_logger() 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 index 2f210922e6d..3d739fcd292 100644 --- 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 @@ -11,8 +11,8 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.adapters.mmdet.models.detectors import CustomDeformableDETR +from otx.utils.logger import get_logger logger = get_logger() 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 index b2f973187bb..be71f1b8b7c 100644 --- 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 @@ -6,8 +6,8 @@ from mmdet.models.builder import DETECTORS -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.adapters.mmdet.models.detectors import CustomDINO +from otx.utils.logger import get_logger logger = get_logger() 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 index afd8b0bcf0e..3d80c497a4c 100644 --- 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 @@ -13,8 +13,8 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger 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 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 index f690c38d86b..a5eedcbcfb3 100644 --- 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 @@ -13,13 +13,13 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger 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 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 index 552891c8978..1e9f663fffc 100644 --- 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 @@ -8,8 +8,8 @@ from mmdet.models.builder import DETECTORS from mmdet.models.detectors.two_stage import TwoStageDetector -from otx.algorithms.common.utils.logger import get_logger 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 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 index d1120387077..14e746f76f7 100644 --- 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 @@ -8,10 +8,10 @@ from mmdet.models.builder import DETECTORS from mmdet.models.detectors.vfnet import VFNet -from otx.algorithms.common.utils.logger import get_logger 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 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 index d86e700ec8b..61c5ed563bc 100644 --- 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 @@ -13,7 +13,6 @@ FeatureVectorHook, ) from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled -from otx.algorithms.common.utils.logger import get_logger 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, @@ -22,6 +21,7 @@ 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 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 index 5463409cfe4..8f3fe5ddfd0 100644 --- 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 @@ -10,7 +10,6 @@ import numpy as np import pandas as pd -from otx.algorithms.common.utils.logger import get_logger 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 @@ -19,6 +18,7 @@ LossDynamicsTracker, LossDynamicsTrackingMixin, ) +from otx.utils.logger import get_logger logger = get_logger() 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 index 05c112a3db4..ac8f99e5240 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py +++ b/src/otx/algorithms/detection/adapters/mmdet/models/detectors/mean_teacher.py @@ -15,7 +15,7 @@ from mmdet.models import DETECTORS, build_detector from mmdet.models.detectors import BaseDetector -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from .sam_detector_mixin import SAMDetectorMixin diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py index d520580c46a..9befb61cbea 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/builder.py @@ -10,7 +10,6 @@ from mmcv.parallel import DataContainer from mmcv.runner import CheckpointLoader from mmcv.utils import Config, ConfigDict -from mmdet.utils import get_root_logger from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY from otx.algorithms.common.adapters.mmcv.utils import ( @@ -21,8 +20,9 @@ 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_root_logger() +logger = get_logger() def build_nncf_detector( # pylint: disable=too-many-locals,too-many-statements diff --git a/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py index 01b912d3791..265e0d5ac36 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py +++ b/src/otx/algorithms/detection/adapters/mmdet/nncf/task.py @@ -8,7 +8,6 @@ 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.common.utils.logger import get_logger 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 ( @@ -22,6 +21,7 @@ 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() diff --git a/src/otx/algorithms/detection/adapters/mmdet/task.py b/src/otx/algorithms/detection/adapters/mmdet/task.py index cf4d2d03a69..353ef2c9aea 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/task.py +++ b/src/otx/algorithms/detection/adapters/mmdet/task.py @@ -40,7 +40,6 @@ 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.logger import get_logger from otx.algorithms.detection.adapters.mmdet.configurer import ( DetectionConfigurer, IncrDetectionConfigurer, @@ -75,6 +74,7 @@ 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() diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py index 61a50ab80db..91ee5e26861 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py @@ -8,11 +8,11 @@ import torch from mmcv.runner import load_checkpoint -from mmcv.utils import Config, ConfigDict, get_logger +from mmcv.utils import Config, ConfigDict -from otx.algorithms.common.utils.logger import LEVEL +from otx.utils.logger import LEVEL, get_logger -logger = get_logger("mmdet") +logger = get_logger() def build_detector( diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py b/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py index 6935eb65e46..3c2431adae3 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py @@ -8,7 +8,6 @@ InputSizeManager, get_configs_by_pairs, ) -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.configs.base import DetectionConfig from otx.algorithms.detection.utils.data import ( adaptive_tile_params, @@ -18,6 +17,7 @@ ) 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 diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py b/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py index 2d82c980fbf..a2c4b71c68c 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/exporter.py @@ -9,8 +9,8 @@ from otx.algorithms.common.adapters.mmdeploy.utils.utils import ( sync_batchnorm_2_batchnorm, ) -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.adapters.mmdet.utils.builder import build_detector +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index bc624027217..af0769dd028 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -36,7 +36,6 @@ from otx.algorithms.common.utils import OTXOpenVinoDataLoader from otx.algorithms.common.utils.ir import check_if_quantized -from otx.algorithms.common.utils.logger import get_logger 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 @@ -86,6 +85,7 @@ OptimizationType, ) from otx.api.utils.dataset_utils import add_saliency_maps_to_dataset_item +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py index a3bd184f690..db50fecbb1e 100644 --- a/src/otx/algorithms/detection/task.py +++ b/src/otx/algorithms/detection/task.py @@ -20,7 +20,6 @@ TrainingProgressCallback, ) from otx.algorithms.common.utils.ir import embed_ir_model_data -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.utils import embed_onnx_model_data 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 @@ -56,6 +55,7 @@ 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() diff --git a/src/otx/algorithms/detection/tools/detection_sample.py b/src/otx/algorithms/detection/tools/detection_sample.py index d55598af7d7..ace9058df0f 100644 --- a/src/otx/algorithms/detection/tools/detection_sample.py +++ b/src/otx/algorithms/detection/tools/detection_sample.py @@ -18,7 +18,6 @@ import sys import numpy as np -from mmcv.utils import get_logger from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create @@ -43,8 +42,9 @@ 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(name="mmdet") +logger = get_logger() def parse_args(): diff --git a/src/otx/algorithms/detection/tools/detection_semisl_sample.py b/src/otx/algorithms/detection/tools/detection_semisl_sample.py index 421c5f6055f..a4ed1c1d652 100644 --- a/src/otx/algorithms/detection/tools/detection_semisl_sample.py +++ b/src/otx/algorithms/detection/tools/detection_semisl_sample.py @@ -19,7 +19,6 @@ from random import randint import numpy as np -from mmcv.utils import get_logger from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create @@ -45,8 +44,9 @@ 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(name="mmdet") +logger = get_logger() def parse_args(): diff --git a/src/otx/algorithms/detection/tools/instance_segmentation_sample.py b/src/otx/algorithms/detection/tools/instance_segmentation_sample.py index 956ea904b7b..354bb40e673 100644 --- a/src/otx/algorithms/detection/tools/instance_segmentation_sample.py +++ b/src/otx/algorithms/detection/tools/instance_segmentation_sample.py @@ -19,7 +19,6 @@ import cv2 import numpy as np -from mmcv.utils import get_logger from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create @@ -44,8 +43,9 @@ 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(name="mmdet") +logger = get_logger() # pylint: disable=too-many-locals, too-many-statements diff --git a/src/otx/algorithms/detection/utils/data.py b/src/otx/algorithms/detection/utils/data.py index 5830e067ea1..3dcd7a741af 100644 --- a/src/otx/algorithms/detection/utils/data.py +++ b/src/otx/algorithms/detection/utils/data.py @@ -10,7 +10,6 @@ from mmdet.datasets.api_wrappers.coco_api import COCO from otx.algorithms.common.utils.data import compute_robust_dataset_statistics -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.detection.configs.base.configuration import DetectionConfig from otx.api.entities.annotation import ( Annotation, @@ -27,6 +26,7 @@ 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() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py index b1c45dfab6c..4a28eb80a5f 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py @@ -19,8 +19,8 @@ remove_custom_hook, ) from otx.algorithms.common.utils import append_dist_rank_suffix -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.segmentation.adapters.mmseg.models.heads import otx_head_factory +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py index 70a27f390c8..7faeaafc518 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py @@ -30,7 +30,6 @@ from mmcv.utils.parrots_wrapper import _BatchNorm from mmseg.models.backbones.resnet import BasicBlock, Bottleneck from mmseg.models.builder import BACKBONES -from mmseg.utils import get_root_logger from torch import nn from otx.algorithms.segmentation.adapters.mmseg.models.utils import ( @@ -39,6 +38,9 @@ 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 @@ -1432,7 +1434,6 @@ def init_weights(self, pretrained=None): """ if isinstance(pretrained, str): - logger = get_root_logger() load_checkpoint(self, pretrained, strict=False, logger=logger) elif pretrained is None: for m in self.modules(): diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py index 41dcc8448d5..46e119d9a9c 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py @@ -24,7 +24,7 @@ from mmseg.ops import resize from torch import nn -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from .otx_encoder_decoder import OTXEncoderDecoder 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 index 1107cc651be..01bafd40ec9 100644 --- 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 @@ -8,8 +8,8 @@ from mmseg.models.segmentors.base import BaseSegmentor from mmseg.ops import resize -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.segmentation.adapters.mmseg.models.heads.proto_head import ProtoNet +from otx.utils.logger import get_logger logger = get_logger() 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 index 8b7766f29a4..14b5cfa6bad 100644 --- 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 @@ -8,10 +8,12 @@ import torch from mmseg.models import SEGMENTORS from mmseg.models.segmentors.encoder_decoder import EncoderDecoder -from mmseg.utils import get_root_logger 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 @@ -57,7 +59,6 @@ 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 = get_root_logger("INFO") logger.info(f"----------------- OTXEncoderDecoder.load_state_dict_pre_hook() called w/ prefix: {prefix}") # Dst to src mapping index diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py index 3f516e6d7a9..d7c2d3ac78b 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py @@ -12,8 +12,6 @@ from mmcv.utils import Config, ConfigDict # pylint: disable=no-name-in-module -from mmseg.utils import get_root_logger # type: ignore - from otx.algorithms.common.adapters.mmcv.nncf.runners import NNCF_META_KEY from otx.algorithms.common.adapters.mmcv.utils import ( get_configs_by_pairs, @@ -22,8 +20,9 @@ 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_root_logger() +logger = get_logger() def build_nncf_segmentor( # noqa: C901 # pylint: disable=too-many-locals,too-many-statements diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py index c4979e607d7..bab4bd206fa 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/nncf/task.py @@ -8,7 +8,6 @@ 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.common.utils.logger import get_logger 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 @@ -27,6 +26,7 @@ ) 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() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/task.py index 0c671a06820..0362932aa76 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/task.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/task.py @@ -39,7 +39,6 @@ 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.logger import get_logger from otx.algorithms.segmentation.adapters.mmseg.configurer import ( IncrSegmentationConfigurer, SegmentationConfigurer, @@ -62,6 +61,7 @@ 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() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py index 3964a00af36..bdfe9ce32cd 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py @@ -25,7 +25,6 @@ from mmseg.datasets.custom import CustomDataset from skimage.segmentation import felzenszwalb -from otx.algorithms.common.utils.logger import get_logger from otx.api.entities.annotation import ( Annotation, AnnotationSceneEntity, @@ -38,6 +37,7 @@ 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() diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py b/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py index 0a13e5f01f3..b7317c8b090 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/utils/exporter.py @@ -8,8 +8,8 @@ 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.common.utils.logger import get_logger from otx.algorithms.segmentation.adapters.mmseg.utils.builder import build_segmentor +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/segmentation/adapters/openvino/task.py b/src/otx/algorithms/segmentation/adapters/openvino/task.py index 51b27286910..df33e99101c 100644 --- a/src/otx/algorithms/segmentation/adapters/openvino/task.py +++ b/src/otx/algorithms/segmentation/adapters/openvino/task.py @@ -34,7 +34,6 @@ 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.common.utils.logger import get_logger 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 @@ -72,6 +71,7 @@ IOptimizationTask, OptimizationType, ) +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/segmentation/task.py b/src/otx/algorithms/segmentation/task.py index cca2befe81d..dac8fe574cb 100644 --- a/src/otx/algorithms/segmentation/task.py +++ b/src/otx/algorithms/segmentation/task.py @@ -20,7 +20,6 @@ TrainingProgressCallback, ) from otx.algorithms.common.utils.ir import embed_ir_model_data -from otx.algorithms.common.utils.logger import get_logger 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 @@ -61,6 +60,7 @@ ) 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 = { diff --git a/src/otx/algorithms/segmentation/tools/segmentation_sample.py b/src/otx/algorithms/segmentation/tools/segmentation_sample.py index ce1ed269286..0f5bb730939 100644 --- a/src/otx/algorithms/segmentation/tools/segmentation_sample.py +++ b/src/otx/algorithms/segmentation/tools/segmentation_sample.py @@ -19,7 +19,6 @@ import cv2 import numpy as np -from mmcv.utils import get_logger from otx.algorithms.common.utils import get_task_class from otx.api.configuration.helper import create @@ -44,8 +43,9 @@ 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(name="mmseg") +logger = get_logger() def parse_args(): 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 index e3382f25526..6a212c9cbb8 100644 --- 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 @@ -20,8 +20,8 @@ from omegaconf import DictConfig, ListConfig, OmegaConf -from otx.algorithms.common.utils.logger import get_logger from otx.api.configuration.configurable_parameters import ConfigurableParameters +from otx.utils.logger import get_logger logger = get_logger() 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 index 6b527cf6d22..9f79eeda019 100644 --- a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py @@ -24,7 +24,6 @@ from torch.utils.data import DataLoader, Dataset from torchvision import transforms -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ( MultipleInputsCompose, Pad, @@ -39,6 +38,7 @@ 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() diff --git a/src/otx/algorithms/visual_prompting/tasks/inference.py b/src/otx/algorithms/visual_prompting/tasks/inference.py index 6ff23ee9050..9358bd93242 100644 --- a/src/otx/algorithms/visual_prompting/tasks/inference.py +++ b/src/otx/algorithms/visual_prompting/tasks/inference.py @@ -32,7 +32,6 @@ from pytorch_lightning.callbacks import TQDMProgressBar from otx.algorithms.common.utils import set_random_seed -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.visual_prompting.adapters.pytorch_lightning.callbacks import ( InferenceCallback, ) @@ -62,6 +61,7 @@ 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() diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index de244698837..7e133438bc1 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -35,7 +35,6 @@ 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.common.utils.logger import get_logger from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( OTXVisualPromptingDataset, @@ -76,6 +75,7 @@ IOptimizationTask, OptimizationType, ) +from otx.utils.logger import get_logger logger = get_logger() diff --git a/src/otx/algorithms/visual_prompting/tasks/train.py b/src/otx/algorithms/visual_prompting/tasks/train.py index 67b734a767b..344601b7b01 100644 --- a/src/otx/algorithms/visual_prompting/tasks/train.py +++ b/src/otx/algorithms/visual_prompting/tasks/train.py @@ -25,7 +25,6 @@ ) from pytorch_lightning.loggers import CSVLogger -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets import ( OTXVisualPromptingDataModule, ) @@ -34,6 +33,7 @@ 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 diff --git a/src/otx/api/entities/dataset_item.py b/src/otx/api/entities/dataset_item.py index 7975a6a5436..77a0d119fe5 100644 --- a/src/otx/api/entities/dataset_item.py +++ b/src/otx/api/entities/dataset_item.py @@ -9,12 +9,12 @@ import copy from inspect import signature import itertools -import logging 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 @@ -26,7 +26,7 @@ from otx.api.entities.subset import Subset from otx.api.utils.shape_factory import ShapeFactory -logger = logging.getLogger(__name__) +logger = get_logger() T = TypeVar("T", bound="DatasetItemEntity") diff --git a/src/otx/api/entities/datasets.py b/src/otx/api/entities/datasets.py index 1a37458ebe9..3da2ee431a5 100644 --- a/src/otx/api/entities/datasets.py +++ b/src/otx/api/entities/datasets.py @@ -14,13 +14,14 @@ 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 = logging.getLogger(__name__) +logger = get_logger() class DatasetPurpose(Enum): diff --git a/src/otx/api/entities/label_schema.py b/src/otx/api/entities/label_schema.py index 2d69c5469ff..fdb3bf047fc 100644 --- a/src/otx/api/entities/label_schema.py +++ b/src/otx/api/entities/label_schema.py @@ -5,7 +5,7 @@ # import copy -import logging +from otx.utils.logger import get_logger import re from enum import Enum from typing import Dict, List, Optional, Sequence, Union @@ -18,7 +18,7 @@ from otx.api.entities.label import LabelEntity from otx.api.entities.scored_label import ScoredLabel -logger = logging.getLogger(__name__) +logger = get_logger() def natural_sort_label_id(target: Union[ID, LabelEntity, ScoredLabel]) -> List[Union[int, str]]: diff --git a/src/otx/api/entities/metrics.py b/src/otx/api/entities/metrics.py index 2abbd3b1240..f33ba288832 100644 --- a/src/otx/api/entities/metrics.py +++ b/src/otx/api/entities/metrics.py @@ -6,15 +6,17 @@ import abc import datetime -import logging 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. @@ -370,7 +372,6 @@ def normalize(self): if not np.all(self.__matrix_values.sum(axis=1, keepdims=True) > 0): self.__matrix_values = np.nan_to_num(self.__matrix_values) - logger = logging.getLogger(__name__) logger.warning("Replacing NaN in the matrix with zeroes since the sum of one (or more) row(s) was zero.") def __repr__(self): diff --git a/src/otx/api/usecases/evaluation/accuracy.py b/src/otx/api/usecases/evaluation/accuracy.py index 25797bc3fa5..2344a7d3cfa 100644 --- a/src/otx/api/usecases/evaluation/accuracy.py +++ b/src/otx/api/usecases/evaluation/accuracy.py @@ -6,12 +6,12 @@ import copy -import logging 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 @@ -37,7 +37,7 @@ IPerformanceProvider, ) -logger = logging.getLogger(__name__) +logger = get_logger() class Accuracy(IPerformanceProvider): diff --git a/src/otx/api/usecases/evaluation/f_measure.py b/src/otx/api/usecases/evaluation/f_measure.py index b8f07522020..cc845ef7609 100644 --- a/src/otx/api/usecases/evaluation/f_measure.py +++ b/src/otx/api/usecases/evaluation/f_measure.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 # -import logging from typing import Dict, List, Optional, Tuple +from otx.utils.logger import get_logger import numpy as np @@ -32,7 +32,7 @@ ) from otx.api.utils.shape_factory import ShapeFactory -logger = logging.getLogger(__name__) +logger = get_logger() ALL_CLASSES_NAME = "All Classes" diff --git a/src/otx/api/usecases/reporting/time_monitor_callback.py b/src/otx/api/usecases/reporting/time_monitor_callback.py index 9965bc4a217..8d032a6acf1 100644 --- a/src/otx/api/usecases/reporting/time_monitor_callback.py +++ b/src/otx/api/usecases/reporting/time_monitor_callback.py @@ -6,8 +6,8 @@ # pylint: disable=too-many-instance-attributes,too-many-arguments -import logging import math +from otx.utils.logger import get_logger import time from copy import deepcopy from typing import List @@ -20,7 +20,7 @@ ) from otx.api.usecases.reporting.callback import Callback -logger = logging.getLogger(__name__) +logger = get_logger() class TimeMonitorCallback(Callback): diff --git a/src/otx/cli/manager/config_manager.py b/src/otx/cli/manager/config_manager.py index 344bc0484fd..1143010cd33 100644 --- a/src/otx/cli/manager/config_manager.py +++ b/src/otx/cli/manager/config_manager.py @@ -3,7 +3,6 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # -import logging import os import shutil from collections import defaultdict @@ -30,6 +29,9 @@ 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 + +logger = get_logger() DEFAULT_MODEL_TEMPLATE_ID = { "CLASSIFICATION": "Custom_Image_Classification_EfficinetNet-B0", @@ -293,7 +295,7 @@ def _check_semisl_requirements(unlabeled_dir): if all_unlabeled_images > 1: return unlabeled_dir - logging.warning( + 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." diff --git a/src/otx/cli/tools/build.py b/src/otx/cli/tools/build.py index 12007c8d0d5..2357534cc85 100644 --- a/src/otx/cli/tools/build.py +++ b/src/otx/cli/tools/build.py @@ -19,6 +19,7 @@ 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", @@ -101,6 +102,7 @@ def main(): 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() diff --git a/src/otx/cli/tools/deploy.py b/src/otx/cli/tools/deploy.py index 4809abff01e..a4321e0cd0f 100644 --- a/src/otx/cli/tools/deploy.py +++ b/src/otx/cli/tools/deploy.py @@ -25,6 +25,7 @@ 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(): @@ -50,6 +51,7 @@ def main(): # 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() diff --git a/src/otx/cli/tools/eval.py b/src/otx/cli/tools/eval.py index 00a533510d0..5cf59156e48 100644 --- a/src/otx/cli/tools/eval.py +++ b/src/otx/cli/tools/eval.py @@ -34,6 +34,7 @@ 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 @@ -94,6 +95,7 @@ def main(): 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() diff --git a/src/otx/cli/tools/explain.py b/src/otx/cli/tools/explain.py index ec7735acbdf..b6e70cb9dc7 100644 --- a/src/otx/cli/tools/explain.py +++ b/src/otx/cli/tools/explain.py @@ -18,7 +18,6 @@ # Update environment variables for CLI use import otx.cli # noqa: F401 -from otx.algorithms.common.utils.logger import get_logger from otx.api.entities.explain_parameters import ExplainParameters from otx.api.entities.task_environment import TaskEnvironment from otx.cli.manager import ConfigManager @@ -36,6 +35,7 @@ get_override_param, get_parser_and_hprams_data, ) +from otx.utils.logger import config_logger, get_logger logger = get_logger() @@ -135,6 +135,7 @@ def main(): 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() diff --git a/src/otx/cli/tools/export.py b/src/otx/cli/tools/export.py index 8ec6c0f92b5..019c855c9ba 100644 --- a/src/otx/cli/tools/export.py +++ b/src/otx/cli/tools/export.py @@ -27,6 +27,7 @@ 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(): @@ -73,6 +74,7 @@ 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() diff --git a/src/otx/cli/tools/optimize.py b/src/otx/cli/tools/optimize.py index 312abe78e3c..c94c723243d 100644 --- a/src/otx/cli/tools/optimize.py +++ b/src/otx/cli/tools/optimize.py @@ -36,6 +36,7 @@ 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 @@ -87,6 +88,7 @@ def main(): 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() diff --git a/src/otx/cli/tools/train.py b/src/otx/cli/tools/train.py index cb7c3fb4830..dfb3fe1c4a1 100644 --- a/src/otx/cli/tools/train.py +++ b/src/otx/cli/tools/train.py @@ -44,6 +44,7 @@ ) 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(): @@ -199,6 +200,7 @@ def train(exit_stack: Optional[ExitStack] = None): # pylint: disable=too-many-b 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() diff --git a/src/otx/cli/utils/experiment.py b/src/otx/cli/utils/experiment.py index cc4af013f80..591d69fdee5 100644 --- a/src/otx/cli/utils/experiment.py +++ b/src/otx/cli/utils/experiment.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # -import logging import multiprocessing as mp import os import time @@ -15,12 +14,14 @@ import psutil import yaml +from otx.utils.logger import get_logger + try: import pynvml except ImportError: pynvml = None -logger = logging.getLogger(__name__) +logger = get_logger() GIB = 1024**3 AVAILABLE_RESOURCE_TYPE = ["cpu", "gpu"] diff --git a/src/otx/cli/utils/hpo.py b/src/otx/cli/utils/hpo.py index dbdcfb23d4a..5a0a82d50af 100644 --- a/src/otx/cli/utils/hpo.py +++ b/src/otx/cli/utils/hpo.py @@ -5,7 +5,6 @@ # import json -import logging import os import re import shutil @@ -31,8 +30,9 @@ 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.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() def _check_hpo_enabled_task(task_type): diff --git a/src/otx/cli/utils/multi_gpu.py b/src/otx/cli/utils/multi_gpu.py index f1ffe7774bc..834a9aa6087 100644 --- a/src/otx/cli/utils/multi_gpu.py +++ b/src/otx/cli/utils/multi_gpu.py @@ -15,7 +15,6 @@ # and limitations under the License. import datetime -import logging import os import signal import socket @@ -31,8 +30,9 @@ import torch.multiprocessing as mp from otx.api.configuration import ConfigurableParameters +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() def _get_free_port(): diff --git a/src/otx/core/data/adapter/segmentation_dataset_adapter.py b/src/otx/core/data/adapter/segmentation_dataset_adapter.py index 4f6ff7c29ba..2ccca25e24c 100644 --- a/src/otx/core/data/adapter/segmentation_dataset_adapter.py +++ b/src/otx/core/data/adapter/segmentation_dataset_adapter.py @@ -23,7 +23,6 @@ from datumaro.util.meta_file_util import parse_meta_file from skimage.segmentation import felzenszwalb -from otx.algorithms.common.utils.logger import get_logger from otx.api.entities.annotation import Annotation from otx.api.entities.dataset_item import DatasetItemEntity from otx.api.entities.datasets import DatasetEntity @@ -31,6 +30,7 @@ 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 diff --git a/src/otx/core/data/caching/mem_cache_handler.py b/src/otx/core/data/caching/mem_cache_handler.py index b9925e88772..02138b7b481 100644 --- a/src/otx/core/data/caching/mem_cache_handler.py +++ b/src/otx/core/data/caching/mem_cache_handler.py @@ -12,7 +12,7 @@ import psutil from multiprocess.synchronize import Lock -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger logger = get_logger() GIB = 1024**3 diff --git a/src/otx/core/ov/graph/graph.py b/src/otx/core/ov/graph/graph.py index e6c197fdaec..e51e1a431ad 100644 --- a/src/otx/core/ov/graph/graph.py +++ b/src/otx/core/ov/graph/graph.py @@ -15,7 +15,7 @@ import networkx as nx from openvino.runtime import Model -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..ops.op import Operation from ..ops.utils import convert_op_to_torch 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 index 2e9c37c3266..0f0ceadb2ae 100644 --- a/src/otx/core/ov/graph/parsers/cls/cls_base_parser.py +++ b/src/otx/core/ov/graph/parsers/cls/cls_base_parser.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..builder import PARSERS from ..parser import parameter_parser diff --git a/src/otx/core/ov/graph/utils.py b/src/otx/core/ov/graph/utils.py index c297fd00e6d..c6bf2b69675 100644 --- a/src/otx/core/ov/graph/utils.py +++ b/src/otx/core/ov/graph/utils.py @@ -7,11 +7,11 @@ import torch -from otx.algorithms.common.utils.logger import get_logger 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() diff --git a/src/otx/core/ov/models/ov_model.py b/src/otx/core/ov/models/ov_model.py index e71fea1a609..aeca7db0397 100644 --- a/src/otx/core/ov/models/ov_model.py +++ b/src/otx/core/ov/models/ov_model.py @@ -16,7 +16,7 @@ import torch from torch.nn import init -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..graph import Graph from ..graph.utils import ( diff --git a/src/otx/core/ov/models/parser_mixin.py b/src/otx/core/ov/models/parser_mixin.py index 2ec165fc484..bb49e0ae36f 100644 --- a/src/otx/core/ov/models/parser_mixin.py +++ b/src/otx/core/ov/models/parser_mixin.py @@ -9,7 +9,7 @@ import openvino.runtime as ov -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..graph.parsers.builder import PARSERS from .ov_model import OVModel diff --git a/src/otx/core/ov/ops/infrastructures.py b/src/otx/core/ov/ops/infrastructures.py index 2572ac7af01..44b39b9d120 100644 --- a/src/otx/core/ov/ops/infrastructures.py +++ b/src/otx/core/ov/ops/infrastructures.py @@ -10,7 +10,7 @@ import numpy as np import torch -from otx.algorithms.common.utils.logger import get_logger +from otx.utils.logger import get_logger from ..utils import get_op_name # type: ignore[attr-defined] from .builder import OPS diff --git a/src/otx/core/ov/ops/utils.py b/src/otx/core/ov/ops/utils.py index 25222a18830..b74d37bbb02 100644 --- a/src/otx/core/ov/ops/utils.py +++ b/src/otx/core/ov/ops/utils.py @@ -28,10 +28,6 @@ def convert_op_to_torch(op_node: Node): try: torch_module = OPS.get_by_type_version(op_type, op_version).from_ov(op_node) except Exception as e: - # logger.error(e) - # logger.error(op_type) - # logger.error(op_version) - # logger.error(op_node.get_attributes()) raise e return torch_module diff --git a/src/otx/hpo/hpo_base.py b/src/otx/hpo/hpo_base.py index dc03f5cb501..17ebc9da4be 100644 --- a/src/otx/hpo/hpo_base.py +++ b/src/otx/hpo/hpo_base.py @@ -15,7 +15,6 @@ # and limitations under the License. import json -import logging import tempfile from abc import ABC, abstractmethod from enum import IntEnum @@ -23,8 +22,9 @@ from otx.hpo.search_space import SearchSpace from otx.hpo.utils import check_mode_input, check_positive +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() class HpoBase(ABC): diff --git a/src/otx/hpo/hpo_runner.py b/src/otx/hpo/hpo_runner.py index 27565329625..27ecc0a84a9 100644 --- a/src/otx/hpo/hpo_runner.py +++ b/src/otx/hpo/hpo_runner.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions # and limitations under the License. -import logging import multiprocessing import os import queue @@ -28,8 +27,9 @@ from otx.hpo.hpo_base import HpoBase, Trial, TrialStatus from otx.hpo.resource_manager import get_resource_manager +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() @dataclass diff --git a/src/otx/hpo/hyperband.py b/src/otx/hpo/hyperband.py index eea8bac0f57..49e5b5003ed 100644 --- a/src/otx/hpo/hyperband.py +++ b/src/otx/hpo/hyperband.py @@ -15,7 +15,6 @@ # and limitations under the License. import json -import logging import math import os from os import path as osp @@ -30,8 +29,9 @@ check_positive, left_vlaue_is_better, ) +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() def _check_reduction_factor_value(reduction_factor: int): diff --git a/src/otx/hpo/resource_manager.py b/src/otx/hpo/resource_manager.py index c514577ab9d..a6df2d9930a 100644 --- a/src/otx/hpo/resource_manager.py +++ b/src/otx/hpo/resource_manager.py @@ -14,7 +14,6 @@ # 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 Any, Dict, List, Literal, Optional @@ -22,8 +21,9 @@ import torch from otx.hpo.utils import check_positive +from otx.utils.logger import get_logger -logger = logging.getLogger(__name__) +logger = get_logger() class BaseResourceManager(ABC): diff --git a/src/otx/hpo/search_space.py b/src/otx/hpo/search_space.py index 81698b56578..acdcf657f0b 100644 --- a/src/otx/hpo/search_space.py +++ b/src/otx/hpo/search_space.py @@ -15,14 +15,14 @@ # and limitations under the License. -import logging import math import typing 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(__name__) +logger = get_logger() AVAILABLE_SEARCH_SPACE_TYPE = ["uniform", "quniform", "loguniform", "qloguniform", "choice"] diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/logger/__init__.py b/src/otx/utils/__init__.py similarity index 82% rename from src/otx/algorithms/anomaly/adapters/anomalib/logger/__init__.py rename to src/otx/utils/__init__.py index c39b63f72c3..1a7b41db1f0 100644 --- a/src/otx/algorithms/anomaly/adapters/anomalib/logger/__init__.py +++ b/src/otx/utils/__init__.py @@ -1,6 +1,6 @@ -"""Logging.""" +"""Collection of tools to run common OTX algorithms.""" -# Copyright (C) 2021 Intel Corporation +# 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. @@ -13,7 +13,3 @@ # WITHOUT 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 .logger import get_logger - -__all__ = ["get_logger"] diff --git a/src/otx/algorithms/common/utils/logger.py b/src/otx/utils/logger.py similarity index 100% rename from src/otx/algorithms/common/utils/logger.py rename to src/otx/utils/logger.py diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_inference.py b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py index b89d59f86a8..12bfb36817c 100644 --- a/tests/unit/algorithms/visual_prompting/tasks/test_inference.py +++ b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py @@ -16,7 +16,7 @@ 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.utils.logger import get_logger +from otx.utils.logger import get_logger from tests.unit.algorithms.visual_prompting.test_helpers import ( generate_visual_prompting_dataset, init_environment, From 8559defdfadced7da661ed7442be2e91529ab962 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Wed, 8 Nov 2023 11:07:49 +0200 Subject: [PATCH 098/146] Fix XAI algorithm for Detection (#2609) * Impove saliency maps algorithm for Detection * Remove extra changes * Update unit tests * Changes for 1 class * Fix pre-commit * Update CHANGELOG --- CHANGELOG.md | 1 + .../hooks/det_class_probability_map_hook.py | 14 ++++++++------ .../detection/test_xai_detection_validity.py | 16 ++++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eabab2e816..f009610b0e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Fix mmcls bug not wrapping model in DataParallel on CPUs () - Fix h-label loss normalization issue w/ exclusive label group of singe label () - Fix division by zero in class incremental learning for classification () +- Fix saliency maps calculation issue for detection models () ## \[v1.4.3\] 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 index 7931e234091..2847f1c573a 100644 --- 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 @@ -60,12 +60,9 @@ def func( else: cls_scores = self._get_cls_scores_from_feature_map(feature_map) - # 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 and self._num_cls_out_channels > 1: - cls_scores = [torch.softmax(t, dim=1) for t in cls_scores] - - batch_size, _, height, width = cls_scores[-1].size() + 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 = [] @@ -82,6 +79,11 @@ def func( ) 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) diff --git a/tests/unit/algorithms/detection/test_xai_detection_validity.py b/tests/unit/algorithms/detection/test_xai_detection_validity.py index 6f684376064..b24b690e3ba 100644 --- a/tests/unit/algorithms/detection/test_xai_detection_validity.py +++ b/tests/unit/algorithms/detection/test_xai_detection_validity.py @@ -24,19 +24,19 @@ class TestExplainMethods: ref_saliency_shapes = { - "MobileNetV2-ATSS": (2, 4, 4), + "MobileNetV2-ATSS": (2, 13, 13), "SSD": (81, 13, 13), - "YOLOX": (80, 13, 13), + "YOLOX": (80, 26, 26), } ref_saliency_vals_det = { - "MobileNetV2-ATSS": np.array([67, 216, 255, 57], dtype=np.uint8), - "YOLOX": np.array([80, 28, 42, 53, 49, 68, 72, 75, 69, 57, 65, 6, 157], dtype=np.uint8), - "SSD": np.array([119, 72, 118, 35, 39, 30, 31, 31, 36, 28, 44, 23, 61], dtype=np.uint8), + "MobileNetV2-ATSS": np.array([34, 67, 148, 132, 172, 147, 146, 155, 167, 159], dtype=np.uint8), + "YOLOX": np.array([177, 94, 147, 147, 161, 162, 164, 164, 163, 166], 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.10465062, + "MobileNetV2-ATSS": -0.014513552, "YOLOX": 0.04948914, "SSD": 0.6629989, } @@ -80,8 +80,8 @@ def test_saliency_map_det(self, template): assert len(saliency_maps) == 2 assert saliency_maps[0].ndim == 3 assert saliency_maps[0].shape == self.ref_saliency_shapes[template.name] - actual_sal_vals = saliency_maps[0][0][0].astype(np.int8) - ref_sal_vals = self.ref_saliency_vals_det[template.name].astype(np.int8) + 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 From 0d9dff8455a7c210f17bbcf9bf324a2be3ff0d7e Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 9 Nov 2023 10:46:32 +0900 Subject: [PATCH 099/146] Tighten dependency constraint only adapting latest patches (#2607) * tighten dependency constratint only adapting latest patches * adjust scikit-image version w.r.t python version * adjust tensorboard version w.r.t python version * remove version specifier for scikit-image --- requirements/api.txt | 12 ++++++------ requirements/base.txt | 17 +++++++++-------- requirements/classification.txt | 2 +- requirements/detection.txt | 4 ++-- requirements/dev.txt | 14 +++++++------- requirements/docs.txt | 14 +++++++------- requirements/openvino.txt | 2 +- requirements/segmentation.txt | 4 ++-- requirements/visual_prompting.txt | 2 +- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/requirements/api.txt b/requirements/api.txt index c9968e9184f..2148d603747 100644 --- a/requirements/api.txt +++ b/requirements/api.txt @@ -1,12 +1,12 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # API Requirements. # -attrs>=21.2.0 +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 -scikit-learn>=1.0.2 +omegaconf==2.3.* +opencv-python==4.8.1.* +pymongo==4.6.* +scikit-learn==1.3.* Shapely>=1.7.1,<=1.8.0 imagesize==1.4.1 -dill>=0.3.6 +dill==0.3.* diff --git a/requirements/base.txt b/requirements/base.txt index f4682fc6150..aad13eace7b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,12 +1,13 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Base Algo Requirements. # -natsort>=6.0.0 -prettytable -protobuf>=3.20.0 +natsort==8.4.* +prettytable==3.9.* +protobuf==3.20.* pyyaml datumaro==1.5.1rc3 -psutil -scipy>=1.8 -bayesian-optimization>=1.2.0 -tensorboard>=2.11.0 -multiprocess +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.* diff --git a/requirements/classification.txt b/requirements/classification.txt index facc2dca543..bfe35197d9c 100644 --- a/requirements/classification.txt +++ b/requirements/classification.txt @@ -4,5 +4,5 @@ mmcv-full==1.7.0 mmcls==0.25.0 timm==0.6.12 mmdeploy==0.14.0 -pytorchcv +pytorchcv==0.0.67 yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/detection.txt b/requirements/detection.txt index 9118ffec5c1..ddcc33f1347 100644 --- a/requirements/detection.txt +++ b/requirements/detection.txt @@ -2,10 +2,10 @@ # Detection Requirements. mmcv-full==1.7.0 mmdet==2.28.1 -pytorchcv +pytorchcv==0.0.67 mmcls==0.25.0 timm==0.6.12 mmdeploy==0.14.0 mmengine==0.7.4 -scikit-image +scikit-image # specifying different version w.r.t python_version is not effect yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/dev.txt b/requirements/dev.txt index d6648af378e..3966fdcf396 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,12 +1,12 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Development Requirements. # pre-commit==2.20.0 -pylint -pytest -coverage -pytest-timeout -pytest-mock +pylint==3.0.* +pytest==7.4.* +coverage==7.3.* +pytest-timeout==2.2.* +pytest-mock==3.12.* onnx==1.13.0 onnxruntime==1.14.1 -pytest-csv -tox>=4.5.1.1 +pytest-csv==3.0.* +tox==4.11.* diff --git a/requirements/docs.txt b/requirements/docs.txt index 3f5c106da4b..f3cb3ec5100 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,10 +1,10 @@ -furo -myst-parser +furo==2023.3.* +myst-parser==1.0.* sphinx==5.3.0 pydata-sphinx-theme==0.12.0 -sphinx-tabs -sphinx-panels +sphinx-tabs==3.4.* +sphinx-panels==0.4.* sphinx-copybutton==0.5.0 -sphinx-autoapi -sphinxemoji -nbsphinx +sphinx-autoapi==2.1.* +sphinxemoji==0.2.* +nbsphinx==0.9.* diff --git a/requirements/openvino.txt b/requirements/openvino.txt index 4a1494bb460..e91ed69252a 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -5,4 +5,4 @@ onnx==1.13.0 openvino-model-api==0.1.6 openvino==2023.0 openvino-dev==2023.0 -openvino-telemetry>=2022.1.0 +openvino-telemetry==2023.2.* diff --git a/requirements/segmentation.txt b/requirements/segmentation.txt index 17820fdbe16..fdda6fa9cde 100644 --- a/requirements/segmentation.txt +++ b/requirements/segmentation.txt @@ -2,9 +2,9 @@ # Segmentation Requirements. mmcv-full==1.7.0 mmsegmentation==0.30.0 -scikit-image +scikit-image # specifying different version w.r.t python_version is not effect mmdeploy==0.14.0 timm==0.6.12 -pytorchcv +pytorchcv==0.0.67 einops==0.6.1 yapf<0.40.0 # it should be removed after https://github.com/google/yapf/issues/1118 is solved diff --git a/requirements/visual_prompting.txt b/requirements/visual_prompting.txt index ea9c9b50de1..5b5762e841a 100644 --- a/requirements/visual_prompting.txt +++ b/requirements/visual_prompting.txt @@ -1,4 +1,4 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Visual Prompting Requirements. -scikit-image +scikit-image # specifying different version w.r.t python_version is not effect pytorch-lightning>=1.7.0,<1.10.0 From 68e42d0b0d6d432254e441ade87c735788429fb6 Mon Sep 17 00:00:00 2001 From: Ashwin Vaidya Date: Thu, 9 Nov 2023 03:16:01 +0100 Subject: [PATCH 100/146] Add metadata to optimized model (#2618) * bug fix for legacy openvino models * Add metadata to optimized model * Revert formatting changes --------- Co-authored-by: Ashwin Vaidya --- src/otx/algorithms/anomaly/tasks/openvino.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/otx/algorithms/anomaly/tasks/openvino.py b/src/otx/algorithms/anomaly/tasks/openvino.py index f607a4a5d4e..a8dfa580e15 100644 --- a/src/otx/algorithms/anomaly/tasks/openvino.py +++ b/src/otx/algorithms/anomaly/tasks/openvino.py @@ -361,6 +361,8 @@ def optimize( 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() From d06e079631b80dc4c21f8080c86e733fcef2347c Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 9 Nov 2023 16:51:42 +0900 Subject: [PATCH 101/146] modify omegaconf version constraint --- requirements/api.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/api.txt b/requirements/api.txt index 2148d603747..16bfbffcffc 100644 --- a/requirements/api.txt +++ b/requirements/api.txt @@ -3,7 +3,7 @@ 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.3.* +omegaconf~=2.1.1 opencv-python==4.8.1.* pymongo==4.6.* scikit-learn==1.3.* From d9cdcc29fec4e523d6e39e8241cbc87019b57828 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Thu, 9 Nov 2023 10:39:16 +0200 Subject: [PATCH 102/146] [release 1.5.0] Fix XAI algorithm for Detection (#2617) Update detection XAI algorithm --- .../hooks/det_class_probability_map_hook.py | 14 ++++---- .../detection/test_xai_detection_validity.py | 36 +++++++++---------- 2 files changed, 26 insertions(+), 24 deletions(-) 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 index 7931e234091..2847f1c573a 100644 --- 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 @@ -60,12 +60,9 @@ def func( else: cls_scores = self._get_cls_scores_from_feature_map(feature_map) - # 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 and self._num_cls_out_channels > 1: - cls_scores = [torch.softmax(t, dim=1) for t in cls_scores] - - batch_size, _, height, width = cls_scores[-1].size() + 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 = [] @@ -82,6 +79,11 @@ def func( ) 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) diff --git a/tests/unit/algorithms/detection/test_xai_detection_validity.py b/tests/unit/algorithms/detection/test_xai_detection_validity.py index 89c28fd83a1..0b38853397e 100644 --- a/tests/unit/algorithms/detection/test_xai_detection_validity.py +++ b/tests/unit/algorithms/detection/test_xai_detection_validity.py @@ -24,31 +24,31 @@ class TestExplainMethods: ref_saliency_shapes = { - "MobileNetV2-ATSS": (2, 4, 4), - "ResNeXt101-ATSS": (2, 4, 4), + "MobileNetV2-ATSS": (2, 13, 13), + "ResNeXt101-ATSS": (2, 13, 13), "SSD": (81, 13, 13), - "YOLOX-TINY": (80, 13, 13), - "YOLOX-S": (80, 13, 13), - "YOLOX-L": (80, 13, 13), - "YOLOX-X": (80, 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([67, 216, 255, 57], dtype=np.uint8), - "ResNeXt101-ATSS": np.array([75, 214, 229, 173], dtype=np.uint8), - "YOLOX-TINY": np.array([80, 28, 42, 53, 49, 68, 72, 75, 69, 57, 65, 6, 157], dtype=np.uint8), - "YOLOX-S": np.array([75, 178, 151, 159, 150, 148, 144, 144, 147, 144, 147, 142, 189], dtype=np.uint8), - "YOLOX-L": np.array([43, 28, 0, 6, 7, 19, 22, 17, 14, 18, 25, 7, 34], dtype=np.uint8), - "YOLOX-X": np.array([255, 144, 83, 76, 83, 86, 82, 90, 91, 93, 110, 104, 83], dtype=np.uint8), - "SSD": np.array([119, 72, 118, 35, 39, 30, 31, 31, 36, 27, 44, 23, 61], dtype=np.uint8), + "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.10465062, - "ResNeXt101-ATSS": -0.073549636, + "MobileNetV2-ATSS": -0.014513552, + "ResNeXt101-ATSS": -0.055565584, "YOLOX-TINY": 0.04948914, - "YOLOX-S": 0.01133332, - "YOLOX-L": 0.01870133, + "YOLOX-S": 0.011557617, + "YOLOX-L": 0.020231, "YOLOX-X": 0.0043506604, "SSD": 0.6629989, } @@ -93,7 +93,7 @@ def test_saliency_map_det(self, template): 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].astype(np.int16) + 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) From 6462a4be44e28c50338af647bd89a5067cb26aaa Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 13:38:29 +0900 Subject: [PATCH 103/146] Update dependency constraint (#2622) --- requirements/api.txt | 6 +++--- requirements/base.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/api.txt b/requirements/api.txt index 16bfbffcffc..4b1f9a8cfb0 100644 --- a/requirements/api.txt +++ b/requirements/api.txt @@ -3,9 +3,9 @@ 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.8.1.* -pymongo==4.6.* +omegaconf>=2.1.1 +opencv-python>=4.5 +pymongo==4.5.* scikit-learn==1.3.* Shapely>=1.7.1,<=1.8.0 imagesize==1.4.1 diff --git a/requirements/base.txt b/requirements/base.txt index aad13eace7b..b9724c57a9b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,6 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Base Algo Requirements. # -natsort==8.4.* +natsort==8.1.* prettytable==3.9.* protobuf==3.20.* pyyaml From 6d3dd3484edfdca56deaf3c0c8e78058aeccf8d0 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 14:33:53 +0900 Subject: [PATCH 104/146] Update tpp (#2621) --- third-party-programs.txt | 241 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/third-party-programs.txt b/third-party-programs.txt index e6ddfe9006c..5cc43c4964c 100644 --- a/third-party-programs.txt +++ b/third-party-programs.txt @@ -1035,3 +1035,244 @@ Apache-2.0 limitations under the License. ------------------------------------------------------------- + +pynvml + +BSD-3-Clause + +Copyright (c) 2011-2021, NVIDIA Corporation. +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 staged-recipes 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. + +------------------------------------------------------------- + +segment-anything + +Apache-2.0 + + 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 [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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 2c9b745d440ef95c028dc9c9353074a08b42b88a Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 10 Nov 2023 16:28:48 +0900 Subject: [PATCH 105/146] Fix h-label bug of missing parent labels in output (#2626) * Fix h-label bug of missing parent labels in output * Fix h-label test data label schema * Update CHANGELOG.md --------- Signed-off-by: Songki Choi --- CHANGELOG.md | 1 + .../adapters/mmcls/datasets/otx_datasets.py | 8 ++++++++ src/otx/algorithms/classification/adapters/mmcls/task.py | 1 + tests/assets/datumaro_h-label/annotations/train.json | 5 +++++ tests/assets/datumaro_h-label/annotations/valid.json | 5 +++++ .../annotations/train.json | 5 +++++ .../annotations/valid.json | 5 +++++ 7 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f009610b0e7..9116e8b6adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Fix h-label loss normalization issue w/ exclusive label group of singe label () - Fix division by zero in class incremental learning for classification () - Fix saliency maps calculation issue for detection models () +- Fix h-label bug of missing parent labels in output () ## \[v1.4.3\] diff --git a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py index 9d62cb48cee..ed4267d366e 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py +++ b/src/otx/algorithms/classification/adapters/mmcls/datasets/otx_datasets.py @@ -300,6 +300,7 @@ class OTXHierarchicalClsDataset(OTXMultilabelClsDataset): 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): @@ -308,6 +309,13 @@ def load_annotations(self): 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"] diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 312c968956c..9d29087e039 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -190,6 +190,7 @@ def configure( 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" diff --git a/tests/assets/datumaro_h-label/annotations/train.json b/tests/assets/datumaro_h-label/annotations/train.json index dc7994026dc..f641fbb2352 100644 --- a/tests/assets/datumaro_h-label/annotations/train.json +++ b/tests/assets/datumaro_h-label/annotations/train.json @@ -3,6 +3,11 @@ "categories": { "label": { "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, { "name": "blue", "group_type": "exclusive", diff --git a/tests/assets/datumaro_h-label/annotations/valid.json b/tests/assets/datumaro_h-label/annotations/valid.json index dc7994026dc..f641fbb2352 100644 --- a/tests/assets/datumaro_h-label/annotations/valid.json +++ b/tests/assets/datumaro_h-label/annotations/valid.json @@ -3,6 +3,11 @@ "categories": { "label": { "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, { "name": "blue", "group_type": "exclusive", diff --git a/tests/assets/datumaro_h-label_class_decremental/annotations/train.json b/tests/assets/datumaro_h-label_class_decremental/annotations/train.json index be96929c774..dbd6dfa0702 100644 --- a/tests/assets/datumaro_h-label_class_decremental/annotations/train.json +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/train.json @@ -3,6 +3,11 @@ "categories": { "label": { "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, { "name": "blue", "group_type": "exclusive", diff --git a/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json b/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json index be96929c774..dbd6dfa0702 100644 --- a/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json +++ b/tests/assets/datumaro_h-label_class_decremental/annotations/valid.json @@ -3,6 +3,11 @@ "categories": { "label": { "label_groups": [ + { + "name": "shape", + "group_type": "exclusive", + "labels": ["blue", "green"] + }, { "name": "blue", "group_type": "exclusive", From 13a39932caa9134f9ab6bc7bbb6c2d09fc28d0e0 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 16:36:02 +0900 Subject: [PATCH 106/146] Update publish workflow (#2625) update publish workflow to push whl to internal pypi --- .github/workflows/publish.yml | 38 +++++++++++-------- src/otx/__init__.py | 2 +- .../exportable_code/demo/requirements.txt | 2 +- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3885e3ec9cf..2d5d14f1601 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Build and upload to PyPI +name: Build and upload to internal PyPI on: workflow_dispatch: # run on request (no need for PR) @@ -40,9 +40,15 @@ jobs: name: Publish package needs: [build_wheels, build_sdist] environment: pypi - runs-on: ubuntu-latest + runs-on: [self-hosted, linux, x64, dev] permissions: write-all steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install dependencies + run: python -m pip install twine - name: Download artifacts uses: actions/download-artifact@v3 with: @@ -50,7 +56,6 @@ jobs: # if `name: artifact` is omitted, the action will create extra parent dir name: artifact path: dist - # to determine where to publish the source distribution to PyPI or TestPyPI - name: Check tag id: check-tag uses: actions-ecosystem/action-regex-match@v2 @@ -66,15 +71,18 @@ jobs: 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@v1.7.1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - name: Publish package distributions to TestPyPI - if: ${{ steps.check-tag.outputs.match == '' }} - uses: pypa/gh-action-pypi-publish@v1.7.1 - with: - password: ${{ secrets.TESTPYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ - verbose: true + - name: Check dist contents + run: twine check dist/* + - name: Publish package dist to internal PyPI + 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: 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/src/otx/__init__.py b/src/otx/__init__.py index 0730c04e7d7..320afca933b 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.4rc1" +__version__ = "1.4.4" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 26d5f57a346..eb4d7eb29fd 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.6 -otx==1.4.4rc1 +otx==1.4.4 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From eadbb15445330518586806dfcff4fbcdf5a9b589 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 19:22:11 +0900 Subject: [PATCH 107/146] bump datumaro version to ~=1.5.0 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 7203f9422a2..7b49f913640 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort==8.1.* prettytable==3.9.* protobuf==3.20.* pyyaml -datumaro==1.5.1rc3 +datumaro~=1.5.0 psutil==5.9.* scipy==1.10.* bayesian-optimization==1.4.* From 516d395e277f6b51cbeb777c3b3c51d8ca98b8b3 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 20:14:49 +0900 Subject: [PATCH 108/146] fixed mistake while mergeing back 1.4.4 --- requirements/base.txt | 2 +- tests/assets/datumaro_h-label/annotations/train.json | 2 +- tests/integration/cli/anomaly/test_anomaly_classification.py | 1 - tests/integration/cli/anomaly/test_anomaly_detection.py | 1 - tests/integration/cli/anomaly/test_anomaly_segmentation.py | 1 - .../cli/instance_segmentation/test_instance_segmentation.py | 3 --- .../cli/instance_segmentation/test_rotated_detection.py | 1 + .../cli/semantic_segmentation/test_segmentation.py | 5 +---- .../classification/adapters/mmcls/test_configurer.py | 5 ----- 9 files changed, 4 insertions(+), 17 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 7b49f913640..7203f9422a2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort==8.1.* prettytable==3.9.* protobuf==3.20.* pyyaml -datumaro~=1.5.0 +datumaro==1.5.1rc3 psutil==5.9.* scipy==1.10.* bayesian-optimization==1.4.* diff --git a/tests/assets/datumaro_h-label/annotations/train.json b/tests/assets/datumaro_h-label/annotations/train.json index 5761a74f3aa..f641fbb2352 100644 --- a/tests/assets/datumaro_h-label/annotations/train.json +++ b/tests/assets/datumaro_h-label/annotations/train.json @@ -4,7 +4,7 @@ "label": { "label_groups": [ { - "name": "blue", + "name": "shape", "group_type": "exclusive", "labels": ["blue", "green"] }, diff --git a/tests/integration/cli/anomaly/test_anomaly_classification.py b/tests/integration/cli/anomaly/test_anomaly_classification.py index abf5a50dad1..833497307a0 100644 --- a/tests/integration/cli/anomaly/test_anomaly_classification.py +++ b/tests/integration/cli/anomaly/test_anomaly_classification.py @@ -11,7 +11,6 @@ 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 ( - generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, diff --git a/tests/integration/cli/anomaly/test_anomaly_detection.py b/tests/integration/cli/anomaly/test_anomaly_detection.py index 7af1219b4c4..9ba6f10f257 100644 --- a/tests/integration/cli/anomaly/test_anomaly_detection.py +++ b/tests/integration/cli/anomaly/test_anomaly_detection.py @@ -11,7 +11,6 @@ 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 ( - generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, diff --git a/tests/integration/cli/anomaly/test_anomaly_segmentation.py b/tests/integration/cli/anomaly/test_anomaly_segmentation.py index de589f46b24..6a52f9727fb 100644 --- a/tests/integration/cli/anomaly/test_anomaly_segmentation.py +++ b/tests/integration/cli/anomaly/test_anomaly_segmentation.py @@ -11,7 +11,6 @@ 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 ( - generate_model_template_testing, nncf_optimize_testing, otx_deploy_openvino_testing, otx_eval_deployment_testing, diff --git a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py index 9d7e7ed3fda..d07943b099f 100644 --- a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py +++ b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py @@ -90,9 +90,6 @@ TestInstanceSegmentationModelTemplates = generate_model_template_testing(templates) -TestInstanceSegmentationModelTemplates = generate_model_template_testing(templates) - - class TestInstanceSegmentationCLI: @e2e_pytest_component @pytest.mark.parametrize("template", templates_with_experimental, ids=templates_ids_with_experimental) diff --git a/tests/integration/cli/instance_segmentation/test_rotated_detection.py b/tests/integration/cli/instance_segmentation/test_rotated_detection.py index 3abab1c9719..e51966eb866 100644 --- a/tests/integration/cli/instance_segmentation/test_rotated_detection.py +++ b/tests/integration/cli/instance_segmentation/test_rotated_detection.py @@ -33,6 +33,7 @@ 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 diff --git a/tests/integration/cli/semantic_segmentation/test_segmentation.py b/tests/integration/cli/semantic_segmentation/test_segmentation.py index 05fdc54430c..7f1b286e0df 100644 --- a/tests/integration/cli/semantic_segmentation/test_segmentation.py +++ b/tests/integration/cli/semantic_segmentation/test_segmentation.py @@ -99,9 +99,6 @@ TestSemanticSegmentationModelTemplates = generate_model_template_testing(templates) -TestSemanticSegmentationModelTemplates = generate_model_template_testing(templates) - - class TestSegmentationCLI: @e2e_pytest_component @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) @@ -246,5 +243,5 @@ def test_otx_train_auto_adapt_batch_size(self, template, tmp_dir_path, bs_adapt_ 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"segmentation_auto_adapt_num_workers" + 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/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index 40f6b785f82..ab513913749 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -47,11 +47,6 @@ def setup(self) -> None: os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_hierarchical.py") ) - self.multilabel_model_cfg = MPAConfig.fromfile(os.path.join(DEFAULT_CLS_TEMPLATE_DIR, "model_multilabel.py")) - self.hierarchical_model_cfg = MPAConfig.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") From d84c3d4bdfafc4a3d14fecf0d2f38e8fbef58b08 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 10 Nov 2023 21:01:44 +0900 Subject: [PATCH 109/146] modifiy readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 53bb6423032..55d4fbc6ec6 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ --- -[Key Features](#key-features) -[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html) -[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.2/index.html) +[Key Features](#key-features) • +[Installation](https://openvinotoolkit.github.io/training_extensions/stable/guide/get_started/installation.html) • +[Documentation](https://openvinotoolkit.github.io/training_extensions/stable/index.html) • [License](#license) [![PyPI](https://img.shields.io/pypi/v/otx)](https://pypi.org/project/otx) @@ -55,7 +55,7 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: - **Action recognition** including action classification and detection - **Anomaly recognition** tasks including anomaly classification, detection and segmentation -OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/algorithms/index.html): +OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/algorithms/index.html): - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks - **Semi-supervised learning** @@ -75,7 +75,7 @@ OpenVINO™ Training Extensions provides the following usability features: ### Installation -Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html). +Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/stable/guide/get_started/installation.html). Note: Python 3.8, 3.9 and 3.10 were tested, along with Ubuntu 18.04, 20.04 and 22.04. @@ -91,7 +91,7 @@ Note: Python 3.8, 3.9 and 3.10 were tested, along with Ubuntu 18.04, 20.04 and 2 - `otx demo` allows one to apply a trained model on the custom data or the online footage from a web camera and see how it will work in a real-life scenario. - `otx explain` runs explain algorithm on the provided data and outputs images with the saliency maps to show how your model makes predictions. -You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/cli_commands.html). +You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/stable/guide/get_started/cli_commands.html). --- From 5e5729bd7bcf431fb3f3e128d26a531791e9e12d Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 13 Nov 2023 09:03:56 +0900 Subject: [PATCH 110/146] remove openvino model wrapper class --- .../model_wrappers/openvino_models.py | 270 ------------------ 1 file changed, 270 deletions(-) delete mode 100644 src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py diff --git a/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py b/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py deleted file mode 100644 index 2936197fa4f..00000000000 --- a/src/otx/algorithms/detection/adapters/openvino/model_wrappers/openvino_models.py +++ /dev/null @@ -1,270 +0,0 @@ -"""OTXMaskRCNNModel & OTXSSDModel 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 typing import Dict - -import cv2 -import numpy as np -from openvino.model_api.models.instance_segmentation import MaskRCNNModel, _expand_box, _segm_postprocess -from openvino.model_api.models.ssd import SSD, find_layer_by_name -from openvino.model_api.models.utils import Detection - - -class OTXMaskRCNNModel(MaskRCNNModel): - """OpenVINO model wrapper for OTX MaskRCNN model.""" - - __model__ = "OTX_MaskRCNN" - - def __init__(self, model_adapter, configuration, preload=False): - super().__init__(model_adapter, configuration, preload) - self.resize_mask = True - - def _check_io_number(self, number_of_inputs, number_of_outputs): - """Checks whether the number of model inputs/outputs is supported. - - Args: - number_of_inputs (int, Tuple(int)): number of inputs supported by wrapper. - Use -1 to omit the check - number_of_outputs (int, Tuple(int)): number of outputs supported by wrapper. - Use -1 to omit the check - - Raises: - WrapperError: if the model has unsupported number of inputs/outputs - """ - super()._check_io_number(number_of_inputs, -1) - - def _get_outputs(self): - output_match_dict = {} - output_names = ["boxes", "labels", "masks", "feature_vector", "saliency_map"] - for output_name in output_names: - for node_name, node_meta in self.outputs.items(): - if output_name in node_meta.names: - output_match_dict[output_name] = node_name - break - return output_match_dict - - def postprocess(self, outputs, meta): - """Post process function for OTX MaskRCNN model.""" - - # pylint: disable-msg=too-many-locals - # FIXME: here, batch dim of IR must be 1 - boxes = outputs[self.output_blob_name["boxes"]] - if boxes.shape[0] == 1: - boxes = boxes.squeeze(0) - assert boxes.ndim == 2 - masks = outputs[self.output_blob_name["masks"]] - if masks.shape[0] == 1: - masks = masks.squeeze(0) - assert masks.ndim == 3 - classes = outputs[self.output_blob_name["labels"]].astype(np.uint32) - if classes.shape[0] == 1: - classes = classes.squeeze(0) - assert classes.ndim == 1 - if self.is_segmentoly: - scores = outputs[self.output_blob_name["scores"]] - else: - scores = boxes[:, 4] - boxes = boxes[:, :4] - classes += 1 - - # Filter out detections with low confidence. - detections_filter = scores > self.confidence_threshold # pylint: disable=no-member - scores = scores[detections_filter] - boxes = boxes[detections_filter] - masks = masks[detections_filter] - classes = classes[detections_filter] - - inputImgWidth, inputImgHeight = ( - meta["original_shape"][1], - meta["original_shape"][0], - ) - invertedScaleX, invertedScaleY = ( - inputImgWidth / self.orig_width, - inputImgHeight / self.orig_height, - ) - padLeft, padTop = 0, 0 - if "fit_to_window" == self.resize_type or "fit_to_window_letterbox" == self.resize_type: - invertedScaleX = invertedScaleY = max(invertedScaleX, invertedScaleY) - if "fit_to_window_letterbox" == self.resize_type: - padLeft = (self.orig_width - round(inputImgWidth / invertedScaleX)) // 2 - padTop = (self.orig_height - round(inputImgHeight / invertedScaleY)) // 2 - - boxes -= (padLeft, padTop, padLeft, padTop) - boxes *= (invertedScaleX, invertedScaleY, invertedScaleX, invertedScaleY) - np.around(boxes, out=boxes) - np.clip( - boxes, - 0.0, - [inputImgWidth, inputImgHeight, inputImgWidth, inputImgHeight], - out=boxes, - ) - - resized_masks = [] - for box, cls, raw_mask in zip(boxes, classes, masks): - raw_cls_mask = raw_mask[cls, ...] if self.is_segmentoly else raw_mask - if self.resize_mask: - resized_masks.append(_segm_postprocess(box, raw_cls_mask, *meta["original_shape"][:-1])) - else: - resized_masks.append(raw_cls_mask) - - return scores, classes, boxes, resized_masks - - def get_saliency_map_from_prediction(self, outputs, meta, num_classes): - """Post process function for saliency map of OTX MaskRCNN model.""" - boxes = outputs[self.output_blob_name["boxes"]] - if boxes.shape[0] == 1: - boxes = boxes.squeeze(0) - scores = boxes[:, 4] - boxes = boxes[:, :4] - masks = outputs[self.output_blob_name["masks"]] - if masks.shape[0] == 1: - masks = masks.squeeze(0) - classes = outputs[self.output_blob_name["labels"]].astype(np.uint32) - if classes.shape[0] == 1: - classes = classes.squeeze(0) - - scale_x = meta["resized_shape"][0] / meta["original_shape"][1] - scale_y = meta["resized_shape"][1] / meta["original_shape"][0] - boxes[:, 0::2] /= scale_x - boxes[:, 1::2] /= scale_y - - saliency_maps = [None for _ in range(num_classes)] - for box, score, cls, raw_mask in zip(boxes, scores, classes, masks): - resized_mask = self._resize_mask(box, raw_mask * score, *meta["original_shape"][:-1]) - if saliency_maps[cls] is None: - saliency_maps[cls] = [resized_mask] - else: - saliency_maps[cls].append(resized_mask) - - saliency_maps = self._average_and_normalize(saliency_maps, num_classes) - return saliency_maps - - def _resize_mask(self, box, raw_cls_mask, im_h, im_w): - # Add zero border to prevent upsampling artifacts on segment borders. - raw_cls_mask = np.pad(raw_cls_mask, ((1, 1), (1, 1)), "constant", constant_values=0) - extended_box = _expand_box(box, raw_cls_mask.shape[0] / (raw_cls_mask.shape[0] - 2.0)).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=[im_w, im_h]) - x1, y1 = np.clip(extended_box[2:] + 1, a_min=0, a_max=[im_w, im_h]) - - raw_cls_mask = cv2.resize(raw_cls_mask.astype(np.float32), (w, h)) - # Put an object mask in an image mask. - im_mask = np.zeros((im_h, im_w), dtype=np.float32) - im_mask[y0:y1, x0:x1] = raw_cls_mask[ - (y0 - extended_box[1]) : (y1 - extended_box[1]), (x0 - extended_box[0]) : (x1 - extended_box[0]) - ] - return im_mask - - @staticmethod - def _average_and_normalize(saliency_maps, num_classes): - for i in range(num_classes): - if saliency_maps[i] is not None: - saliency_maps[i] = np.array(saliency_maps[i]).mean(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 segm_postprocess(self, *args, **kwargs): - """Post-process for segmentation masks.""" - return _segm_postprocess(*args, **kwargs) - - def disable_mask_resizing(self): - """Disable mask resizing. - - There is no need to resize mask in tile as it will be processed at the end. - """ - self.resize_mask = False - - -class OTXSSDModel(SSD): - """OpenVINO model wrapper for OTX SSD model.""" - - __model__ = "OTX_SSD" - - def __init__(self, model_adapter, configuration=None, preload=False): - # pylint: disable-next=bad-super-call - super(SSD, self).__init__(model_adapter, configuration, preload) - self.image_info_blob_name = self.image_info_blob_names[0] if len(self.image_info_blob_names) == 1 else None - self.output_parser = BatchBoxesLabelsParser( - self.outputs, - self.inputs[self.image_blob_name].shape[2:][::-1], - ) - - def _get_outputs(self) -> Dict: - """Match the output names with graph node index.""" - output_match_dict = {} - output_names = ["boxes", "labels", "feature_vector", "saliency_map"] - for output_name in output_names: - for node_name, node_meta in self.outputs.items(): - if output_name in node_meta.names: - output_match_dict[output_name] = node_name - break - return output_match_dict - - -class BatchBoxesLabelsParser: - """Batched output parser.""" - - def __init__(self, layers, input_size, labels_layer="labels", default_label=0): - try: - self.labels_layer = find_layer_by_name(labels_layer, layers) - except ValueError: - self.labels_layer = None - self.default_label = default_label - - try: - self.bboxes_layer = self.find_layer_bboxes_output(layers) - except ValueError: - self.bboxes_layer = find_layer_by_name("boxes", layers) - - self.input_size = input_size - - @staticmethod - def find_layer_bboxes_output(layers): - """find_layer_bboxes_output.""" - filter_outputs = [name for name, data in layers.items() if len(data.shape) == 3 and data.shape[-1] == 5] - if not filter_outputs: - raise ValueError("Suitable output with bounding boxes is not found") - if len(filter_outputs) > 1: - raise ValueError("More than 1 candidate for output with bounding boxes.") - return filter_outputs[0] - - def __call__(self, outputs): - """Parse bboxes.""" - # FIXME: here, batch dim of IR must be 1 - bboxes = outputs[self.bboxes_layer] - if bboxes.shape[0] == 1: - bboxes = bboxes.squeeze(0) - assert bboxes.ndim == 2 - scores = bboxes[:, 4] - bboxes = bboxes[:, :4] - bboxes[:, 0::2] /= self.input_size[0] - bboxes[:, 1::2] /= self.input_size[1] - if self.labels_layer: - labels = outputs[self.labels_layer] - else: - labels = np.full(len(bboxes), self.default_label, dtype=bboxes.dtype) - if labels.shape[0] == 1: - labels = labels.squeeze(0) - - detections = [Detection(*bbox, score, label) for label, score, bbox in zip(labels, scores, bboxes)] - return detections From 0325c148326590a7e4b619e9d4c1a1d003b1af7d Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 13 Nov 2023 09:20:55 +0900 Subject: [PATCH 111/146] remove openvino model wrapper tests --- .../test_detection_openvino_models.py | 182 ------------------ 1 file changed, 182 deletions(-) delete mode 100644 tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py diff --git a/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py b/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py deleted file mode 100644 index d7b1646bdc2..00000000000 --- a/tests/unit/algorithms/detection/adapters/openvino/model_wrappers/test_detection_openvino_models.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Unit Test for otx.algorithms.detection.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.detection.adapters.openvino.model_wrappers.openvino_models import ( - BatchBoxesLabelsParser, - OTXMaskRCNNModel, - OTXSSDModel, -) -from tests.test_suite.e2e_test_system import e2e_pytest_unit - - -class MockOpenvinoAdapter(OpenvinoAdapter): - """Mock class for OpenvinoAdapter.""" - - def __init__(self): - pass - - -class MockBatchBoxesLabelsParser(BatchBoxesLabelsParser): - """Mock class for BatchBoxesLabelsParser.""" - - def __init__(self): - self.labels_layer = "labels" - self.bboxes_layer = "boxes" - self.input_size = (10, 10) - - -class MockOTXMaskRCNNModel(OTXMaskRCNNModel): - """Mock class for OTXMaskRCNNModel.""" - - def __init__(self, *args): - self.inputs: Dict[str, np.ndarray] = { - "image": np.ndarray([1, 3, 10, 10]), - } - - self.outputs: Dict[str, Config] = { - "boxes": Config({"names": "boxes", "shape": [1, 1, 5]}), - "labels": Config({"names": "labels", "shape": [1, 1]}), - "masks": Config({"names": "masks", "shape": [1, 0, 28, 28]}), - "feature_vector": Config({"names": "feature_vector", "shape": [1, 1, 1, 1]}), - "saliency_map": Config({"names": "saliency_map", "shape": [1, 1, 1]}), - } - self.is_segmentoly = len(self.inputs) == 2 - self.output_blob_name = self._get_outputs() - self.confidence_threshold = 0.5 - self.orig_width = 100 - self.orig_height = 100 - self.resize_type = "" - super().__init__(MockOpenvinoAdapter, {}) - - -class MockOTXSSDModel(OTXSSDModel): - """Mock class for OTXSSDModel.""" - - def __init__(self, *args): - self.inputs: Dict[str, np.ndarray] = { - "image": np.ndarray([1, 3, 10, 10]), - } - - self.outputs: Dict[str, Config] = { - "boxes": Config({"names": "boxes", "shape": [1, 1, 5]}), - "labels": Config({"names": "labels", "shape": [1, 1]}), - "masks": Config({"names": "masks", "shape": [1, 0, 28, 28]}), - "feature_vector": Config({"names": "feature_vector", "shape": [1, 1, 1, 1]}), - "saliency_map": Config({"names": "saliency_map", "shape": [1, 1, 1]}), - } - self.confidence_threshold = 0.375 - self.resize_type = "standard" - self.output_parser = MockBatchBoxesLabelsParser() - self.labels = [] - self.w = 10 - self.h = 10 - super().__init__(MockOpenvinoAdapter) - - -class TestOTXMaskRCNNModel: - """Test OTXMaskRCNNModel class. - - Test postprocess function - - 1. Generate sample output & meta - 2. Check whether postprocess function returns (scores, classes, boxes, resized_masks) tuple with length 4. - """ - - @pytest.fixture(autouse=True) - def setup(self, mocker) -> None: - mocker.patch( - "openvino.model_api.models.MaskRCNNModel.__init__", - return_value=True, - ) - self.model = MockOTXMaskRCNNModel() - - @e2e_pytest_unit - def test_postprocess(self) -> None: - """Test postprocess function.""" - - sample_output = { - "boxes": np.random.rand(1, 1, 5), - "labels": np.random.rand(1, 1), - "masks": np.random.rand(1, 1, 28, 28), - "feature_vector": np.random.rand(1, 1, 1, 1), - "saliency_map": np.random.rand(1, 1, 21), - } - sample_meta = {"original_shape": (10, 10, 3), "resized_shape": (5, 5, 3)} - out = self.model.postprocess(sample_output, meta=sample_meta) - assert len(out) == 4 - - -class TestOTXSSDModel: - """Test OTXSSDModel class. - Test postprocess function - - 1. Generate sample output & meta - 2. Check whether postprocess function returns 'detection' with length 1. - """ - - @pytest.fixture(autouse=True) - def setup(self, mocker) -> None: - mocker.patch( - "otx.algorithms.detection.adapters.openvino.model_wrappers.openvino_models.OTXSSDModel.__init__", - return_value=True, - ) - self.model = MockOTXSSDModel() - - @e2e_pytest_unit - def test_postprocess(self) -> None: - """Test postprocess function.""" - - sample_output = { - "boxes": np.random.rand(1, 1, 5), - "labels": np.random.rand(1, 1), - "feature_vector": np.random.rand(1, 1, 1, 1), - "saliency_map": np.random.rand(1, 1, 21), - } - sample_meta = {"original_shape": (10, 10, 3), "resized_shape": (5, 5, 3)} - out = self.model.postprocess(sample_output, meta=sample_meta) - assert len(out.objects) <= 1 - - -class TestBatchBoxesLabelsParser: - """Test BatchBoxesLabelsParser class. - - 1. Test __init__ function - - 1. Check parser's attributes for bboxes_layer and input_size - 3. Test layer_bboxes_output function - - 2. Check postprocess function's output return's argmax - """ - - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.layers: Dict[str, Config] = { - "boxes": Config({"names": "boxes", "shape": [1, 1, 5]}), - "labels": Config({"names": "labels", "shape": [1, 1]}), - "masks": Config({"names": "masks", "shape": [1, 0, 28, 28]}), - "feature_vector": Config({"names": "feature_vector", "shape": [1, 1, 1, 1]}), - "saliency_map": Config({"names": "saliency_map", "shape": [1, 1, 1]}), - } - input_size = (10, 10) - self.parser = BatchBoxesLabelsParser(self.layers, input_size) - - @e2e_pytest_unit - def test_init(self) -> None: - assert hasattr(self.parser, "bboxes_layer") - assert hasattr(self.parser, "input_size") - - @e2e_pytest_unit - def test_layer_bboxes_output(self) -> None: - """Test postprocess function.""" - out = self.parser.find_layer_bboxes_output(self.layers) - assert out == "boxes" From 16dd6887cdd4b48458673c4c109307ef17059313 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Mon, 13 Nov 2023 08:24:08 +0200 Subject: [PATCH 112/146] [release 1.5.0] DeiT: enable tests + add ViTFeatureVectorHook (#2630) Add ViT feature vector hook --- .../classification/adapters/mmcls/task.py | 22 ++++------- .../mmcv/hooks/recording_forward_hook.py | 12 +++++- .../cli/classification/test_classification.py | 14 ------- .../cli/classification/test_classification.py | 38 ------------------- 4 files changed, 19 insertions(+), 67 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 55b7ea0dd3a..e42e4d5f628 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -19,7 +19,6 @@ from mmcv.runner import wrap_fp16_model from mmcv.utils import Config, ConfigDict -from otx.algorithms import TRANSFORMER_BACKBONES from otx.algorithms.classification.adapters.mmcls.utils.exporter import ( ClassificationExporter, ) @@ -31,6 +30,7 @@ EigenCamHook, FeatureVectorHook, ReciproCAMHook, + ViTFeatureVectorHook, ViTReciproCAMHook, ) from otx.algorithms.common.adapters.mmcv.utils import ( @@ -225,7 +225,6 @@ def _infer_model( ) ) - dump_features = True dump_saliency_map = not inference_parameters.is_evaluation if inference_parameters else True self._init_task() @@ -274,16 +273,16 @@ def hook(module, inp, outp): forward_explainer_hook: Union[nullcontext, BaseRecordingForwardHook] if model_type == "VisionTransformer": forward_explainer_hook = ViTReciproCAMHook(feature_model) - elif ( - not dump_saliency_map or model_type in TRANSFORMER_BACKBONES - ): # TODO: remove latter "or" condition after resolving Issue#2098 + elif not dump_saliency_map: forward_explainer_hook = nullcontext() else: forward_explainer_hook = ReciproCAMHook(feature_model) - if ( - not dump_features or model_type in TRANSFORMER_BACKBONES - ): # TODO: remove latter "or" condition after resolving Issue#2098 - feature_vector_hook: Union[nullcontext, BaseRecordingForwardHook] = nullcontext() + + 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) @@ -533,11 +532,6 @@ def _export_model(self, precision: ModelPrecision, export_format: ExportType, du export_options["precision"] = str(precision) export_options["type"] = str(export_format) - # [TODO] Enable dump_features for ViT backbones - model_type = cfg.model.backbone.type.split(".")[-1] # mmcls.VisionTransformer => VisionTransformer - if model_type in TRANSFORMER_BACKBONES: - dump_features = False - export_options["deploy_cfg"]["dump_features"] = dump_features if dump_features: output_names = export_options["deploy_cfg"]["ir_config"]["output_names"] 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 index 062cc230367..f71440f0fef 100644 --- a/src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py +++ b/src/otx/algorithms/common/adapters/mmcv/hooks/recording_forward_hook.py @@ -16,7 +16,7 @@ from __future__ import annotations from abc import ABC -from typing import List, Optional, Sequence, Union +from typing import List, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -172,6 +172,16 @@ def func(feature_map: Union[torch.Tensor, Sequence[torch.Tensor]], fpn_idx: int 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. diff --git a/tests/e2e/cli/classification/test_classification.py b/tests/e2e/cli/classification/test_classification.py index 3252c596bcd..9f075925119 100644 --- a/tests/e2e/cli/classification/test_classification.py +++ b/tests/e2e/cli/classification/test_classification.py @@ -137,8 +137,6 @@ def test_otx_resume(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny" and dump_features: - pytest.skip(reason="Issue#2098 ViT template does not support dump_features.") tmp_dir_path = tmp_dir_path / "multi_class_cls" otx_export_testing(template, tmp_dir_path, dump_features) @@ -160,8 +158,6 @@ def test_otx_eval(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_class_cls" otx_explain_testing(template, tmp_dir_path, otx_dir, args) @@ -169,8 +165,6 @@ def test_otx_explain(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_class_cls" otx_explain_openvino_testing(template, tmp_dir_path, otx_dir, args) @@ -383,8 +377,6 @@ def test_otx_resume(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny" and dump_features: - pytest.skip(reason="Issue#2098 ViT template does not support dump_features.") tmp_dir_path = tmp_dir_path / "multi_label_cls" otx_export_testing(template, tmp_dir_path, dump_features) @@ -399,8 +391,6 @@ def test_otx_eval(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_label_cls" otx_explain_testing(template, tmp_dir_path, otx_dir, args_m) @@ -546,8 +536,6 @@ def test_otx_resume(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny" and dump_features: - pytest.skip(reason="Issue#2098 ViT template does not support dump_features.") tmp_dir_path = tmp_dir_path / "h_label_cls" otx_export_testing(template, tmp_dir_path, dump_features) @@ -562,8 +550,6 @@ def test_otx_eval(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "h_label_cls" otx_explain_testing(template, tmp_dir_path, otx_dir, args_h) diff --git a/tests/integration/cli/classification/test_classification.py b/tests/integration/cli/classification/test_classification.py index 9e927d8bfbb..186613de79f 100644 --- a/tests/integration/cli/classification/test_classification.py +++ b/tests/integration/cli/classification/test_classification.py @@ -124,8 +124,6 @@ def test_otx_resume(self, template, tmp_dir_path): @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): - if template.name == "DeiT-Tiny" and dump_features: - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_class_cls" otx_export_testing(template, tmp_dir_path, dump_features, check_ir_meta=True) @@ -150,48 +148,36 @@ def test_otx_eval(self, template, tmp_dir_path): @e2e_pytest_component @pytest.mark.parametrize("template", templates, ids=templates_ids) def test_otx_explain(self, template, tmp_dir_path): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_class_cls" otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args) @@ -365,48 +351,36 @@ def test_otx_eval(self, template, tmp_dir_path): @e2e_pytest_component @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) def test_otx_explain(self, template, tmp_dir_path): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "multi_label_cls" otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args_m) @@ -502,48 +476,36 @@ def test_otx_eval(self, template, tmp_dir_path): @e2e_pytest_component @pytest.mark.parametrize("template", default_templates, ids=default_templates_ids) def test_otx_explain(self, template, tmp_dir_path): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") 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): - if template.name == "DeiT-Tiny": - pytest.skip(reason="Issue#2098 ViT inference does not work by FeatureVectorHook.") tmp_dir_path = tmp_dir_path / "h_label_cls" otx_explain_process_saliency_maps_openvino_testing(template, tmp_dir_path, otx_dir, args_h) From c46120024dbca27b9404deb57d25bfc45810c111 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 13 Nov 2023 16:00:56 +0900 Subject: [PATCH 113/146] Fix docs broken link to datatumaro_h-label Signed-off-by: Songki Choi --- .../algorithms/classification/hierarhical_classification.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst b/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst index 6c4dc241610..ca1267f7bb3 100644 --- a/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/hierarhical_classification.rst @@ -39,7 +39,7 @@ Dataset Format .. _hierarchical_dataset: 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 `_. +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: @@ -66,4 +66,4 @@ We use the same model templates as for Multi-class Classification. Please, refer .. Incremental Learning .. ******************** -.. To be added soon \ No newline at end of file +.. To be added soon From 9f4a34b59e166f0fcc5e85741c3c0b051ade56ce Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 13 Nov 2023 16:23:58 +0900 Subject: [PATCH 114/146] Fix wrong label settings for non-anomaly task ModelAPIs Signed-off-by: Songki Choi --- .../exportable_code/demo/demo_package/model_container.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 75ef3a5afe1..4723de654de 100644 --- 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 @@ -1,7 +1,6 @@ """ModelContainer class used for loading the model in the model wrapper.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -# import importlib import json @@ -64,7 +63,8 @@ def __init__(self, model_dir: Path, device="CPU") -> None: else ["Normal", "Anomaly"] ) else: - self.model_parameters["labels"] = [] + # model already contains correct labels + self.model_parameters.pop("labels") self._initialize_wrapper() self.core_model = Model.create_model( From 4ca63e8889d72e417edb83da521317cc844c65d2 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 14 Nov 2023 15:48:41 +0900 Subject: [PATCH 115/146] Update publish workflow for tag checking (#2632) --- .github/workflows/publish.yml | 38 +++++------ .github/workflows/publish_internal.yml | 92 ++++++++++++++++++++++++++ requirements/base.txt | 4 +- 3 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/publish_internal.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2d5d14f1601..3885e3ec9cf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Build and upload to internal PyPI +name: Build and upload to PyPI on: workflow_dispatch: # run on request (no need for PR) @@ -40,15 +40,9 @@ jobs: name: Publish package needs: [build_wheels, build_sdist] environment: pypi - runs-on: [self-hosted, linux, x64, dev] + runs-on: ubuntu-latest permissions: write-all steps: - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install dependencies - run: python -m pip install twine - name: Download artifacts uses: actions/download-artifact@v3 with: @@ -56,6 +50,7 @@ jobs: # if `name: artifact` is omitted, the action will create extra parent dir name: artifact path: dist + # to determine where to publish the source distribution to PyPI or TestPyPI - name: Check tag id: check-tag uses: actions-ecosystem/action-regex-match@v2 @@ -71,18 +66,15 @@ jobs: tag: ${{ github.ref }} overwrite: true file_glob: true - - name: Check dist contents - run: twine check dist/* - - name: Publish package dist to internal PyPI - 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: Clean up dist - if: ${{ always() }} - run: | - if OUTPUT=$(ls | grep -c dist) - then - echo "Cleaning up dist directory" - rm -r dist - fi + - name: Publish package distributions to PyPI + if: ${{ steps.check-tag.outputs.match != '' }} + uses: pypa/gh-action-pypi-publish@v1.7.1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + - name: Publish package distributions to TestPyPI + if: ${{ steps.check-tag.outputs.match == '' }} + uses: pypa/gh-action-pypi-publish@v1.7.1 + with: + password: ${{ secrets.TESTPYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + verbose: true diff --git a/.github/workflows/publish_internal.yml b/.github/workflows/publish_internal.yml new file mode 100644 index 00000000000..9e160481265 --- /dev/null +++ b/.github/workflows/publish_internal.yml @@ -0,0 +1,92 @@ +name: Build and upload to internal PyPI + +on: + workflow_dispatch: # run on request (no need for PR) + +jobs: + build_wheels: + name: Build wheels + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build wheels + uses: pypa/cibuildwheel@v2.13.1 + - uses: actions/upload-artifact@v3 + with: + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install pypa/build + run: python -m pip install build + - name: Build sdist + run: python -m build --sdist + - uses: actions/upload-artifact@v3 + with: + path: dist/*.tar.gz + + publish_package: + name: Publish package + needs: [build_wheels, build_sdist] + environment: pypi + runs-on: [self-hosted, linux, x64, dev] + permissions: write-all + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install dependencies + run: python -m pip install twine + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + # unpacks default artifact into dist/ + # if `name: artifact` is omitted, the action will create extra parent dir + name: artifact + path: dist + - name: Check tag + id: check-tag + uses: actions-ecosystem/action-regex-match@v2 + with: + text: ${{ github.ref }} + regex: '^refs/heads/releases/[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@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/* + tag: ${{ github.ref }} + overwrite: true + file_glob: true + - 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.TESTPYPI_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/requirements/base.txt b/requirements/base.txt index b9724c57a9b..c191b72f6f9 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,10 +1,10 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -# Base Algo Requirements. # +# Base Algo Requirements. # natsort==8.1.* prettytable==3.9.* protobuf==3.20.* pyyaml -datumaro==1.5.1rc3 +datumaro~=1.5.1rc4 psutil==5.9.* scipy==1.10.* bayesian-optimization==1.4.* From 061aecf7e98d4ac11f4f3bebb93cef77393a181a Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Wed, 15 Nov 2023 02:41:21 +0200 Subject: [PATCH 116/146] Update e2e tests for XAI Detection (#2634) Fix e2e XAI ref value --- tests/e2e/cli/detection/test_api_xai_sanity_detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py index 02024d54bfd..4cd11fa937c 100644 --- a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py +++ b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py @@ -32,7 +32,7 @@ class TestOVDetXAIAPI(DetectionTaskAPIBase): ref_raw_saliency_shapes = { - "MobileNetV2-ATSS": (4, 4), # Need to be adapted to configurable or adaptive input size + "MobileNetV2-ATSS": (16, 16), # Need to be adapted to configurable or adaptive input size } @e2e_pytest_api From 9f591b65edc4b4f3a9146ceac215051e3f888d46 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Wed, 15 Nov 2023 13:38:58 +0900 Subject: [PATCH 117/146] Disable QAT for newly added models (#2636) --- .../detection/configs/detection/cspdarknet_yolox_l/template.yaml | 1 - .../detection/configs/detection/cspdarknet_yolox_s/template.yaml | 1 - .../detection/configs/detection/cspdarknet_yolox_x/template.yaml | 1 - .../detection/configs/detection/resnext101_atss/template.yaml | 1 - .../convnext_maskrcnn/template_experimental.yaml | 1 - .../configs/instance_segmentation/maskrcnn_swin_t/template.yaml | 1 - 6 files changed, 6 deletions(-) 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 index 83a90ecfcca..f06013bba10 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 0c94a081ce3..335b07f8099 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 50e07835a96..1fdf665d533 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml index 27fe398fd1b..79308f5388a 100644 --- a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 6baef6921d3..44821039d86 100644 --- 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 @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index ed02d0f7e9f..61f359406e9 100644 --- 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 @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: From a2545f9ab241a549afcc8a38c3812c56af1f291c Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 15 Nov 2023 14:50:14 +0900 Subject: [PATCH 118/146] Update release note and readme (#2637) * update release note and readme * remove package upload step on internal publish wf --- .github/workflows/publish_internal.yml | 9 --------- README.md | 16 ++++++++-------- docs/source/guide/release_notes/index.rst | 7 +++++++ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish_internal.yml b/.github/workflows/publish_internal.yml index 9e160481265..800cc2c60ac 100644 --- a/.github/workflows/publish_internal.yml +++ b/.github/workflows/publish_internal.yml @@ -60,15 +60,6 @@ jobs: with: text: ${{ github.ref }} regex: '^refs/heads/releases/[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@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/* - tag: ${{ github.ref }} - overwrite: true - file_glob: true - name: Check dist contents run: twine check dist/* - name: Publish package dist to internal PyPI diff --git a/README.md b/README.md index a75f8436fa1..44931932aa4 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ --- -[Key Features](#key-features) -[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html) -[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.2/index.html) +[Key Features](#key-features) • +[Installation](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/get_started/installation.html) • +[Documentation](https://openvinotoolkit.github.io/training_extensions/1.4.4/index.html) • [License](#license) [![PyPI](https://img.shields.io/pypi/v/otx)](https://pypi.org/project/otx) @@ -54,7 +54,7 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: - **Action recognition** including action classification and detection - **Anomaly recognition** tasks including anomaly classification, detection and segmentation -OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/algorithms/index.html): +OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/explanation/algorithms/index.html): - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks - **Semi-supervised learning** @@ -64,9 +64,9 @@ OpenVINO™ Training Extensions will provide the following features in coming re - **Distributed training** to accelerate the training process when you have multiple GPUs - **Half-precision training** to save GPUs memory and use larger batch sizes -- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. +- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. - OpenVINO™ Training Extensions uses [Datumaro](https://openvinotoolkit.github.io/datumaro/v1.4.1/index.html) as the backend to hadle datasets. Thanks to that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We constantly working to extend supported formats to give more freedom of datasets format choice. -- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. +- [Auto-configuration functionality](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. --- @@ -74,7 +74,7 @@ OpenVINO™ Training Extensions will provide the following features in coming re ### Installation -Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/installation.html). +Please refer to the [installation guide](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/get_started/installation.html). Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. @@ -90,7 +90,7 @@ Note: Python 3.8 and 3.9 were tested, along with Ubuntu 18.04 and 20.04. - `otx demo` allows one to apply a trained model on the custom data or the online footage from a web camera and see how it will work in a real-life scenario. - `otx explain` runs explain algorithm on the provided data and outputs images with the saliency maps to show how your model makes predictions. -You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.2/guide/get_started/cli_commands.html). +You can find more details with examples in the [CLI command intro](https://openvinotoolkit.github.io/training_extensions/1.4.4/guide/get_started/cli_commands.html). --- diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 2699992177b..9e63a320237 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -4,6 +4,13 @@ Releases .. toctree:: :maxdepth: 1 +v1.4.4 (4Q23) +------------- + +- Update ModelAPI configuration +- Add Anomaly modelAPI changes +- Update Image numpy access + v1.4.3 (4Q23) ------------- From 0e09a19db7e1297b13949ebf1c9a443f49040bb9 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 15 Nov 2023 15:41:31 +0900 Subject: [PATCH 119/146] update release note and, changelog, and readme --- CHANGELOG.md | 12 +++++------ README.md | 26 +++++++++++++---------- docs/source/guide/release_notes/index.rst | 22 +++++++++++++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15ccc7979b..7c8a8dc855d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,18 @@ All notable changes to this project will be documented in this file. -## \[v1.5.0 - unreleased\] +## \[v1.5.0\] ### New features -- Enable configurable confidence threshold for otx eval and export() +- Enable configurable confidence threshold for otx eval and export () - Add YOLOX variants as new object detector models () -- Enable FeatureVectorHook to support action tasks() +- Enable FeatureVectorHook to support action tasks () - Add ONNX metadata to detection, instance segmantation, and segmentation models () -- Add a new feature to configure input size() +- Add a new feature to configure input size () - Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () -- Add a new object detector Lite-DINO() -- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task() +- Add a new object detector Lite-DINO () +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () - Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () - Add new argument to track resource usage in train command () - Add Self-SL for semantic segmentation of SegNext families () diff --git a/README.md b/README.md index 6bebbe793b2..c78b75cb508 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ OpenVINO™ Training Extensions supports the following computer vision tasks: - **Action recognition** including action classification and detection - **Anomaly recognition** tasks including anomaly classification, detection and segmentation -OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/1.5.0/guide/explanation/algorithms/index.html): +OpenVINO™ Training Extensions supports the [following learning methods](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/algorithms/index.html): - **Supervised**, incremental training, which includes class incremental scenario and contrastive learning for classification and semantic segmentation tasks - **Semi-supervised learning** @@ -97,16 +97,20 @@ You can find more details with examples in the [CLI command intro](https://openv ## Updates -### v1.4.0 (3Q23) - -- Support encrypted dataset training () -- Add custom max iou assigner to prevent CPU OOM when large annotations are used () -- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional () -- Add per-class XAI saliency maps for Mask R-CNN model () -- Add new object detector Deformable DETR () -- Add new object detector DINO () -- Add new visual prompting task (, , , , ) -- Add new object detector ResNeXt101-ATSS () +### v1.5.0 (4Q23) + +- Enable configurable confidence threshold for otx eval and export () +- Add YOLOX variants as new object detector models () +- Enable FeatureVectorHook to support action tasks () +- Add ONNX metadata to detection, instance segmantation, and segmentation models () +- Add a new feature to configure input size () +- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () +- Add a new object detector Lite-DINO () +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () +- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () +- Add new argument to track resource usage in train command () +- Add Self-SL for semantic segmentation of SegNext families () +- Adapt input size automatically based on dataset statistics () ### Release History diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 9e63a320237..133b7350c9e 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -4,6 +4,28 @@ Releases .. toctree:: :maxdepth: 1 +v1.5.0 (4Q23) +------------- + +- Enable configurable confidence threshold for otx eval and export +- Add YOLOX variants as new object detector models +- Enable FeatureVectorHook to support action tasks +- Add ONNX metadata to detection, instance segmantation, and segmentation models +- Add a new feature to configure input size +- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime +- Add a new object detector Lite-DINO +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task +- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS +- Add new argument to track resource usage in train command +- Add Self-SL for semantic segmentation of SegNext families +- Adapt input size automatically based on dataset statistics +- Refine input data in-memory caching +- Adapt timeout value of initialization for distributed training +- Optimize data loading by merging load & resize operations w/ caching support for cls/det/iseg/sseg +- Support torch==2.0.1 +- Set "Auto" as default input size mode + + v1.4.4 (4Q23) ------------- From dd84a241dad5a6c020253797c602298150ffcd9e Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 16 Nov 2023 16:34:33 +0900 Subject: [PATCH 120/146] update version string to 1.6.0dev --- CHANGELOG.md | 2 ++ src/otx/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8a8dc855d..9953ee7d95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. +## \[unreleased\] + ## \[v1.5.0\] ### New features diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 16aa2ab6d12..ce673d0b5d0 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.5.0" +__version__ = "1.6.0dev" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release From 202c193226cae04841c0e10e4f58b47598858a17 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Thu, 16 Nov 2023 16:44:38 +0900 Subject: [PATCH 121/146] fix datumaro version to 1.6.0rc0 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index f1f7314df86..538912b36cc 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ natsort==8.1.* prettytable==3.9.* protobuf==3.20.* pyyaml -datumaro~=1.5.1rc4 +datumaro~=1.6.0rc0 psutil==5.9.* scipy==1.10.* bayesian-optimization==1.4.* From 2f67686103df873d020681f6d504f9595ce4a963 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 17 Nov 2023 09:16:32 +0900 Subject: [PATCH 122/146] Mergeback 1.5.0 to develop (#2642) * Update publish workflow for tag checking (#2632) * Update e2e tests for XAI Detection (#2634) * Disable QAT for newly added models (#2636) * Update release note and readme (#2637) * remove package upload step on internal publish wf * update release note and, changelog, and readme * update version string to 1.6.0dev --------- Co-authored-by: Galina Zalesskaya Co-authored-by: Jaeguk Hyun --- .github/workflows/publish_internal.yml | 19 +++++------- CHANGELOG.md | 14 +++++---- README.md | 30 +++++++++++-------- docs/source/guide/release_notes/index.rst | 29 ++++++++++++++++++ requirements/base.txt | 2 +- src/otx/__init__.py | 2 +- .../cspdarknet_yolox_l/template.yaml | 1 - .../cspdarknet_yolox_s/template.yaml | 1 - .../cspdarknet_yolox_x/template.yaml | 1 - .../detection/resnext101_atss/template.yaml | 1 - .../template_experimental.yaml | 1 - .../maskrcnn_swin_t/template.yaml | 1 - .../test_api_xai_sanity_detection.py | 2 +- 13 files changed, 64 insertions(+), 40 deletions(-) diff --git a/.github/workflows/publish_internal.yml b/.github/workflows/publish_internal.yml index 2d5d14f1601..800cc2c60ac 100644 --- a/.github/workflows/publish_internal.yml +++ b/.github/workflows/publish_internal.yml @@ -2,8 +2,6 @@ name: Build and upload to internal PyPI on: workflow_dispatch: # run on request (no need for PR) - release: - types: [published] jobs: build_wheels: @@ -61,23 +59,20 @@ jobs: uses: actions-ecosystem/action-regex-match@v2 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@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/* - tag: ${{ github.ref }} - overwrite: true - file_glob: true + 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.TESTPYPI_API_TOKEN }} - name: Clean up dist if: ${{ always() }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index c15ccc7979b..9953ee7d95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,20 @@ All notable changes to this project will be documented in this file. -## \[v1.5.0 - unreleased\] +## \[unreleased\] + +## \[v1.5.0\] ### New features -- Enable configurable confidence threshold for otx eval and export() +- Enable configurable confidence threshold for otx eval and export () - Add YOLOX variants as new object detector models () -- Enable FeatureVectorHook to support action tasks() +- Enable FeatureVectorHook to support action tasks () - Add ONNX metadata to detection, instance segmantation, and segmentation models () -- Add a new feature to configure input size() +- Add a new feature to configure input size () - Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () -- Add a new object detector Lite-DINO() -- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task() +- Add a new object detector Lite-DINO () +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () - Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () - Add new argument to track resource usage in train command () - Add Self-SL for semantic segmentation of SegNext families () diff --git a/README.md b/README.md index 665dc2ae5bc..e8e3f424eeb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you are an experienced user, you can configure your own model based on [torch Furthermore, OpenVINO™ Training Extensions provides automatic configuration for ease of use. The framework will analyze your dataset and identify the most suitable model and figure out the best input size setting and other hyper-parameters. -The development team is continuously extending this [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/auto_configuration.html) functionalities to make training as simple as possible so that single CLI command can obtain accurate, efficient and robust models ready to be integrated into your project. +The development team is continuously extending this [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/auto_configuration.html) functionalities to make training as simple as possible so that single CLI command can obtain accurate, efficient and robust models ready to be integrated into your project. ### Key Features @@ -63,11 +63,11 @@ OpenVINO™ Training Extensions supports the [following learning methods](https: OpenVINO™ Training Extensions provides the following usability features: -- [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model with appropriate input size to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. +- [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model with appropriate input size to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. - [Datumaro](https://openvinotoolkit.github.io/datumaro/stable/index.html) data frontend: OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We are constantly working to extend supported formats to give more freedom of datasets format choice. - **Distributed training** to accelerate the training process when you have multiple GPUs - **Mixed-precision training** to save GPUs memory and use larger batch sizes -- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. +- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. --- @@ -97,16 +97,20 @@ You can find more details with examples in the [CLI command intro](https://openv ## Updates -### v1.4.0 (3Q23) - -- Support encrypted dataset training () -- Add custom max iou assigner to prevent CPU OOM when large annotations are used () -- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional () -- Add per-class XAI saliency maps for Mask R-CNN model () -- Add new object detector Deformable DETR () -- Add new object detector DINO () -- Add new visual prompting task (, , , , ) -- Add new object detector ResNeXt101-ATSS () +### v1.5.0 (4Q23) + +- Enable configurable confidence threshold for otx eval and export () +- Add YOLOX variants as new object detector models () +- Enable FeatureVectorHook to support action tasks () +- Add ONNX metadata to detection, instance segmantation, and segmentation models () +- Add a new feature to configure input size () +- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () +- Add a new object detector Lite-DINO () +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () +- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () +- Add new argument to track resource usage in train command () +- Add Self-SL for semantic segmentation of SegNext families () +- Adapt input size automatically based on dataset statistics () ### Release History diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 2699992177b..133b7350c9e 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -4,6 +4,35 @@ Releases .. toctree:: :maxdepth: 1 +v1.5.0 (4Q23) +------------- + +- Enable configurable confidence threshold for otx eval and export +- Add YOLOX variants as new object detector models +- Enable FeatureVectorHook to support action tasks +- Add ONNX metadata to detection, instance segmantation, and segmentation models +- Add a new feature to configure input size +- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime +- Add a new object detector Lite-DINO +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task +- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS +- Add new argument to track resource usage in train command +- Add Self-SL for semantic segmentation of SegNext families +- Adapt input size automatically based on dataset statistics +- Refine input data in-memory caching +- Adapt timeout value of initialization for distributed training +- Optimize data loading by merging load & resize operations w/ caching support for cls/det/iseg/sseg +- Support torch==2.0.1 +- Set "Auto" as default input size mode + + +v1.4.4 (4Q23) +------------- + +- Update ModelAPI configuration +- Add Anomaly modelAPI changes +- Update Image numpy access + v1.4.3 (4Q23) ------------- diff --git a/requirements/base.txt b/requirements/base.txt index e284377021d..538912b36cc 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,5 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -# Base Algo Requirements. # +# Base Algo Requirements. # natsort==8.1.* prettytable==3.9.* protobuf==3.20.* diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 16aa2ab6d12..ce673d0b5d0 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.5.0" +__version__ = "1.6.0dev" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release 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 index 83a90ecfcca..f06013bba10 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 0c94a081ce3..335b07f8099 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 50e07835a96..1fdf665d533 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml index 27fe398fd1b..79308f5388a 100644 --- a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index 6baef6921d3..44821039d86 100644 --- 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 @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: 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 index ed02d0f7e9f..61f359406e9 100644 --- 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 @@ -14,7 +14,6 @@ framework: OTXDetection v2.9.1 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: diff --git a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py index 02024d54bfd..4cd11fa937c 100644 --- a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py +++ b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py @@ -32,7 +32,7 @@ class TestOVDetXAIAPI(DetectionTaskAPIBase): ref_raw_saliency_shapes = { - "MobileNetV2-ATSS": (4, 4), # Need to be adapted to configurable or adaptive input size + "MobileNetV2-ATSS": (16, 16), # Need to be adapted to configurable or adaptive input size } @e2e_pytest_api From ddd73f2651687f6c2a723bc57cf05607e6a39b84 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 17 Nov 2023 09:20:40 +0900 Subject: [PATCH 123/146] Revert "Mergeback 1.5.0 to develop" (#2645) Revert "Mergeback 1.5.0 to develop (#2642)" This reverts commit 2f67686103df873d020681f6d504f9595ce4a963. --- .github/workflows/publish_internal.yml | 19 +++++++----- CHANGELOG.md | 14 ++++----- README.md | 30 ++++++++----------- docs/source/guide/release_notes/index.rst | 29 ------------------ requirements/base.txt | 2 +- src/otx/__init__.py | 2 +- .../cspdarknet_yolox_l/template.yaml | 1 + .../cspdarknet_yolox_s/template.yaml | 1 + .../cspdarknet_yolox_x/template.yaml | 1 + .../detection/resnext101_atss/template.yaml | 1 + .../template_experimental.yaml | 1 + .../maskrcnn_swin_t/template.yaml | 1 + .../test_api_xai_sanity_detection.py | 2 +- 13 files changed, 40 insertions(+), 64 deletions(-) diff --git a/.github/workflows/publish_internal.yml b/.github/workflows/publish_internal.yml index 800cc2c60ac..2d5d14f1601 100644 --- a/.github/workflows/publish_internal.yml +++ b/.github/workflows/publish_internal.yml @@ -2,6 +2,8 @@ name: Build and upload to internal PyPI on: workflow_dispatch: # run on request (no need for PR) + release: + types: [published] jobs: build_wheels: @@ -59,20 +61,23 @@ jobs: uses: actions-ecosystem/action-regex-match@v2 with: text: ${{ github.ref }} - regex: '^refs/heads/releases/[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+rc[0-9]+|rc[0-9]+)?$' + 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@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/* + tag: ${{ github.ref }} + overwrite: true + file_glob: true - 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.TESTPYPI_API_TOKEN }} - name: Clean up dist if: ${{ always() }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9953ee7d95b..c15ccc7979b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,18 @@ All notable changes to this project will be documented in this file. -## \[unreleased\] - -## \[v1.5.0\] +## \[v1.5.0 - unreleased\] ### New features -- Enable configurable confidence threshold for otx eval and export () +- Enable configurable confidence threshold for otx eval and export() - Add YOLOX variants as new object detector models () -- Enable FeatureVectorHook to support action tasks () +- Enable FeatureVectorHook to support action tasks() - Add ONNX metadata to detection, instance segmantation, and segmentation models () -- Add a new feature to configure input size () +- Add a new feature to configure input size() - Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () -- Add a new object detector Lite-DINO () -- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () +- Add a new object detector Lite-DINO() +- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task() - Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () - Add new argument to track resource usage in train command () - Add Self-SL for semantic segmentation of SegNext families () diff --git a/README.md b/README.md index e8e3f424eeb..665dc2ae5bc 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you are an experienced user, you can configure your own model based on [torch Furthermore, OpenVINO™ Training Extensions provides automatic configuration for ease of use. The framework will analyze your dataset and identify the most suitable model and figure out the best input size setting and other hyper-parameters. -The development team is continuously extending this [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/auto_configuration.html) functionalities to make training as simple as possible so that single CLI command can obtain accurate, efficient and robust models ready to be integrated into your project. +The development team is continuously extending this [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/auto_configuration.html) functionalities to make training as simple as possible so that single CLI command can obtain accurate, efficient and robust models ready to be integrated into your project. ### Key Features @@ -63,11 +63,11 @@ OpenVINO™ Training Extensions supports the [following learning methods](https: OpenVINO™ Training Extensions provides the following usability features: -- [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model with appropriate input size to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. +- [Auto-configuration](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/auto_configuration.html). OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model with appropriate input size to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. - [Datumaro](https://openvinotoolkit.github.io/datumaro/stable/index.html) data frontend: OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. We are constantly working to extend supported formats to give more freedom of datasets format choice. - **Distributed training** to accelerate the training process when you have multiple GPUs - **Mixed-precision training** to save GPUs memory and use larger batch sizes -- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/stable/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. +- Integrated, efficient [hyper-parameter optimization module (HPO)](https://openvinotoolkit.github.io/training_extensions/latest/guide/explanation/additional_features/hpo.html). Through dataset proxy and built-in hyper-parameter optimizer, you can get much faster hyper-parameter optimization compared to other off-the-shelf tools. The hyperparameter optimization is dynamically scheduled based on your resource budget. --- @@ -97,20 +97,16 @@ You can find more details with examples in the [CLI command intro](https://openv ## Updates -### v1.5.0 (4Q23) - -- Enable configurable confidence threshold for otx eval and export () -- Add YOLOX variants as new object detector models () -- Enable FeatureVectorHook to support action tasks () -- Add ONNX metadata to detection, instance segmantation, and segmentation models () -- Add a new feature to configure input size () -- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime () -- Add a new object detector Lite-DINO () -- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task () -- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS () -- Add new argument to track resource usage in train command () -- Add Self-SL for semantic segmentation of SegNext families () -- Adapt input size automatically based on dataset statistics () +### v1.4.0 (3Q23) + +- Support encrypted dataset training () +- Add custom max iou assigner to prevent CPU OOM when large annotations are used () +- Auto train type detection for Semi-SL, Self-SL and Incremental: "--train-type" now is optional () +- Add per-class XAI saliency maps for Mask R-CNN model () +- Add new object detector Deformable DETR () +- Add new object detector DINO () +- Add new visual prompting task (, , , , ) +- Add new object detector ResNeXt101-ATSS () ### Release History diff --git a/docs/source/guide/release_notes/index.rst b/docs/source/guide/release_notes/index.rst index 133b7350c9e..2699992177b 100644 --- a/docs/source/guide/release_notes/index.rst +++ b/docs/source/guide/release_notes/index.rst @@ -4,35 +4,6 @@ Releases .. toctree:: :maxdepth: 1 -v1.5.0 (4Q23) -------------- - -- Enable configurable confidence threshold for otx eval and export -- Add YOLOX variants as new object detector models -- Enable FeatureVectorHook to support action tasks -- Add ONNX metadata to detection, instance segmantation, and segmentation models -- Add a new feature to configure input size -- Introduce the OTXSampler and AdaptiveRepeatDataHook to achieve faster training at the small data regime -- Add a new object detector Lite-DINO -- Add Semi-SL Mean Teacher algorithm for Instance Segmentation task -- Official supports for YOLOX-X, YOLOX-L, YOLOX-S, ResNeXt101-ATSS -- Add new argument to track resource usage in train command -- Add Self-SL for semantic segmentation of SegNext families -- Adapt input size automatically based on dataset statistics -- Refine input data in-memory caching -- Adapt timeout value of initialization for distributed training -- Optimize data loading by merging load & resize operations w/ caching support for cls/det/iseg/sseg -- Support torch==2.0.1 -- Set "Auto" as default input size mode - - -v1.4.4 (4Q23) -------------- - -- Update ModelAPI configuration -- Add Anomaly modelAPI changes -- Update Image numpy access - v1.4.3 (4Q23) ------------- diff --git a/requirements/base.txt b/requirements/base.txt index 538912b36cc..e284377021d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,5 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -# Base Algo Requirements. # +# Base Algo Requirements. # natsort==8.1.* prettytable==3.9.* protobuf==3.20.* diff --git a/src/otx/__init__.py b/src/otx/__init__.py index ce673d0b5d0..16aa2ab6d12 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.6.0dev" +__version__ = "1.5.0" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release 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 index f06013bba10..83a90ecfcca 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_l/template.yaml @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: 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 index 335b07f8099..0c94a081ce3 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_s/template.yaml @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: 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 index 1fdf665d533..50e07835a96 100644 --- a/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/cspdarknet_yolox_x/template.yaml @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: diff --git a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml index 79308f5388a..27fe398fd1b 100644 --- a/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml +++ b/src/otx/algorithms/detection/configs/detection/resnext101_atss/template.yaml @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: 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 index 44821039d86..6baef6921d3 100644 --- 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 @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: 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 index 61f359406e9..ed02d0f7e9f 100644 --- 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 @@ -14,6 +14,7 @@ framework: OTXDetection v2.9.1 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: diff --git a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py index 4cd11fa937c..02024d54bfd 100644 --- a/tests/e2e/cli/detection/test_api_xai_sanity_detection.py +++ b/tests/e2e/cli/detection/test_api_xai_sanity_detection.py @@ -32,7 +32,7 @@ class TestOVDetXAIAPI(DetectionTaskAPIBase): ref_raw_saliency_shapes = { - "MobileNetV2-ATSS": (16, 16), # Need to be adapted to configurable or adaptive input size + "MobileNetV2-ATSS": (4, 4), # Need to be adapted to configurable or adaptive input size } @e2e_pytest_api From 9cf5624caad7f1b559b49257fc195379440e4480 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Mon, 20 Nov 2023 11:04:13 +0900 Subject: [PATCH 124/146] Add a tool to help conduct experiments (#2651) * implement run and experiment * implement experiment result aggregator * refactor experiment.py * refactor run.py * get export model speed * add var collumn * refactor experiment.py * refine a way to update argument in cmd * refine resource tracker * support anomaly on research framework * refine code aggregating exp result * bugfix * make other task available * eval task save avg_time_per_images as result * Add new argument to track CPU&GPU utilization and memory usage (#2500) * add argument to track resource usage * fix bug * fix a bug in a multi gpu case * use total cpu usage * add unit test * add mark to unit test * cover edge case * add pynvml in requirement * align with pre-commit * add license comment * update changelog * refine argument help * align with pre-commit * add version to requirement and raise an error if not supported values are given * apply new resource tracker format * refactor run.py * support optimize in research framework * cover edge case * Handle a case where fail cases exist * make argparse raise error rather than exit if problem exist * revert tensorboard aggregator * bugfix * save failed cases as yaml file * deal with integer in variables * add epoch to metric * use latest log.json file * align with otx logging method * move experiment.py from cli to tools * refactor experiment.py * merge otx run feature into experiment.py * move set_arguments_to_cmd definition into experiment.py * refactor experiment.py * bugfix * minor bugfix * use otx.cli instead of each otx entry * add feature to parse single workspace * add comments * fix bugs * align with pre-commit * revert parser argument * align with pre-commit --- .../classification/adapters/openvino/task.py | 9 +- .../detection/adapters/openvino/task.py | 9 +- .../segmentation/adapters/openvino/task.py | 9 +- .../visual_prompting/tasks/openvino.py | 9 +- src/otx/cli/tools/eval.py | 8 +- tools/experiment.py | 791 ++++++++++++++++++ 6 files changed, 827 insertions(+), 8 deletions(-) create mode 100644 tools/experiment.py diff --git a/src/otx/algorithms/classification/adapters/openvino/task.py b/src/otx/algorithms/classification/adapters/openvino/task.py index 0fd2d822414..2c87e96a3c5 100644 --- a/src/otx/algorithms/classification/adapters/openvino/task.py +++ b/src/otx/algorithms/classification/adapters/openvino/task.py @@ -176,6 +176,12 @@ def __init__(self, task_environment: TaskEnvironment): 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.""" @@ -270,7 +276,8 @@ def add_prediction(id: int, predicted_scene: AnnotationSceneEntity, aux_data: tu self.inferencer.await_all() - logger.info(f"Avg time per image: {total_time/len(dataset)} secs") + 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") diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index af0769dd028..a0e7eb9998c 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -387,6 +387,7 @@ def __init__(self, task_environment: TaskEnvironment): 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 @@ -394,6 +395,11 @@ 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. @@ -557,7 +563,8 @@ def add_prediction(id: int, predicted_scene: AnnotationSceneEntity, aux_data: tu self.inferencer.await_all() - logger.info(f"Avg time per image: {total_time/len(dataset)} secs") + 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 diff --git a/src/otx/algorithms/segmentation/adapters/openvino/task.py b/src/otx/algorithms/segmentation/adapters/openvino/task.py index df33e99101c..0a4e9693192 100644 --- a/src/otx/algorithms/segmentation/adapters/openvino/task.py +++ b/src/otx/algorithms/segmentation/adapters/openvino/task.py @@ -162,6 +162,7 @@ def __init__(self, task_environment: TaskEnvironment): 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)) @@ -173,6 +174,11 @@ 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: @@ -248,7 +254,8 @@ def add_prediction( self.inferencer.await_all() - logger.info(f"Avg time per image: {total_time/len(dataset)} secs") + 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") diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index 7e133438bc1..fe499300970 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -258,6 +258,7 @@ def __init__(self, task_environment: TaskEnvironment) -> None: 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)) @@ -270,6 +271,11 @@ 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: @@ -328,7 +334,8 @@ def add_prediction(id: int, annotations: List[Annotation]): self.inferencer.await_all() - logger.info(f"Avg time per image: {total_time/len(dataset)} secs") + 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") diff --git a/src/otx/cli/tools/eval.py b/src/otx/cli/tools/eval.py index 5cf59156e48..2ed3b22a477 100644 --- a/src/otx/cli/tools/eval.py +++ b/src/otx/cli/tools/eval.py @@ -156,11 +156,11 @@ def main(): print(resultset.performance) output_path = Path(args.output) if args.output else config_manager.output_path + performance = {resultset.performance.score.name: resultset.performance.score.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( - {resultset.performance.score.name: resultset.performance.score.value}, - write_file, - ) + json.dump(performance, write_file) return dict(retcode=0, template=template.name) diff --git a/tools/experiment.py b/tools/experiment.py new file mode 100644 index 00000000000..eb9f19a1641 --- /dev/null +++ b/tools/experiment.py @@ -0,0 +1,791 @@ +"""OTX experiment helper.""" +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import argparse +import csv +import dataclasses +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, Union + +import yaml +from otx.cli.tools.cli import main as otx_cli +from rich.console import Console +from rich.table import Table + + +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.") + 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) -> 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. + + 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).glob(file_name), reverse=True, key=lambda x: x.stat().st_mtime) + if not train_record_files: + return None + return train_record_files[0] + + +@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 + test_score: Union[float, 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_model_score: Union[float, None] = None + avg_ov_infer_time: Union[float, 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_model_score: Union[float, None] = None + epoch: Union[int, None] = None + + def get_formatted_result(self) -> Dict: + """Return dictionary format result.""" + result = dataclasses.asdict(self) + + for attr_name in ["max_cpu_mem", "max_gpu_mem"]: + max_mem = result.pop(attr_name) + result[f"{attr_name}(GiB)"] = max_mem + + for attr_name in ["avg_cpu_util", "avg_gpu_util"]: + res_util = result.pop(attr_name) + result[f"{attr_name}(%)"] = res_util + + if self.train_e2e_time is not None: + result["train_e2e_time"] = str(self.train_e2e_time).split(".")[0] + + # delete None value + for key in list(result.keys()): + if result[key] is None: + del result[key] + elif isinstance(result[key], float): + result[key] = round(result[key], 4) + + return 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") + for key, val in formatted_dict.items(): + max_mem_name = max_mem_pat.search(key) + cpu_util_name = cpu_util_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 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 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) + + def _parse_eval_output(self, file_path: Path): + # NOTE: It is assumed that performance.json has key named either score or avg_time_per_image + with file_path.open("r") as f: + eval_output: Dict = json.load(f) + + if "train" in str(file_path.parent.name): + self._exp_result.test_score = list(eval_output.values())[0] + elif "export" in str(file_path.parent.name): + for key, val in eval_output.items(): + if key == "avg_time_per_image": + self._exp_result.avg_ov_infer_time = val + else: + self._exp_result.export_model_score = val + elif "optimize" in str(file_path.parent.name): + self._exp_result.optimize_model_score = list(eval_output.values())[0] + + 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 "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 + for line in lines: + iter_history = json.loads(line) + if iter_history.get("mode") == "train": + self._iter_time_arr.append(iter_history["time"]) + self._data_time_arr.append(iter_history["data_time"]) + if iter_history["epoch"] > last_epoch: + last_epoch = iter_history["epoch"] + + 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 "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]) + + # 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 get_exp_parser(workspace: Path) -> BaseExpParser: + """Get experiment parser depending on framework. + + Args: + workspace (Path): Workspace to parse. + + Returns: + BaseExpParser: Experiment parser. + """ + with (workspace / "template.yaml").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) + 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". + """ + # print experiment summary to console + table = Table(title=table_title) + for header in headers: + table.add_column(header, justify="center", no_wrap=True) + for each_exp_result_summary in rows: + table_row = [] + for header in headers: + val = each_exp_result_summary.get(header) + table_row.append(str(val)) + + table.add_row(*table_row) + + console = Console() + console.print(table) + + +def aggregate_all_exp_result(exp_dir: Union[str, Path]): + """Aggregate all experiment results. + + 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) + + print_table(headers, rows, "Experiment Summary") + + +@dataclass +class Command: + """Command dataclass.""" + + command: List[str] + variable: Dict[str, str] = field(default_factory=dict) + + +class ExpRecipeParser: + """Class to parse an experiment recipe. + + Args: + recipe_file (Union[str, Path]): Recipe file to parse. + """ + + def __init__(self, recipe_file: Union[str, Path]): + if not os.path.exists(recipe_file): + raise RuntimeError(f"{recipe_file} doesn't exist.") + + with open(recipe_file, "r") as f: + self._exp_recipe: Dict = yaml.safe_load(f) + constants = self._exp_recipe.get("constants", {}) + self._cvt_number_to_str(constants) + self._constants: Dict[str, str] = constants + self._variables: Optional[Dict[str, str]] = None + self._commands: Optional[List[Command]] = None + self.output_path: Path = Path( + self._exp_recipe.get("output_path", f"experiment_{datetime.now().strftime('%Y%m%d_%H%M%S')}") + ) + self.repeat: int = self._exp_recipe.get("repeat", 1) + 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: + variables = self._exp_recipe.get("variables", {}) + self._cvt_number_to_str(variables) + self._variables = self._replace_var_in_target(self.constants, 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._exp_recipe.get("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)] + + 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 = [] + for each_str in target_str: + found_keys.extend([x for x in set(self._replace_pat.findall(each_str)) if x in variable]) + if not found_keys: + return [] + + 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): + for i in range(len(target)): + target[i] = self._replace_var_in_target(variable, target[i]) + elif isinstance(target, dict): + 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 + + @staticmethod + def _cvt_number_to_str(target: Dict): + """Convert int or float in dict to string.""" + for key, val in target.items(): + if isinstance(val, (int, float)): + target[key] = str(val) + elif isinstance(val, list): + for i in range(len(val)): + if isinstance(val[i], (int, float)): + val[i] = str(val[i]) + + +@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_fail_cases(fail_cases: List[CommandFailInfo], output_path: Path): + """Print fail cases and save it as a file. + + Args: + fail_cases (List[CommandFailInfo]): False cases. + output_path (Path): Where fale cases are saved. + """ + console = Console() + console.rule("[bold red]List of failed cases") + for each_fail_case in fail_cases: + console.print(f"Case : {each_fail_case.variable}", crop=False) + console.print(f"command : {each_fail_case.command}", crop=False) + console.print("Error log:", str(each_fail_case.exception), crop=False) + console.print() + console.rule() + + with (output_path / "failed_cases.yaml").open("w") as f: + yaml.safe_dump([fail_case.get_formatted_result() for fail_case in fail_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 = {"export": "openvino.bin", "optimize": "weights.pth"} + + def __init__(self, command_ins: Command, repeat_idx: int): + self._command_ins = command_ins + self._repeat_idx = repeat_idx + self._command_var = copy(command_ins.variable) + self._workspace = Path("_".join(self._command_var.values()).replace("/", "_") + f"_repeat_{repeat_idx}") + self._command_var["repeat"] = str(repeat_idx) + self._fail_logs: List[CommandFailInfo] = [] + self._previous_cmd_entry: Optional[str] = None + + @property + def fail_logs(self) -> List[CommandFailInfo]: + """Information of all failed cases.""" + return self._fail_logs + + def run_command_list(self): + """Run all commands and organize experiment results.""" + for command in self._command_ins.command: + command = command.split() + if not self._prepare_run_command(command): + print(f"otx {command[1]} is skipped.") + continue + + self._run_otx_command(command) + + self._previous_cmd_entry = command[1] + + 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] + if cmd_entry == "train": + self.set_arguments_to_cmd(command, "--seed", str(self._repeat_idx)) + elif cmd_entry == "eval": + if self._previous_cmd_entry in self.OUTPUT_FILE_NAME: + file_path = self._find_model_path(self._previous_cmd_entry) + 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) + + 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 + file_path = list(output_dir.rglob(self.OUTPUT_FILE_NAME[cmd_entry])) + if not file_path: + print(f"{self.OUTPUT_FILE_NAME[cmd_entry]} can't be found.") + return None + return file_path[0] + + @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_recipe(recipe_file: Union[str, Path]): + """Run experiments based on the recipe. + + Args: + recipe_file (Union[str, Path]): Recipe file to run. + """ + exp_recipe = ExpRecipeParser(recipe_file) + output_path = exp_recipe.output_path + output_path.mkdir(exist_ok=True) + current_dir = os.getcwd() + os.chdir(output_path) + + fail_cases: List[CommandFailInfo] = [] + for command_ins in exp_recipe.commands: + for repeat_idx in range(exp_recipe.repeat): + otx_cmd_runner = OtxCommandRunner(command_ins, repeat_idx) + otx_cmd_runner.run_command_list() + fail_cases.extend(otx_cmd_runner.fail_logs) + + os.chdir(current_dir) + + if fail_cases: + log_fail_cases(fail_cases, output_path) + + aggregate_all_exp_result(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) + elif args.parse is not None: + organize_exp_result(args.parse) + else: + parser.print_help() + + +if __name__ == "__main__": + main() From a0780a8cf36cb4bd924c0307134ccffbd6d8f240 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 20 Nov 2023 13:30:58 +0900 Subject: [PATCH 125/146] Make `max_num_detections` configurable (#2647) * Make max_num_detections configurable * Fix RCNN case with integration test * Apply max_num_detections to train_cfg, too --------- Signed-off-by: Songki Choi --- CHANGELOG.md | 1 + .../common/configs/training_base.py | 25 +++++++++---------- .../detection/adapters/mmdet/configurer.py | 22 ++++++++++++++-- .../detection/adapters/mmdet/task.py | 20 ++++++--------- .../detection/adapters/openvino/task.py | 15 ++--------- .../detection/configs/base/configuration.py | 15 ++--------- .../configs/detection/configuration.yaml | 19 ++++++++++++++ .../instance_segmentation/configuration.yaml | 19 ++++++++++++++ .../convnext_maskrcnn/model.py | 4 +-- .../resnet50_maskrcnn/model.py | 17 +++---------- .../rotated_detection/configuration.yaml | 19 ++++++++++++++ .../resnet50_maskrcnn/model.py | 2 +- src/otx/algorithms/detection/task.py | 24 ++++++++---------- .../cli/detection/test_detection.py | 10 +++++++- .../cli/detection/test_tiling_detection.py | 4 ++- .../test_instance_segmentation.py | 10 +++++++- .../test_tiling_instseg.py | 4 ++- .../adapters/mmdet/test_configurer.py | 6 +++-- 18 files changed, 145 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9116e8b6adb..0746c1f194b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Update ModelAPI configuration() - Add Anomaly modelAPI changes () - Update Image numpy access () +- Make max_num_detections configurable () ### Bug fixes diff --git a/src/otx/algorithms/common/configs/training_base.py b/src/otx/algorithms/common/configs/training_base.py index 40f9b7a7529..6690b6e1c3e 100644 --- a/src/otx/algorithms/common/configs/training_base.py +++ b/src/otx/algorithms/common/configs/training_base.py @@ -1,18 +1,7 @@ """Base Configuration 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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 from sys import maxsize @@ -227,6 +216,16 @@ class BasePostprocessing(ParameterGroup): 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", diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index 0e4d63b1b99..3f1b624954e 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -64,13 +64,14 @@ def configure( ir_options=None, data_classes=None, model_classes=None, + max_num_detections=0, ): """Create MMCV-consumable config from given inputs.""" logger.info(f"configure!: training={training}") self.configure_base(cfg, data_cfg, data_classes, model_classes) self.configure_device(cfg, training) - self.configure_model(cfg, ir_options) + self.configure_model(cfg, ir_options, max_num_detections) self.configure_ckpt(cfg, model_ckpt) self.configure_data(cfg, training, data_cfg) self.configure_regularization(cfg, training) @@ -113,7 +114,7 @@ def configure_base(self, cfg, data_cfg, data_classes, model_classes): new_classes = np.setdiff1d(data_classes, model_classes).tolist() train_data_cfg["new_classes"] = new_classes - def configure_model(self, cfg, ir_options): # noqa: C901 + def configure_model(self, cfg, ir_options, max_num_detections=0): # noqa: C901 """Patch config's model. Change model type to super type @@ -149,6 +150,23 @@ def is_mmov_model(key, value): {"model_path": ir_model_path, "weight_path": ir_weight_path, "init_weight": ir_weight_init}, ) + # Test config + 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_data(self, cfg, training, data_cfg): # noqa: C901 """Patch cfg.data. diff --git a/src/otx/algorithms/detection/adapters/mmdet/task.py b/src/otx/algorithms/detection/adapters/mmdet/task.py index 592a63e4eb8..e1c229630b2 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/task.py +++ b/src/otx/algorithms/detection/adapters/mmdet/task.py @@ -1,18 +1,7 @@ """Task of OTX Detection using mmdetection 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. +# SPDX-License-Identifier: Apache-2.0 import glob import io @@ -206,6 +195,7 @@ def configure(self, training=True, subset="train", ir_options=None, train_datase ir_options, data_classes, model_classes, + self.max_num_detections, ) if should_cluster_anchors(self._recipe_cfg): if train_dataset is not None: @@ -513,6 +503,12 @@ def _export_model( 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: diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index 08b5423eef2..1a8376e453e 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -1,18 +1,7 @@ """Openvino Task of 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. +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 import copy import io diff --git a/src/otx/algorithms/detection/configs/base/configuration.py b/src/otx/algorithms/detection/configs/base/configuration.py index 0e258f7ed8c..c2481a1633b 100644 --- a/src/otx/algorithms/detection/configs/base/configuration.py +++ b/src/otx/algorithms/detection/configs/base/configuration.py @@ -1,18 +1,7 @@ """Configuration file 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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 from attr import attrs diff --git a/src/otx/algorithms/detection/configs/detection/configuration.yaml b/src/otx/algorithms/detection/configs/detection/configuration.yaml index 2784be12db9..9172db3ff4e 100644 --- a/src/otx/algorithms/detection/configs/detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -258,6 +258,25 @@ postprocessing: 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 diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml index 34c63e88ae0..9b906969a51 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -258,6 +258,25 @@ postprocessing: 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 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 index e35799ed7e0..84d89dd682f 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py @@ -115,9 +115,7 @@ 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 - ), + rcnn=dict(score_thr=0.05, nms=dict(type="nms", iou_threshold=0.5), max_per_img=100, mask_thr_binary=0.5), ), ) 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 index 6832028e425..22a3bdf4b9e 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py @@ -1,18 +1,7 @@ """Model configuration of Resnet50-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. +# Copyright (C) 2022-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # pylint: disable=invalid-name @@ -149,7 +138,7 @@ ), rcnn=dict( score_thr=0.05, - nms=dict(type="nms", iou_threshold=0.5, max_num=100), + nms=dict(type="nms", iou_threshold=0.5), max_per_img=100, mask_thr_binary=0.5, ), diff --git a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml index aa1f41e9ec7..3b5059e0155 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -277,6 +277,25 @@ postprocessing: 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 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 index eee17a545c7..c406c97bd00 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py @@ -139,7 +139,7 @@ ), rcnn=dict( score_thr=0.05, - nms=dict(type="nms", iou_threshold=0.5, max_num=100), + nms=dict(type="nms", iou_threshold=0.5), max_per_img=100, mask_thr_binary=0.5, ), diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py index ee94af104c2..92fed6fc3c5 100644 --- a/src/otx/algorithms/detection/task.py +++ b/src/otx/algorithms/detection/task.py @@ -1,18 +1,7 @@ """Task 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. +# SPDX-License-Identifier: Apache-2.0 import io import os @@ -83,11 +72,13 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] ) 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 - else: - self.confidence_threshold = 0.0 + 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() @@ -112,6 +103,11 @@ def _load_postprocessing(self, model_data): 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 def _load_tiling_parameters(self, model_data): """Load tiling parameters from PyTorch model. diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index b9ba80011ba..a18625bcf43 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -36,7 +36,15 @@ "--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"], + "train_params": [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--postprocessing.max_num_detections", + "200", + ], } args_semisl = { diff --git a/tests/integration/cli/detection/test_tiling_detection.py b/tests/integration/cli/detection/test_tiling_detection.py index a37ab5c1b45..02ba75a8a9c 100644 --- a/tests/integration/cli/detection/test_tiling_detection.py +++ b/tests/integration/cli/detection/test_tiling_detection.py @@ -1,5 +1,5 @@ """Tests for MPA Class-Incremental Learning for object detection with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import os @@ -36,6 +36,8 @@ "1", "--tiling_parameters.enable_adaptive_params", "1", + "--postprocessing.max_num_detections", + "200", ], } diff --git a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py index c2ba3b9c3df..93133e09dd1 100644 --- a/tests/integration/cli/instance_segmentation/test_instance_segmentation.py +++ b/tests/integration/cli/instance_segmentation/test_instance_segmentation.py @@ -32,7 +32,15 @@ "--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"], + "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 diff --git a/tests/integration/cli/instance_segmentation/test_tiling_instseg.py b/tests/integration/cli/instance_segmentation/test_tiling_instseg.py index 5b8d528e3c0..d39b4be372e 100644 --- a/tests/integration/cli/instance_segmentation/test_tiling_instseg.py +++ b/tests/integration/cli/instance_segmentation/test_tiling_instseg.py @@ -1,5 +1,5 @@ """Tests for MPA Class-Incremental Learning for instance segmentation with OTX CLI""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import copy @@ -41,6 +41,8 @@ "1", "--tiling_parameters.enable_adaptive_params", "1", + "--postprocessing.max_num_detections", + "200", ], } diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index 38dada73c0a..a582a266111 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -43,10 +43,12 @@ def test_configure(self, mocker): model_cfg = copy.deepcopy(self.model_cfg) data_cfg = copy.deepcopy(self.data_cfg) - returned_value = self.configurer.configure(model_cfg, self.det_dataset, "", data_cfg, True) + returned_value = self.configurer.configure( + model_cfg, self.det_dataset, "", data_cfg, True, max_num_detections=100 + ) mock_cfg_base.assert_called_once_with(model_cfg, data_cfg, None, None) mock_cfg_device.assert_called_once_with(model_cfg, True) - mock_cfg_model.assert_called_once_with(model_cfg, None) + mock_cfg_model.assert_called_once_with(model_cfg, None, 100) mock_cfg_ckpt.assert_called_once_with(model_cfg, "") mock_cfg_regularization.assert_called_once_with(model_cfg, True) mock_cfg_task.assert_called_once_with(model_cfg, self.det_dataset, True) From 72cf37ca446c847c980b804ff37901e8d8ce3092 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 20 Nov 2023 13:31:59 +0900 Subject: [PATCH 126/146] Revert inference batch size to 1 for instance segmentation (#2648) Signed-off-by: Songki Choi --- .../convnext_maskrcnn/template_experimental.yaml | 2 +- .../efficientnetb2b_maskrcnn/template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 44821039d86..b12d8f7e5fb 100644 --- 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 @@ -28,7 +28,7 @@ hyper_parameters: default_value: 2 auto_hpo_state: POSSIBLE inference_batch_size: - default_value: 2 + default_value: 1 learning_rate: default_value: 0.001 auto_hpo_state: POSSIBLE 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 index 272a648c551..f825fbac61d 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/template.yaml @@ -29,7 +29,7 @@ hyper_parameters: default_value: 4 auto_hpo_state: POSSIBLE inference_batch_size: - default_value: 4 + default_value: 1 learning_rate: default_value: 0.015 auto_hpo_state: POSSIBLE From aceebdaa6dff910264a18aa301a52f4e4483f9ec Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 21 Nov 2023 09:57:20 +0900 Subject: [PATCH 127/146] Fix CPU training issue on non-CUDA system (#2655) Fix bug that auto adaptive batch size raises an error if CUDA isn't available (#2410) --------- Co-authored-by: Sungman Cho Co-authored-by: Eunwoo Shin --- .../common/adapters/mmcv/utils/automatic_bs.py | 5 +++++ .../common/adapters/mmcv/utils/test_automatic_bs.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py index c01cf8abc76..9b95d58195f 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/automatic_bs.py @@ -8,6 +8,7 @@ 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.algorithms.common.utils.logger import get_logger @@ -53,6 +54,10 @@ def adapt_batch_size(train_func: Callable, cfg, datasets: List, validate: bool = 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) 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 index f9e1eb7d231..d590b74faf2 100644 --- a/tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py +++ b/tests/unit/algorithms/common/adapters/mmcv/utils/test_automatic_bs.py @@ -109,6 +109,19 @@ def test_adapt_batch_size( 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): From 6b8729a33749d7ce44452d34e2d45f4ba8b25a99 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Tue, 21 Nov 2023 14:24:16 +0900 Subject: [PATCH 128/146] Remove unnecessary log while building a model (#2658) * revert logger in otx/algorithms/detection/adapters/mmdet/utils/builder.py * revert logger in otx/algorithms/classification/adapters/mmcls/utils/builder.py * make change more readable --- .../classification/adapters/mmcls/utils/builder.py | 10 +++++----- .../detection/adapters/mmdet/utils/builder.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py index 2ee660f270b..9496930cce5 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py +++ b/src/otx/algorithms/classification/adapters/mmcls/utils/builder.py @@ -8,11 +8,11 @@ import torch from mmcv.runner import load_checkpoint -from mmcv.utils import Config, ConfigDict +from mmcv.utils import Config, ConfigDict, get_logger -from otx.utils.logger import LEVEL, get_logger +from otx.utils.logger import LEVEL -logger = get_logger() +mmcls_logger = get_logger("mmcls") def build_classifier( @@ -35,9 +35,9 @@ def build_classifier( model_cfg = deepcopy(config.model) model = origin_build_classifier(model_cfg) - logger.setLevel("WARNING") + mmcls_logger.setLevel("WARNING") # make logger less verbose temporally model.init_weights() - logger.setLevel(LEVEL) + mmcls_logger.setLevel(LEVEL) model = model.to(device) checkpoint = checkpoint if checkpoint else config.pop("load_from", None) diff --git a/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py index 91ee5e26861..c2e1ee54db2 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/builder.py @@ -8,11 +8,11 @@ import torch from mmcv.runner import load_checkpoint -from mmcv.utils import Config, ConfigDict +from mmcv.utils import Config, ConfigDict, get_logger -from otx.utils.logger import LEVEL, get_logger +from otx.utils.logger import LEVEL -logger = get_logger() +mmdet_logger = get_logger("mmdet") def build_detector( @@ -37,9 +37,9 @@ def build_detector( model_cfg = deepcopy(config.model) model = origin_build_detector(model_cfg, train_cfg=train_cfg, test_cfg=test_cfg) - logger.setLevel("WARNING") + mmdet_logger.setLevel("WARNING") # make logger less verbose temporally model.init_weights() - logger.setLevel(LEVEL) + mmdet_logger.setLevel(LEVEL) model = model.to(device) checkpoint = checkpoint if checkpoint else config.pop("load_from", None) From 0fbfbb11e85b2522635f2b0b48fcc6af030de080 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 22 Nov 2023 09:19:41 +0900 Subject: [PATCH 129/146] Fix a minor bug of experiment.py (#2662) fix bug --- tools/experiment.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/experiment.py b/tools/experiment.py index eb9f19a1641..70e48bad9de 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -249,6 +249,9 @@ class MMCVExpParser(BaseExpParser): 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")) @@ -297,6 +300,9 @@ class AnomalibExpParser(BaseExpParser): 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")) From 090ae97c68b9f87d062ce922f8ac2a7fae48c04d Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 22 Nov 2023 16:21:06 +0900 Subject: [PATCH 130/146] Not check avg_time_per_image during test (#2665) * ignore avg_time_per_image during test * do not call stdev when length of array is less than 2 * ignore avg_time_per_image during regerssion test --- tests/regression/regression_command.py | 4 ++++ tests/test_suite/run_test_command.py | 30 ++++++++++++-------------- tools/experiment.py | 8 +++++-- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/regression/regression_command.py b/tests/regression/regression_command.py index 5ffd95d3053..121f2c33efb 100644 --- a/tests/regression/regression_command.py +++ b/tests/regression/regression_command.py @@ -130,6 +130,8 @@ def regression_openvino_testing( model_criteria = criteria[template.name] * (1.0 - reg_threshold) 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] < model_criteria: regression_result["passed"] = False @@ -180,6 +182,8 @@ def regression_deployment_testing( modified_criteria = model_criteria - (model_criteria * reg_threshold) for k in exported_performance.keys(): + if k == "avg_time_per_image": + continue if isinstance(criteria, dict) and template.name in criteria.keys(): result_dict[k] = round(deployed_performance[k], 3) if deployed_performance[k] < modified_criteria: diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index 0ba7e21470f..c40d092bffd 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -10,7 +10,7 @@ import sys import torch from pathlib import Path -from typing import Dict +from typing import Dict, Union import onnx import onnxruntime @@ -349,11 +349,7 @@ def otx_eval_openvino_testing( with open(perf_path) as read_file: exported_performance = json.load(read_file) - for k in trained_performance.keys(): - assert ( - exported_performance[k] >= trained_performance[k] - or abs(trained_performance[k] - exported_performance[k]) / (trained_performance[k] + 1e-10) <= threshold - ), f"{trained_performance[k]=}, {exported_performance[k]=}" + compare_model_accuracy(exported_performance, trained_performance, threshold) def otx_demo_testing(template, root, otx_dir, args): @@ -494,11 +490,7 @@ def otx_eval_deployment_testing(template, root, otx_dir, args, threshold=0.0): 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(): - assert ( - deployed_performance[k] >= exported_performance[k] - or abs(exported_performance[k] - deployed_performance[k]) / (exported_performance[k] + 1e-10) <= threshold - ), f"{exported_performance[k]=}, {deployed_performance[k]=}" + compare_model_accuracy(deployed_performance, deployed_performance, threshold) def otx_demo_deployment_testing(template, root, otx_dir, args): @@ -745,11 +737,7 @@ def nncf_eval_testing(template, root, otx_dir, args, threshold=0.01): 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(): - assert ( - evaluated_performance[k] >= trained_performance[k] - or abs(trained_performance[k] - evaluated_performance[k]) / (trained_performance[k] + 1e-10) <= threshold - ), f"{trained_performance[k]=}, {evaluated_performance[k]=}" + compare_model_accuracy(evaluated_performance, trained_performance, threshold) def nncf_eval_openvino_testing(template, root, otx_dir, args): @@ -1174,3 +1162,13 @@ def test_default_for_task(self): 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/tools/experiment.py b/tools/experiment.py index 70e48bad9de..311b7641c2d 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -192,11 +192,15 @@ def get_exp_result(self): 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) + 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) + 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): # NOTE: It is assumed that performance.json has key named either score or avg_time_per_image From aa8d1750af301a8cd6ffc65bbc6b424549a0ce4b Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Fri, 24 Nov 2023 10:24:23 +0900 Subject: [PATCH 131/146] Update docs for enabling sphinx.ext.autosummary (#2654) * fix some errors/warnings on docs source * enable sphinx-autosummary for API reference documentation * Update Makefile * update sphinx configuration --- .gitignore | 4 + docs/Makefile | 6 ++ docs/source/conf.py | 38 ++++++- .../action/action_classification.rst | 2 +- .../explanation/algorithms/anomaly/index.rst | 2 +- .../multi_class_classification.rst | 3 +- .../multi_label_classification.rst | 1 + .../guide/explanation/algorithms/index.rst | 2 + .../object_detection/object_detection.rst | 2 + .../segmentation/instance_segmentation.rst | 6 +- .../source/guide/get_started/installation.rst | 6 +- .../source/guide/get_started/introduction.rst | 4 +- docs/source/guide/index.rst | 6 +- .../algorithm/action/adapters/index.rst | 8 -- .../algorithm/action/adapters/mmaction.rst | 18 ---- .../algorithm/action/adapters/openvino.rst | 14 --- .../reference/algorithm/action/configs.rst | 22 ---- .../reference/algorithm/action/index.rst | 9 -- .../reference/algorithm/action/tasks.rst | 10 -- .../algorithm/anomaly/adapters/callbacks.rst | 6 -- .../algorithm/anomaly/adapters/config.rst | 6 -- .../algorithm/anomaly/adapters/data.rst | 6 -- .../anomaly/adapters/exportable_code.rst | 6 -- .../algorithm/anomaly/adapters/index.rst | 15 --- .../algorithm/anomaly/adapters/logger.rst | 6 -- .../reference/algorithm/anomaly/configs.rst | 26 ----- .../reference/algorithm/anomaly/index.rst | 10 -- .../reference/algorithm/anomaly/tasks.rst | 10 -- .../reference/algorithm/anomaly/tools.rst | 10 -- .../classification/adapters/index.rst | 8 -- .../classification/adapters/mmcls.rst | 62 ----------- .../classification/adapters/openvino.rst | 10 -- .../algorithm/classification/configs.rst | 10 -- .../algorithm/classification/index.rst | 10 -- .../algorithm/classification/tasks.rst | 10 -- .../algorithm/classification/utils.rst | 10 -- .../algorithm/detection/adapters/index.rst | 8 -- .../algorithm/detection/adapters/mmdet.rst | 62 ----------- .../algorithm/detection/adapters/openvino.rst | 10 -- .../reference/algorithm/detection/configs.rst | 26 ----- .../reference/algorithm/detection/index.rst | 10 -- .../reference/algorithm/detection/tasks.rst | 10 -- .../reference/algorithm/detection/utils.rst | 10 -- .../guide/reference/algorithm/index.rst | 79 -------------- .../algorithm/segmentation/adapters/index.rst | 8 -- .../algorithm/segmentation/adapters/mmseg.rst | 62 ----------- .../segmentation/adapters/openvino.rst | 10 -- .../algorithm/segmentation/configs.rst | 14 --- .../algorithm/segmentation/index.rst | 9 -- .../algorithm/segmentation/tasks.rst | 10 -- .../reference/api/configuration/index.rst | 7 -- .../guide/reference/api/entities/general.rst | 102 ------------------ .../guide/reference/api/entities/index.rst | 11 -- .../reference/api/entities/interfaces.rst | 6 -- .../guide/reference/api/entities/shapes.rst | 18 ---- docs/source/guide/reference/api/index.rst | 9 -- .../guide/reference/api/usecases/adapters.rst | 6 -- .../reference/api/usecases/evaluation.rst | 38 ------- .../api/usecases/exportable_code.rst | 2 - .../guide/reference/api/usecases/index.rst | 15 --- .../reference/api/usecases/reporting.rst | 6 -- .../guide/reference/api/usecases/tasks.rst | 6 -- docs/source/guide/reference/core/data.rst | 18 ---- docs/source/guide/reference/core/index.rst | 8 -- docs/source/guide/reference/core/ov/graph.rst | 22 ---- docs/source/guide/reference/core/ov/index.rst | 25 ----- .../source/guide/reference/core/ov/models.rst | 22 ---- docs/source/guide/reference/core/ov/ops.rst | 82 -------------- docs/source/guide/reference/hpo/hpo.rst | 10 -- docs/source/guide/reference/index.rst | 11 ++ docs/source/guide/reference/mpa/index.rst | 8 -- .../utils/convert_public_data_to_cvat.py | 22 ++-- .../anomaly/adapters/anomalib/data/dataset.py | 1 + .../tools/classification_sample.py | 23 ++-- .../convnext_maskrcnn/model.py | 2 +- .../efficientnetb2b_maskrcnn/model.py | 2 +- .../efficientnetb2b_maskrcnn/semisl/model.py | 2 +- .../maskrcnn_swin_t/model.py | 2 +- .../resnet50_maskrcnn/model.py | 2 +- .../resnet50_maskrcnn/semisl/model.py | 2 +- .../efficientnetb2b_maskrcnn/model.py | 2 +- .../resnet50_maskrcnn/model.py | 2 +- .../mmseg/models/utils/channel_shuffle.py | 3 +- .../pytorch_lightning/models/backbones/vit.py | 2 +- src/otx/api/configuration/__init__.py | 4 - src/otx/api/configuration/helper/convert.py | 4 +- .../api/configuration/helper/substitute.py | 3 +- src/otx/api/usecases/__init__.py | 8 +- src/otx/api/usecases/reporting/__init__.py | 13 +-- .../__init__.py | 0 .../incremental.py | 0 .../semisl.py | 0 .../train.py | 0 .../unit/algorithms/detection/test_helpers.py | 2 +- tox.ini | 3 + 95 files changed, 120 insertions(+), 1128 deletions(-) delete mode 100644 docs/source/guide/reference/algorithm/action/adapters/index.rst delete mode 100644 docs/source/guide/reference/algorithm/action/adapters/mmaction.rst delete mode 100644 docs/source/guide/reference/algorithm/action/adapters/openvino.rst delete mode 100644 docs/source/guide/reference/algorithm/action/configs.rst delete mode 100644 docs/source/guide/reference/algorithm/action/index.rst delete mode 100644 docs/source/guide/reference/algorithm/action/tasks.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/callbacks.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/config.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/data.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/exportable_code.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/index.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/adapters/logger.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/configs.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/index.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/tasks.rst delete mode 100644 docs/source/guide/reference/algorithm/anomaly/tools.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/adapters/index.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/adapters/mmcls.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/adapters/openvino.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/configs.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/index.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/tasks.rst delete mode 100644 docs/source/guide/reference/algorithm/classification/utils.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/adapters/index.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/adapters/mmdet.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/adapters/openvino.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/configs.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/index.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/tasks.rst delete mode 100644 docs/source/guide/reference/algorithm/detection/utils.rst delete mode 100644 docs/source/guide/reference/algorithm/index.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/adapters/index.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/adapters/mmseg.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/adapters/openvino.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/configs.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/index.rst delete mode 100644 docs/source/guide/reference/algorithm/segmentation/tasks.rst delete mode 100644 docs/source/guide/reference/api/configuration/index.rst delete mode 100644 docs/source/guide/reference/api/entities/general.rst delete mode 100644 docs/source/guide/reference/api/entities/index.rst delete mode 100644 docs/source/guide/reference/api/entities/interfaces.rst delete mode 100644 docs/source/guide/reference/api/entities/shapes.rst delete mode 100644 docs/source/guide/reference/api/index.rst delete mode 100644 docs/source/guide/reference/api/usecases/adapters.rst delete mode 100644 docs/source/guide/reference/api/usecases/evaluation.rst delete mode 100644 docs/source/guide/reference/api/usecases/exportable_code.rst delete mode 100644 docs/source/guide/reference/api/usecases/index.rst delete mode 100644 docs/source/guide/reference/api/usecases/reporting.rst delete mode 100644 docs/source/guide/reference/api/usecases/tasks.rst delete mode 100644 docs/source/guide/reference/core/data.rst delete mode 100644 docs/source/guide/reference/core/index.rst delete mode 100644 docs/source/guide/reference/core/ov/graph.rst delete mode 100644 docs/source/guide/reference/core/ov/index.rst delete mode 100644 docs/source/guide/reference/core/ov/models.rst delete mode 100644 docs/source/guide/reference/core/ov/ops.rst delete mode 100644 docs/source/guide/reference/hpo/hpo.rst create mode 100644 docs/source/guide/reference/index.rst delete mode 100644 docs/source/guide/reference/mpa/index.rst rename src/otx/recipes/stages/{instance-segmentation => instance_segmentation}/__init__.py (100%) rename src/otx/recipes/stages/{instance-segmentation => instance_segmentation}/incremental.py (100%) rename src/otx/recipes/stages/{instance-segmentation => instance_segmentation}/semisl.py (100%) rename src/otx/recipes/stages/{instance-segmentation => instance_segmentation}/train.py (100%) diff --git a/.gitignore b/.gitignore index 934218f75c1..d7ed0a6bbd6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ results/ build/ dist/ !src/otx/recipes/** +src/otx/recipes/**/__pycache__ *egg-info *.pth @@ -45,3 +46,6 @@ src/**/*.so # Dataset made by unit-test tests/**/detcon_mask/* + +# sphinx-autosummary generated files +docs/**/_autosummary/ diff --git a/docs/Makefile b/docs/Makefile index 290eb859f4e..2750a53f965 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -23,3 +23,9 @@ html: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +# Custom clean target that also removes autosummary generated files. Can +# be removed when https://github.com/sphinx-doc/sphinx/issues/1999 is fixed. +clean: + rm -rf "$(SOURCEDIR)/guide/reference/_autosummary" + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py index 61741e262b6..02e674cf0cf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,8 +33,23 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx.ext.napoleon", # Support for NumPy and Google style docstrings '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 +] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +suppress_warnings = [ + "ref.python", + "autosectionlabel.*", ] # Add any paths that contain templates here, relative to this directory. @@ -45,7 +60,6 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -74,3 +88,25 @@ html_css_files = [ 'css/custom.css', ] + +# -- Extension configuration ------------------------------------------------- +autodoc_docstring_signature = True +autodoc_member_order = "bysource" +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} +autodoc_member_order = "groupwise" +autodoc_default_options = { + "members": True, + "methods": True, + "special-members": "__call__", + "exclude-members": "_abc_impl", + "show-inheritance": True, +} + +autoclass_content = "both" + +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/algorithms/action/action_classification.rst b/docs/source/guide/explanation/algorithms/action/action_classification.rst index 52fef81707c..be04c27e2b7 100644 --- a/docs/source/guide/explanation/algorithms/action/action_classification.rst +++ b/docs/source/guide/explanation/algorithms/action/action_classification.rst @@ -1,5 +1,5 @@ Action Classification -================== +===================== Action classification is a problem of identifying the action that is being performed in a video. The input to the algorithm is a sequence of video frames, and the output is a label indicating the action that is being performed. diff --git a/docs/source/guide/explanation/algorithms/anomaly/index.rst b/docs/source/guide/explanation/algorithms/anomaly/index.rst index cc2f1ba1ca5..806b235a668 100644 --- a/docs/source/guide/explanation/algorithms/anomaly/index.rst +++ b/docs/source/guide/explanation/algorithms/anomaly/index.rst @@ -143,7 +143,7 @@ Since STFPM trains the student network, we use the following parameters for its - ``Aditional Techniques``: - ``Early Stopping``: Early stopping is used to stop the training process when the validation loss stops improving. The default value of the early stopping patience is ``10``. -For more information on STFPM's training. We invite you to read Anomalib's `STFPM documentation`_. +For more information on STFPM's training. We invite you to read Anomalib's `STFPM documentation `_. Reconstruction-based Models --------------------------- 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 dd1930c0e97..76fe3683d6e 100644 --- a/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/multi_class_classification.rst @@ -100,7 +100,7 @@ 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). +\* These datasets were splitted with auto-split (80% train, 20% test). ************************ Semi-supervised Learning @@ -145,7 +145,6 @@ In the table below the top-1 accuracy on some academic datasets using our pipeli | 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 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 46840d0c955..eed5090426e 100644 --- a/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst +++ b/docs/source/guide/explanation/algorithms/classification/multi_label_classification.rst @@ -28,6 +28,7 @@ Specifically, this format should be converted in our `internal representation --data_root_dir --output .. note:: diff --git a/docs/source/guide/explanation/algorithms/index.rst b/docs/source/guide/explanation/algorithms/index.rst index 8202085affc..03351d37705 100644 --- a/docs/source/guide/explanation/algorithms/index.rst +++ b/docs/source/guide/explanation/algorithms/index.rst @@ -11,7 +11,9 @@ To this end, we support: - **Supervised training**. This is the most common approach for computer vision tasks such as object detection and image classification. Supervised learning involves training a model on a labeled dataset of images. The model learns to associate specific features in the images with the corresponding labels. - **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. 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 7edc3065a41..e8022cc9ea1 100644 --- a/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst +++ b/docs/source/guide/explanation/algorithms/object_detection/object_detection.rst @@ -92,6 +92,7 @@ We support the following ready-to-use model templates: Above table can be found using the following command .. code-block:: + $ otx find --task detection `MobileNetV2-ATSS `_ is a good medium-range model that works well and fast in most cases. @@ -147,6 +148,7 @@ Please, refer to the :doc:`tutorial <../../../tutorials/advanced/backbones>` how 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. diff --git a/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst b/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst index c50fbe259ff..0c7b0c6191c 100644 --- a/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst +++ b/docs/source/guide/explanation/algorithms/segmentation/instance_segmentation.rst @@ -61,11 +61,11 @@ 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_EfficientNetB2B `_ | MaskRCNN-EfficientNetB2B | 68.48 | 13.27 | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ -| `Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 `_ | MaskRCNN-ResNet50 | 533.80 | 177.90 | +| `Custom_Counting_Instance_Segmentation_MaskRCNN_ResNet50 `_ | MaskRCNN-ResNet50 | 533.80 | 177.90 | +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+---------------------+-----------------+ -| `Custom_Counting_Instance_Segmentation_MaskRCNN_ConvNeXt `_ | MaskRCNN-ConvNeXt | 266.78 | 192.4 | +| `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. diff --git a/docs/source/guide/get_started/installation.rst b/docs/source/guide/get_started/installation.rst index 1a2cb39d0a4..339992cf8d3 100644 --- a/docs/source/guide/get_started/installation.rst +++ b/docs/source/guide/get_started/installation.rst @@ -1,5 +1,5 @@ Installation -============= +============ ************** Prerequisites @@ -88,9 +88,9 @@ Install ``tox`` and create a development environment: Then you may change code, and all fixes will be directly applied to the editable package. -**************************************************** +***************************************************** Install OpenVINO™ Training Extensions by using Docker -**************************************************** +***************************************************** .. code-block:: diff --git a/docs/source/guide/get_started/introduction.rst b/docs/source/guide/get_started/introduction.rst index d7a635c874e..048eb99f80d 100644 --- a/docs/source/guide/get_started/introduction.rst +++ b/docs/source/guide/get_started/introduction.rst @@ -42,9 +42,9 @@ OpenVINO™ Training Extensions will provide the :doc:`following features <../ex - OpenVINO™ Training Extensions uses `Datumaro `_ as the backend to handle datasets. On account of that, OpenVINO™ Training Extensions supports the most common academic field dataset formats for each task. In the future there will be more supported formats available to give more freedom of datasets format choice. - Improved :doc:`auto-configuration functionality <../explanation/additional_features/auto_configuration>`. OpenVINO™ Training Extensions analyzes provided dataset and selects the proper task and model template to provide the best accuracy/speed trade-off. It will also make a random auto-split of your dataset if there is no validation set provided. -************ +********************* Documentation content -************ +********************* 1. **Quick start guide**: diff --git a/docs/source/guide/index.rst b/docs/source/guide/index.rst index 9311c189d9d..504684ce8e0 100644 --- a/docs/source/guide/index.rst +++ b/docs/source/guide/index.rst @@ -32,11 +32,7 @@ Guide :hidden: :caption: Reference - reference/api/index - reference/algorithm/index - reference/core/index - reference/hpo/hpo - reference/mpa/index + reference/index .. toctree:: diff --git a/docs/source/guide/reference/algorithm/action/adapters/index.rst b/docs/source/guide/reference/algorithm/action/adapters/index.rst deleted file mode 100644 index 0327ca90094..00000000000 --- a/docs/source/guide/reference/algorithm/action/adapters/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Adapters --------- - -.. toctree:: - :maxdepth: 3 - - mmaction - openvino diff --git a/docs/source/guide/reference/algorithm/action/adapters/mmaction.rst b/docs/source/guide/reference/algorithm/action/adapters/mmaction.rst deleted file mode 100644 index af4f59b9940..00000000000 --- a/docs/source/guide/reference/algorithm/action/adapters/mmaction.rst +++ /dev/null @@ -1,18 +0,0 @@ -mmaction -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.action.adapters.mmaction.data - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.adapters.mmaction.models - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.adapters.mmaction.utils - :members: - :undoc-members: diff --git a/docs/source/guide/reference/algorithm/action/adapters/openvino.rst b/docs/source/guide/reference/algorithm/action/adapters/openvino.rst deleted file mode 100644 index 1ea1a3970de..00000000000 --- a/docs/source/guide/reference/algorithm/action/adapters/openvino.rst +++ /dev/null @@ -1,14 +0,0 @@ -openvino -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.action.adapters.openvino.model_wrappers - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.adapters.openvino.dataloader - :members: - :undoc-members: diff --git a/docs/source/guide/reference/algorithm/action/configs.rst b/docs/source/guide/reference/algorithm/action/configs.rst deleted file mode 100644 index 0955b6a9e74..00000000000 --- a/docs/source/guide/reference/algorithm/action/configs.rst +++ /dev/null @@ -1,22 +0,0 @@ -Configs -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.action.configs - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.configs.base - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.configs.classification - :members: - :undoc-members: - -.. automodule:: otx.algorithms.action.configs.detection - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/action/index.rst b/docs/source/guide/reference/algorithm/action/index.rst deleted file mode 100644 index f61adddc4ea..00000000000 --- a/docs/source/guide/reference/algorithm/action/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Action -========= - -.. toctree:: - :maxdepth: 3 - - adapters/index - configs - tasks \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/action/tasks.rst b/docs/source/guide/reference/algorithm/action/tasks.rst deleted file mode 100644 index d4bb129c5cb..00000000000 --- a/docs/source/guide/reference/algorithm/action/tasks.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tasks -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.action.tasks - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/callbacks.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/callbacks.rst deleted file mode 100644 index 5ae0f5c42d4..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/callbacks.rst +++ /dev/null @@ -1,6 +0,0 @@ -Callbacks -^^^^^^^^^ - -.. automodule:: otx.algorithms.anomaly.adapters.anomalib.callbacks - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/config.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/config.rst deleted file mode 100644 index 6520e37f1ac..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/config.rst +++ /dev/null @@ -1,6 +0,0 @@ -Config -^^^^^^ - -.. automodule:: otx.algorithms.anomaly.adapters.anomalib.config - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/data.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/data.rst deleted file mode 100644 index 5a588ac1818..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/data.rst +++ /dev/null @@ -1,6 +0,0 @@ -Data -^^^^ - -.. automodule:: otx.algorithms.anomaly.adapters.anomalib.data - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/exportable_code.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/exportable_code.rst deleted file mode 100644 index 3291aae1b10..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/exportable_code.rst +++ /dev/null @@ -1,6 +0,0 @@ -Exportable Code -^^^^^^^^^^^^^^^ - -.. automodule:: otx.algorithms.anomaly.adapters.anomalib.exportable_code - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/index.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/index.rst deleted file mode 100644 index 5b9463cc709..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Adapters --------- - -This section contains adapters that wrap ``anomalib`` to be used with OpenVINO™ Training Extensions. -Overall, these adapters could be categorized into ``config``, ``data``, -``callbacks``, ``logger`` and ``exportable_code``. - -.. toctree:: - :maxdepth: 3 - - config - data - callbacks - logger - exportable_code diff --git a/docs/source/guide/reference/algorithm/anomaly/adapters/logger.rst b/docs/source/guide/reference/algorithm/anomaly/adapters/logger.rst deleted file mode 100644 index 21c5f1bc91f..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/adapters/logger.rst +++ /dev/null @@ -1,6 +0,0 @@ -Logger -^^^^^^ - -.. automodule:: otx.algorithms.anomaly.adapters.anomalib.logger - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/configs.rst b/docs/source/guide/reference/algorithm/anomaly/configs.rst deleted file mode 100644 index 3e4eb194d1b..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/configs.rst +++ /dev/null @@ -1,26 +0,0 @@ -Configs -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.anomaly.configs - :members: - :undoc-members: - -.. automodule:: otx.algorithms.anomaly.configs.base - :members: - :undoc-members: - -.. automodule:: otx.algorithms.anomaly.configs.classification - :members: - :undoc-members: - -.. automodule:: otx.algorithms.anomaly.configs.detection - :members: - :undoc-members: - -.. automodule:: otx.algorithms.anomaly.configs.segmentation - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/index.rst b/docs/source/guide/reference/algorithm/anomaly/index.rst deleted file mode 100644 index 49511129691..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Anomaly -======= - -.. toctree:: - :maxdepth: 3 - - adapters/index - configs - tasks - tools \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/tasks.rst b/docs/source/guide/reference/algorithm/anomaly/tasks.rst deleted file mode 100644 index d076967f0c5..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/tasks.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tasks -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.anomaly.tasks - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/anomaly/tools.rst b/docs/source/guide/reference/algorithm/anomaly/tools.rst deleted file mode 100644 index 8e51743457a..00000000000 --- a/docs/source/guide/reference/algorithm/anomaly/tools.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tools -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.anomaly.tools.sample - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/classification/adapters/index.rst b/docs/source/guide/reference/algorithm/classification/adapters/index.rst deleted file mode 100644 index 36b5cab324b..00000000000 --- a/docs/source/guide/reference/algorithm/classification/adapters/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Adapters --------- - -.. toctree:: - :maxdepth: 3 - - mmcls - openvino diff --git a/docs/source/guide/reference/algorithm/classification/adapters/mmcls.rst b/docs/source/guide/reference/algorithm/classification/adapters/mmcls.rst deleted file mode 100644 index 540b104e094..00000000000 --- a/docs/source/guide/reference/algorithm/classification/adapters/mmcls.rst +++ /dev/null @@ -1,62 +0,0 @@ -mmclassification -^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.classification.adapters.mmcls - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.exporter - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.incremental - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.incremental.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.incremental.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.incremental.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.semisl - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.semisl.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.semisl.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.classification.adapters.mmcls.task.semisl.trainer - :members: - :undoc-members: diff --git a/docs/source/guide/reference/algorithm/classification/adapters/openvino.rst b/docs/source/guide/reference/algorithm/classification/adapters/openvino.rst deleted file mode 100644 index 53d7f438522..00000000000 --- a/docs/source/guide/reference/algorithm/classification/adapters/openvino.rst +++ /dev/null @@ -1,10 +0,0 @@ -openvino -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.classification.adapters.openvino.model_wrappers - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/classification/configs.rst b/docs/source/guide/reference/algorithm/classification/configs.rst deleted file mode 100644 index 8533edf73d8..00000000000 --- a/docs/source/guide/reference/algorithm/classification/configs.rst +++ /dev/null @@ -1,10 +0,0 @@ -Configs -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.classification.configs - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/classification/index.rst b/docs/source/guide/reference/algorithm/classification/index.rst deleted file mode 100644 index 418f8bd89ad..00000000000 --- a/docs/source/guide/reference/algorithm/classification/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Classification -============== - -.. toctree:: - :maxdepth: 3 - - adapters/index - configs - tasks - utils \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/classification/tasks.rst b/docs/source/guide/reference/algorithm/classification/tasks.rst deleted file mode 100644 index f9c5da8af0f..00000000000 --- a/docs/source/guide/reference/algorithm/classification/tasks.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tasks -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.classification.task - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/classification/utils.rst b/docs/source/guide/reference/algorithm/classification/utils.rst deleted file mode 100644 index 4c71c29c26b..00000000000 --- a/docs/source/guide/reference/algorithm/classification/utils.rst +++ /dev/null @@ -1,10 +0,0 @@ -Utils -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.classification.utils - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/detection/adapters/index.rst b/docs/source/guide/reference/algorithm/detection/adapters/index.rst deleted file mode 100644 index 3a313287abc..00000000000 --- a/docs/source/guide/reference/algorithm/detection/adapters/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Adapters --------- - -.. toctree:: - :maxdepth: 3 - - mmdet - openvino diff --git a/docs/source/guide/reference/algorithm/detection/adapters/mmdet.rst b/docs/source/guide/reference/algorithm/detection/adapters/mmdet.rst deleted file mode 100644 index 67b2d413d82..00000000000 --- a/docs/source/guide/reference/algorithm/detection/adapters/mmdet.rst +++ /dev/null @@ -1,62 +0,0 @@ -mmdetection -^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.detection.adapters.mmdet - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.exporter - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.incremental - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.incremental.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.incremental.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.incremental.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.semisl - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.semisl.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.semisl.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.adapters.mmdet.tasks.semisl.trainer - :members: - :undoc-members: diff --git a/docs/source/guide/reference/algorithm/detection/adapters/openvino.rst b/docs/source/guide/reference/algorithm/detection/adapters/openvino.rst deleted file mode 100644 index 68dbc7cd54e..00000000000 --- a/docs/source/guide/reference/algorithm/detection/adapters/openvino.rst +++ /dev/null @@ -1,10 +0,0 @@ -openvino -^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.detection.adapters.openvino.model_wrappers - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/detection/configs.rst b/docs/source/guide/reference/algorithm/detection/configs.rst deleted file mode 100644 index 2a3ba546d1c..00000000000 --- a/docs/source/guide/reference/algorithm/detection/configs.rst +++ /dev/null @@ -1,26 +0,0 @@ -Configs -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.detection.configs - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.configs.base - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.configs.detection - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.configs.instance_segmentation - :members: - :undoc-members: - -.. automodule:: otx.algorithms.detection.configs.rotated_detection - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/detection/index.rst b/docs/source/guide/reference/algorithm/detection/index.rst deleted file mode 100644 index 388b39f21dd..00000000000 --- a/docs/source/guide/reference/algorithm/detection/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Detection -========= - -.. toctree:: - :maxdepth: 3 - - adapters/index - configs - tasks - utils \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/detection/tasks.rst b/docs/source/guide/reference/algorithm/detection/tasks.rst deleted file mode 100644 index 6edd44fde3a..00000000000 --- a/docs/source/guide/reference/algorithm/detection/tasks.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tasks -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.detection.tasks - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/detection/utils.rst b/docs/source/guide/reference/algorithm/detection/utils.rst deleted file mode 100644 index 131d5c27148..00000000000 --- a/docs/source/guide/reference/algorithm/detection/utils.rst +++ /dev/null @@ -1,10 +0,0 @@ -Utils -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.detection.utils - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/index.rst b/docs/source/guide/reference/algorithm/index.rst deleted file mode 100644 index 74c57a88281..00000000000 --- a/docs/source/guide/reference/algorithm/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -Algorithm -=================== - -Introduction ------------- -This section contains algorithmic implementations. OpenVINO™ Training Extensions provides number of -different algorithms such as classification, detection, -segmentation and anomaly with various learning types such as supervised, -semi and self-supervised learning. - -.. toctree:: - :maxdepth: 1 - - action/index - anomaly/index - classification/index - detection/index - segmentation/index - - -Organizational Structure ------------------------- -Algorithms have the following organizational structure: - -.. code-block:: bash - - - ├── adapters - │ └── - │ ├── config - │ ├── data - │ └── ... - ├── configs - │ └── - │ ├── template.yaml - │ ├── configuration.py - │ ├── configuration.yaml - │ ├── compression_config.json - │ └── hpo_config.yaml - ├── tasks - │ ├── train.py - │ ├── inference.py - │ ├── nncf.py - │ └── openvino.py - └── tools - ├── README.md - └── sample.py - -where each algorithm has ``adapters``, ``configs``, ``tasks`` and ``tools``. - -Adapters -^^^^^^^^ -``adapters`` contain modules to wrap the original library used to perform the -task. For instance, detection task uses -`mmdetection `_ library, meaning that -``adapters`` comprises adapters to wrap ``mmdetection`` to use with OpenVINO™ Training Extensions. - -Configs -^^^^^^^ -``configs`` contain configuration related files including training, inference, -`NNCF `_ and -`HPO `_. - -Tasks -^^^^^ -.. _tasks: - -Tasks contain implementations that correspond to each phase in the workflow from -training to OpenVINO inference. Each algorithm expects ``train``, ``inference``, -``nncf`` and ``openvino`` python modules that implement the -`task interfaces `_. - -Tools -^^^^^ -Tools contain python implementations that performs :ref:`tasks ` in -end-to-end workflow. For example, current anomaly implementation has ``sample.py`` -file that reads an input dataset, trains a model and exports the model to -OpenVINO IR via either `POT `_ -or `NNCF `_. \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/segmentation/adapters/index.rst b/docs/source/guide/reference/algorithm/segmentation/adapters/index.rst deleted file mode 100644 index b304aac6cf1..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/adapters/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Adapters --------- - -.. toctree:: - :maxdepth: 3 - - mmseg - openvino diff --git a/docs/source/guide/reference/algorithm/segmentation/adapters/mmseg.rst b/docs/source/guide/reference/algorithm/segmentation/adapters/mmseg.rst deleted file mode 100644 index c03e42319c6..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/adapters/mmseg.rst +++ /dev/null @@ -1,62 +0,0 @@ -mmsegmentation -^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.exporter - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.incremental - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.incremental.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.incremental.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.incremental.trainer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.semisl - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.semisl.inferrer - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.semisl.stage - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.adapters.mmseg.tasks.semisl.trainer - :members: - :undoc-members: diff --git a/docs/source/guide/reference/algorithm/segmentation/adapters/openvino.rst b/docs/source/guide/reference/algorithm/segmentation/adapters/openvino.rst deleted file mode 100644 index 72452bfe4f4..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/adapters/openvino.rst +++ /dev/null @@ -1,10 +0,0 @@ -openvino -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.segmentation.adapters.openvino.model_wrappers - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/segmentation/configs.rst b/docs/source/guide/reference/algorithm/segmentation/configs.rst deleted file mode 100644 index ede35f526f1..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/configs.rst +++ /dev/null @@ -1,14 +0,0 @@ -Configs -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.segmentation.configs - :members: - :undoc-members: - -.. automodule:: otx.algorithms.segmentation.configs.base - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/segmentation/index.rst b/docs/source/guide/reference/algorithm/segmentation/index.rst deleted file mode 100644 index 20141192ba3..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Segmentation -============ - -.. toctree:: - :maxdepth: 3 - - adapters/index - configs - tasks \ No newline at end of file diff --git a/docs/source/guide/reference/algorithm/segmentation/tasks.rst b/docs/source/guide/reference/algorithm/segmentation/tasks.rst deleted file mode 100644 index 15c516fbe56..00000000000 --- a/docs/source/guide/reference/algorithm/segmentation/tasks.rst +++ /dev/null @@ -1,10 +0,0 @@ -Tasks -^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.algorithms.segmentation.task - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/api/configuration/index.rst b/docs/source/guide/reference/api/configuration/index.rst deleted file mode 100644 index 106ea5e9e05..00000000000 --- a/docs/source/guide/reference/api/configuration/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Configuration -============= - -.. automodule:: otx.api.configuration - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/guide/reference/api/entities/general.rst b/docs/source/guide/reference/api/entities/general.rst deleted file mode 100644 index c2219795e4e..00000000000 --- a/docs/source/guide/reference/api/entities/general.rst +++ /dev/null @@ -1,102 +0,0 @@ -General -------- - -.. automodule:: otx.api.entities.annotation - :members: - :undoc-members: - -.. automodule:: otx.api.entities.color - :members: - :undoc-members: - -.. automodule:: otx.api.entities.coordinate - :members: - :undoc-members: - -.. automodule:: otx.api.entities.dataset_item - :members: - :undoc-members: - -.. automodule:: otx.api.entities.datasets - :members: - :undoc-members: - -.. automodule:: otx.api.entities.graph - :members: - :undoc-members: - -.. automodule:: otx.api.entities.id - :members: - :undoc-members: - -.. automodule:: otx.api.entities.image - :members: - :undoc-members: - -.. automodule:: otx.api.entities.inference_parameters - :members: - :undoc-members: - -.. automodule:: otx.api.entities.label_schema - :members: - :undoc-members: - -.. automodule:: otx.api.entities.label - :members: - :undoc-members: - -.. automodule:: otx.api.entities.media - :members: - :undoc-members: - -.. automodule:: otx.api.entities.metadata - :members: - :undoc-members: - -.. automodule:: otx.api.entities.metrics - :members: - :undoc-members: - -.. automodule:: otx.api.entities.model_template - :members: - :undoc-members: - -.. automodule:: otx.api.entities.model - :members: - :undoc-members: - -.. automodule:: otx.api.entities.optimization_parameters - :members: - :undoc-members: - -.. automodule:: otx.api.entities.result_media - :members: - :undoc-members: - -.. automodule:: otx.api.entities.resultset - :members: - :undoc-members: - -.. automodule:: otx.api.entities.scored_label - :members: - :undoc-members: - -.. automodule:: otx.api.entities.subset - :members: - :undoc-members: - -.. automodule:: otx.api.entities.task_environment - :members: - :undoc-members: - -.. automodule:: otx.api.entities.tensor - :members: - :undoc-members: - -.. automodule:: otx.api.entities.train_parameters - :members: - :undoc-members: - -.. automodule:: otx.api.entities.url - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/api/entities/index.rst b/docs/source/guide/reference/api/entities/index.rst deleted file mode 100644 index d1901ffbf19..00000000000 --- a/docs/source/guide/reference/api/entities/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Entities -======== - -This page contains a reference description for the Entities. - -.. toctree:: - :maxdepth: 3 - - general - shapes - interfaces \ No newline at end of file diff --git a/docs/source/guide/reference/api/entities/interfaces.rst b/docs/source/guide/reference/api/entities/interfaces.rst deleted file mode 100644 index c266a107608..00000000000 --- a/docs/source/guide/reference/api/entities/interfaces.rst +++ /dev/null @@ -1,6 +0,0 @@ -Interfaces ----------- - -.. automodule:: otx.api.entities.interfaces.graph_interface - :members: - :undoc-members: diff --git a/docs/source/guide/reference/api/entities/shapes.rst b/docs/source/guide/reference/api/entities/shapes.rst deleted file mode 100644 index 5bf152948a9..00000000000 --- a/docs/source/guide/reference/api/entities/shapes.rst +++ /dev/null @@ -1,18 +0,0 @@ -Shapes ------- - -.. automodule:: otx.api.entities.shapes.ellipse - :members: - :undoc-members: - -.. automodule:: otx.api.entities.shapes.polygon - :members: - :undoc-members: - -.. automodule:: otx.api.entities.shapes.rectangle - :members: - :undoc-members: - -.. automodule:: otx.api.entities.shapes.shape - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/api/index.rst b/docs/source/guide/reference/api/index.rst deleted file mode 100644 index 57d7916a447..00000000000 --- a/docs/source/guide/reference/api/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -API -============= - -.. toctree:: - :maxdepth: 2 - - configuration/index - entities/index - usecases/index \ No newline at end of file diff --git a/docs/source/guide/reference/api/usecases/adapters.rst b/docs/source/guide/reference/api/usecases/adapters.rst deleted file mode 100644 index 8368053095a..00000000000 --- a/docs/source/guide/reference/api/usecases/adapters.rst +++ /dev/null @@ -1,6 +0,0 @@ -Adapters --------- - -.. automodule:: otx.api.usecases.adapters.model_adapter - :members: - :undoc-members: diff --git a/docs/source/guide/reference/api/usecases/evaluation.rst b/docs/source/guide/reference/api/usecases/evaluation.rst deleted file mode 100644 index a397f1eed2f..00000000000 --- a/docs/source/guide/reference/api/usecases/evaluation.rst +++ /dev/null @@ -1,38 +0,0 @@ -Evaluation ----------- - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.api.usecases.evaluation.accuracy - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.anomaly_metrics - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.averaging - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.basic_operations - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.dice - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.f_measure - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.metrics_helper - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.evaluation.performance_provider_interface - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/api/usecases/exportable_code.rst b/docs/source/guide/reference/api/usecases/exportable_code.rst deleted file mode 100644 index f726b02cfad..00000000000 --- a/docs/source/guide/reference/api/usecases/exportable_code.rst +++ /dev/null @@ -1,2 +0,0 @@ -Exportable Code ---------------- \ No newline at end of file diff --git a/docs/source/guide/reference/api/usecases/index.rst b/docs/source/guide/reference/api/usecases/index.rst deleted file mode 100644 index ef4bd632477..00000000000 --- a/docs/source/guide/reference/api/usecases/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Usecases -======== - -.. automodule:: otx.api.usecases - :members: - :undoc-members: - -.. toctree:: - :maxdepth: 3 - - adapters - evaluation - exportable_code - reporting - tasks diff --git a/docs/source/guide/reference/api/usecases/reporting.rst b/docs/source/guide/reference/api/usecases/reporting.rst deleted file mode 100644 index ee85548324c..00000000000 --- a/docs/source/guide/reference/api/usecases/reporting.rst +++ /dev/null @@ -1,6 +0,0 @@ -Reporting ---------- - -.. automodule:: otx.api.usecases.reporting - :members: - :undoc-members: diff --git a/docs/source/guide/reference/api/usecases/tasks.rst b/docs/source/guide/reference/api/usecases/tasks.rst deleted file mode 100644 index 93a4da176a4..00000000000 --- a/docs/source/guide/reference/api/usecases/tasks.rst +++ /dev/null @@ -1,6 +0,0 @@ -Task interfaces ---------------- - -.. automodule:: otx.api.usecases.tasks - :members: - :undoc-members: diff --git a/docs/source/guide/reference/core/data.rst b/docs/source/guide/reference/core/data.rst deleted file mode 100644 index 86860aa1186..00000000000 --- a/docs/source/guide/reference/core/data.rst +++ /dev/null @@ -1,18 +0,0 @@ -Data -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.core.data - :members: - :undoc-members: - -.. automodule:: otx.core.data.adapter - :members: - :undoc-members: - -.. automodule:: otx.core.data.manager - :members: - :undoc-members: diff --git a/docs/source/guide/reference/core/index.rst b/docs/source/guide/reference/core/index.rst deleted file mode 100644 index b55754cce07..00000000000 --- a/docs/source/guide/reference/core/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Core -==== - -.. toctree:: - :maxdepth: 1 - - data - ov/index diff --git a/docs/source/guide/reference/core/ov/graph.rst b/docs/source/guide/reference/core/ov/graph.rst deleted file mode 100644 index 6f47f9e154b..00000000000 --- a/docs/source/guide/reference/core/ov/graph.rst +++ /dev/null @@ -1,22 +0,0 @@ -Graph -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.core.ov.graph - :members: - :undoc-members: - -.. automodule:: otx.core.ov.graph.graph - :members: - :undoc-members: - -.. automodule:: otx.core.ov.graph.utils - :members: - :undoc-members: - -.. automodule:: otx.core.ov.graph.parsers - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/core/ov/index.rst b/docs/source/guide/reference/core/ov/index.rst deleted file mode 100644 index 07ce1abd4cf..00000000000 --- a/docs/source/guide/reference/core/ov/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -OpenVINO -=================== - -.. toctree:: - :maxdepth: 1 - - graph - models - ops - -.. automodule:: otx.core.ov - :members: - :undoc-members: - -.. automodule:: otx.core.ov.omz_wrapper - :members: - :undoc-members: - -.. automodule:: otx.core.ov.registry - :members: - :undoc-members: - -.. automodule:: otx.core.ov.utils - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/core/ov/models.rst b/docs/source/guide/reference/core/ov/models.rst deleted file mode 100644 index c3f535e82ab..00000000000 --- a/docs/source/guide/reference/core/ov/models.rst +++ /dev/null @@ -1,22 +0,0 @@ -Models -^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.core.ov.models - :members: - :undoc-members: - -.. automodule:: otx.core.ov.models.mmov_model - :members: - :undoc-members: - -.. automodule:: otx.core.ov.models.ov_model - :members: - :undoc-members: - -.. automodule:: otx.core.ov.models.parser_mixin - :members: - :undoc-members: diff --git a/docs/source/guide/reference/core/ov/ops.rst b/docs/source/guide/reference/core/ov/ops.rst deleted file mode 100644 index 7b249e02702..00000000000 --- a/docs/source/guide/reference/core/ov/ops.rst +++ /dev/null @@ -1,82 +0,0 @@ -OPS -^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.core.ov.ops - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.activations - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.arithmetics - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.builder - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.convolutions - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.generation - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.image_processings - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.infrastructures - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.matmuls - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.movements - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.normalizations - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.object_detections - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.op - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.poolings - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.reductions - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.shape_manipulations - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.sorting_maximization - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.type_conversions - :members: - :undoc-members: - -.. automodule:: otx.core.ov.ops.utils - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/hpo/hpo.rst b/docs/source/guide/reference/hpo/hpo.rst deleted file mode 100644 index 13ead8124b8..00000000000 --- a/docs/source/guide/reference/hpo/hpo.rst +++ /dev/null @@ -1,10 +0,0 @@ -HPO -^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.hpo - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/index.rst b/docs/source/guide/reference/index.rst new file mode 100644 index 00000000000..6abaf03ba3d --- /dev/null +++ b/docs/source/guide/reference/index.rst @@ -0,0 +1,11 @@ +API reference +============= + +.. _api_reference: + +.. autosummary:: + :recursive: + :nosignatures: + :toctree: _autosummary + + otx diff --git a/docs/source/guide/reference/mpa/index.rst b/docs/source/guide/reference/mpa/index.rst deleted file mode 100644 index 2b7ebc58cf3..00000000000 --- a/docs/source/guide/reference/mpa/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Model Preparation Algorithm -=========================== - -.. toctree:: - :maxdepth: 1 - - modules/index - utils 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 index 1553951a2b2..26dd67dc05b 100644 --- a/src/otx/algorithms/action/utils/convert_public_data_to_cvat.py +++ b/src/otx/algorithms/action/utils/convert_public_data_to_cvat.py @@ -4,16 +4,18 @@ 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 +:: + + root + |- video_0 + | |- images + | |- frames_001.png + | |- frames_002.png + | |- annotations.xml + |- video_1 + | |- images + | |- annotations.xml + |- video_2 """ diff --git a/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py b/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py index fbc92b1d9b7..3db5c341aff 100644 --- a/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py +++ b/src/otx/algorithms/anomaly/adapters/anomalib/data/dataset.py @@ -1,4 +1,5 @@ """DataLoaders for Anomaly Tasks.""" + # Copyright (C) 2021 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/otx/algorithms/classification/tools/classification_sample.py b/src/otx/algorithms/classification/tools/classification_sample.py index 45104ddc19b..de9283dfbed 100644 --- a/src/otx/algorithms/classification/tools/classification_sample.py +++ b/src/otx/algorithms/classification/tools/classification_sample.py @@ -44,16 +44,19 @@ logger = get_logger() -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") -args = parser.parse_args() +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): + +def load_test_dataset(data_type, args): """Load test dataset.""" import PIL from PIL import ImageDraw @@ -221,11 +224,11 @@ def validate(task, validation_dataset, model): print(str(resultset.performance)) -def main(): +def main(args): """Main of Classification Sample Test.""" logger.info("Train initial model with OLD dataset") - dataset, labels_list = load_test_dataset("old") + 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") @@ -363,4 +366,4 @@ def main(): if __name__ == "__main__": - sys.exit(main() or 0) + sys.exit(main(parse_args()) or 0) 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 index 84d89dd682f..a0069179b4f 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/instance_segmentation/convnext_maskrcnn/model.py @@ -6,7 +6,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../base/models/detector.py", ] 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 index 72ca9481ef3..603bc853e46 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/instance_segmentation/efficientnetb2b_maskrcnn/model.py @@ -17,7 +17,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", "../../base/models/detector.py", ] 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 index 547a17db75e..17dae156d2b 100644 --- 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 @@ -6,7 +6,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../../recipes/stages/instance-segmentation/semisl.py", + "../../../../../../recipes/stages/instance_segmentation/semisl.py", "../../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", "../../../base/models/detector.py", ] 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 index 66f7522bdee..ab5612190ac 100644 --- 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 @@ -6,7 +6,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../base/models/detector.py", ] 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 index 22a3bdf4b9e..23c04f512c8 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/instance_segmentation/resnet50_maskrcnn/model.py @@ -6,7 +6,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", "../../base/models/detector.py", ] 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 index 780c6cc8f39..6f9cce1a468 100644 --- 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 @@ -6,7 +6,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../../recipes/stages/instance-segmentation/semisl.py", + "../../../../../../recipes/stages/instance_segmentation/semisl.py", "../../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", "../../../base/models/detector.py", ] 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 index 10001822570..5a7a818925c 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/rotated_detection/efficientnetb2b_maskrcnn/model.py @@ -7,7 +7,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../../../common/adapters/mmcv/configs/backbones/efficientnet_b2b.yaml", "../../base/models/detector.py", ] 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 index c406c97bd00..5d528fe4796 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py +++ b/src/otx/algorithms/detection/configs/rotated_detection/resnet50_maskrcnn/model.py @@ -7,7 +7,7 @@ # pylint: disable=invalid-name _base_ = [ - "../../../../../recipes/stages/instance-segmentation/incremental.py", + "../../../../../recipes/stages/instance_segmentation/incremental.py", "../../../../common/adapters/mmcv/configs/backbones/resnet50.yaml", "../../base/models/detector.py", ] 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 index a86218532e1..a5697b7d984 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/models/utils/channel_shuffle.py @@ -17,8 +17,7 @@ def channel_shuffle(x, groups): Args: x (Tensor): The input tensor. - groups (int): The number of groups to divide the input tensor - in the channel dimension. + groups (int): The number of groups to divide the input tensor in the channel dimension. Returns: Tensor: The output tensor after channel shuffle operation. 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 index cd6300fca03..6ef1d934cad 100644 --- 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 @@ -357,7 +357,7 @@ def add_decomposed_rel_pos( q_size: Tuple[int, int], k_size: Tuple[int, int], ) -> Tensor: - """Calculate decomposed Relative Positional Embeddings from :paper:`mvitv2`. + """Calculate decomposed Relative Positional Embeddings from `mvitv2`. https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py diff --git a/src/otx/api/configuration/__init__.py b/src/otx/api/configuration/__init__.py index 5f765627168..e706fd27c4a 100644 --- a/src/otx/api/configuration/__init__.py +++ b/src/otx/api/configuration/__init__.py @@ -4,10 +4,6 @@ functions to interact with them. The configuration helper module can be imported as `otx_config_helper` and implements the following: - -.. automodule:: otx.api.configuration.helper - :members: - """ # Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 diff --git a/src/otx/api/configuration/helper/convert.py b/src/otx/api/configuration/helper/convert.py index 3d9a2a2b0ec..350293655f7 100644 --- a/src/otx/api/configuration/helper/convert.py +++ b/src/otx/api/configuration/helper/convert.py @@ -105,8 +105,8 @@ def convert( 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. + 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 diff --git a/src/otx/api/configuration/helper/substitute.py b/src/otx/api/configuration/helper/substitute.py index bf24e884a3b..d0a1926b76f 100644 --- a/src/otx/api/configuration/helper/substitute.py +++ b/src/otx/api/configuration/helper/substitute.py @@ -172,8 +172,7 @@ def substitute_values_for_lifecycle( Args: config (ConfigurableParameters): ConfigurableParameter object to substitute values into - value_input (ConfigurableParameters): ConfigurableParameters to take the values to be substituted - from. + 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 diff --git a/src/otx/api/usecases/__init__.py b/src/otx/api/usecases/__init__.py index 933c7db5f8b..3bea124f765 100644 --- a/src/otx/api/usecases/__init__.py +++ b/src/otx/api/usecases/__init__.py @@ -1,10 +1,4 @@ -"""Utilities and use cases built on top of OTX API. - -.. automodule:: otx.api.usecases.adapters - :members: - :undoc-members: - -""" +"""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/reporting/__init__.py b/src/otx/api/usecases/reporting/__init__.py index 8d27e8621f8..2b3bb382475 100644 --- a/src/otx/api/usecases/reporting/__init__.py +++ b/src/otx/api/usecases/reporting/__init__.py @@ -1,15 +1,4 @@ -"""Training reporting. - -.. automodule:: otx.api.usecases.reporting.callback - :members: - :undoc-members: - -.. automodule:: otx.api.usecases.reporting.time_monitor_callback - :members: - :undoc-members: - -""" - +"""Training reporting.""" # Copyright (C) 2021-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # diff --git a/src/otx/recipes/stages/instance-segmentation/__init__.py b/src/otx/recipes/stages/instance_segmentation/__init__.py similarity index 100% rename from src/otx/recipes/stages/instance-segmentation/__init__.py rename to src/otx/recipes/stages/instance_segmentation/__init__.py diff --git a/src/otx/recipes/stages/instance-segmentation/incremental.py b/src/otx/recipes/stages/instance_segmentation/incremental.py similarity index 100% rename from src/otx/recipes/stages/instance-segmentation/incremental.py rename to src/otx/recipes/stages/instance_segmentation/incremental.py diff --git a/src/otx/recipes/stages/instance-segmentation/semisl.py b/src/otx/recipes/stages/instance_segmentation/semisl.py similarity index 100% rename from src/otx/recipes/stages/instance-segmentation/semisl.py rename to src/otx/recipes/stages/instance_segmentation/semisl.py diff --git a/src/otx/recipes/stages/instance-segmentation/train.py b/src/otx/recipes/stages/instance_segmentation/train.py similarity index 100% rename from src/otx/recipes/stages/instance-segmentation/train.py rename to src/otx/recipes/stages/instance_segmentation/train.py diff --git a/tests/unit/algorithms/detection/test_helpers.py b/tests/unit/algorithms/detection/test_helpers.py index 32c8a8d67a7..d93d5531d68 100644 --- a/tests/unit/algorithms/detection/test_helpers.py +++ b/tests/unit/algorithms/detection/test_helpers.py @@ -33,7 +33,7 @@ "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" +DEFAULT_ISEG_RECIPE_CONFIG_PATH = "src/otx/recipes/stages/instance_segmentation/incremental.py" class MockImage(Image): diff --git a/tox.ini b/tox.ini index b5de386f04d..22983697622 100644 --- a/tox.ini +++ b/tox.ini @@ -83,6 +83,7 @@ commands = deps = {[testenv:tests-all-py310-pt1]deps} atheris +extras = full commands = 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} @@ -97,7 +98,9 @@ deps = change_dir = {toxinidir}/docs allowlist_externals = make +extras = full commands = + make clean make html From 9c56c20c89cbaf625a8991a2365b04593f4972d0 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Fri, 24 Nov 2023 15:57:32 +0200 Subject: [PATCH 132/146] Update PTQ docs (#2672) * Replace POT -> PTQ * Fixes from comments --- .../models_optimization.rst | 14 +++++++------- docs/source/guide/get_started/cli_commands.rst | 14 +++++++------- .../guide/tutorials/advanced/semi_sl.rst | 2 +- docs/source/guide/tutorials/base/deploy.rst | 2 +- .../how_to_train/action_classification.rst | 14 ++++++++------ .../base/how_to_train/action_detection.rst | 4 +++- .../base/how_to_train/anomaly_detection.rst | 11 ++++++----- .../base/how_to_train/classification.rst | 10 +++++----- .../tutorials/base/how_to_train/detection.rst | 18 +++++++++--------- .../how_to_train/instance_segmentation.rst | 8 ++++---- .../how_to_train/semantic_segmentation.rst | 11 ++++++----- src/otx/cli/tools/eval.py | 4 ++-- 12 files changed, 59 insertions(+), 53 deletions(-) diff --git a/docs/source/guide/explanation/additional_features/models_optimization.rst b/docs/source/guide/explanation/additional_features/models_optimization.rst index 4b3c9ec4787..08715fb3165 100644 --- a/docs/source/guide/explanation/additional_features/models_optimization.rst +++ b/docs/source/guide/explanation/additional_features/models_optimization.rst @@ -1,17 +1,17 @@ Models Optimization =================== -OpenVINO™ Training Extensions provides two types of optimization algorithms: `Post-training Optimization Tool (POT) `_ and `Neural Network Compression Framework (NNCF) `_. +OpenVINO™ Training Extensions provides two types of optimization algorithms: `Post-Training Quantization tool (PTQ) `_ and `Neural Network Compression Framework (NNCF) `_. ******************************* -Post-training Optimization Tool +Post-Training Quantization Tool ******************************* -POT is designed to optimize the inference of models by applying post-training methods that do not require model retraining or fine-tuning. If you want to know more details about how POT works and to be more familiar with model optimization methods, please refer to `documentation `_. +PTQ is designed to optimize the inference of models by applying post-training methods that do not require model retraining or fine-tuning. If you want to know more details about how PTQ works and to be more familiar with model optimization methods, please refer to `documentation `_. -To run Post-training optimization 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, see the `DefaultQuantization Parameters `_ for further information about configuring the optimization. +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. -POT 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 POT, that can be found in the same directory with ``template.yaml``, for example for `PaDiM `_, `OCR-Lite-HRNe-18-mod2 `_ model. +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 @@ -25,9 +25,9 @@ You can refer to configuration files for default templates for each task accordi 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 POT is faster but can degrade accuracy more than the training-enabled approach. +Meanwhile, the PTQ is faster but can degrade accuracy more than the training-enabled approach. .. 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. -Please, refer to our :doc:`dedicated tutorials <../../tutorials/base/how_to_train/index>` on how to optimize your model using POT or NNCF. \ No newline at end of file +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/get_started/cli_commands.rst b/docs/source/guide/get_started/cli_commands.rst index 74d1e975729..a280da898f5 100644 --- a/docs/source/guide/get_started/cli_commands.rst +++ b/docs/source/guide/get_started/cli_commands.rst @@ -342,10 +342,10 @@ To use the exported model as an input for ``otx explain``, please dump additiona Optimization ************ -``otx optimize`` optimizes a model using `NNCF `_ or `POT `_ depending on the model 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 -- POT optimization used for models exported in the OpenVINO™ IR format +- PTQ optimization used for models exported in the OpenVINO™ IR format With the ``--help`` command, you can list additional information: @@ -383,16 +383,16 @@ Command example for optimizing a PyTorch model (.pth) with OpenVINO™ NNCF: --output outputs/nncf -Command example for optimizing OpenVINO™ model (.xml) with OpenVINO™ POT: +Command example for optimizing OpenVINO™ model (.xml) with OpenVINO™ PTQ: .. code-block:: (otx) ...$ otx optimize SSD --load-weights \ --val-data-roots \ - --output outputs/pot + --output outputs/ptq -Thus, to use POT pass the path to exported IR (.xml) model, to use NNCF pass the path to the PyTorch (.pth) weights. +Thus, to use PTQ pass the path to exported IR (.xml) model, to use NNCF pass the path to the PyTorch (.pth) weights. *********** @@ -419,7 +419,7 @@ With the ``--help`` command, you can list additional information, such as its pa --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 (POT only) or exported model. + 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. @@ -532,7 +532,7 @@ Demonstration -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 (POT only) or exported model. + 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. diff --git a/docs/source/guide/tutorials/advanced/semi_sl.rst b/docs/source/guide/tutorials/advanced/semi_sl.rst index 236c72f1189..b4ce1142627 100644 --- a/docs/source/guide/tutorials/advanced/semi_sl.rst +++ b/docs/source/guide/tutorials/advanced/semi_sl.rst @@ -28,7 +28,7 @@ The process has been tested on the following configuration: 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™ POT, refer to `classification optimization <../base/how_to_train/classification.html#optimization>`__. + 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. diff --git a/docs/source/guide/tutorials/base/deploy.rst b/docs/source/guide/tutorials/base/deploy.rst index 8fbbdac7d97..367597d414c 100644 --- a/docs/source/guide/tutorials/base/deploy.rst +++ b/docs/source/guide/tutorials/base/deploy.rst @@ -52,7 +52,7 @@ using the command below: 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 POT, passing the path to this model in IR format to ``--load-weights`` parameter. +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. 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 12295845c8b..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 @@ -212,7 +212,7 @@ 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 efficiently, especially on the CPU. Also, the resulting IR model is required to run POT optimization. IR model consists of two files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. +It allows running the model on the Intel hardware much more efficiently, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of two files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. 2. Run the command line below to export the trained model and save the exported model to the ``openvino`` folder. @@ -235,7 +235,7 @@ and save the exported model to the ``openvino`` folder. 2023-02-21 22:54:35,424 - mmaction - INFO - Exporting completed -3. Check the accuracy of the IR model and the consistency between the exported model and the PyTorch model, +3. Check the accuracy of the IR optimimodel and the consistency between the exported model and the PyTorch model, using ``otx eval`` and passing the IR model path to the ``--load-weights`` parameter. .. code-block:: @@ -254,22 +254,24 @@ Optimization ************* 1. You can further optimize the model with ``otx optimize``. -Currently, quantization jobs that include POT is supported for X3D template. MoViNet will be supported in near future. +Currently, quantization jobs that include PTQ is supported for X3D template. MoViNet will be supported in near future. + +The optimized model will be quantized to ``INT8`` format. Refer to :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section for more details on model optimization. 2. Example command for optimizing -OpenVINO™ model (.xml) with OpenVINO™ POT. +OpenVINO™ model (.xml) with OpenVINO™ PTQ. .. code-block:: (otx) ...$ otx optimize --load-weights openvino/openvino.xml \ - --output pot_model + --output ptq_model ... Performance(score: 0.6252587703095486, dashboard: (3 metric groups)) -Keep in mind that POT will take some time (generally less than NNCF optimization) without logging to optimize the model. +Keep in mind that PTQ will take some time (generally less than NNCF optimization) without logging to optimize the model. 3. Now, you have fully trained, optimized and exported an efficient model representation ready-to-use action classification model. diff --git a/docs/source/guide/tutorials/base/how_to_train/action_detection.rst b/docs/source/guide/tutorials/base/how_to_train/action_detection.rst index c1252ad011a..6103735beb0 100644 --- a/docs/source/guide/tutorials/base/how_to_train/action_detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/action_detection.rst @@ -201,6 +201,8 @@ Optimization 1. You can further optimize the model with ``otx optimize``. Currently, only PTQ is supported for action detection. NNCF will be supported in near future. + +The optimized model will be quantized to ``INT8`` format. Refer to :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section for more details on model optimization. 2. Example command for optimizing @@ -209,7 +211,7 @@ OpenVINO™ model (.xml) with OpenVINO™ PTQ. .. code-block:: (otx) ...$ otx optimize --load-weights openvino/openvino.xml \ - --save-model-to pot_model + --save-model-to ptq_model ... 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 31b51809c95..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 @@ -156,7 +156,7 @@ 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 POT optimization. IR model consists of 2 files: ``openvino.xml`` for weights and ``openvino.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: @@ -200,10 +200,11 @@ This gives the following results: Optimization ************ -Anomaly tasks can be optimized either in POT or NNCF format. For more information refer to the :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section. +Anomaly tasks can be optimized either in PTQ or NNCF format. The model will be quantized to ``INT8`` format. +For more information refer to the :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section. -1. Let's start with POT +1. Let's start with PTQ optimization. .. code-block:: @@ -211,7 +212,7 @@ optimization. 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/pot_model + --output otx-workspace-ANOMALY_DETECTION/ptq_model This command generates the following files that can be used to run :doc:`otx demo <../demo>`: @@ -233,7 +234,7 @@ weights to the ``opitmize`` command: --load-weights otx-workspace-ANOMALY_DETECTION/models/weights.pth \ --output otx-workspace-ANOMALY_DETECTION/nncf_model -Similar to POT optimization, it generates the following files: +Similar to PTQ optimization, it generates the following files: - image_threshold - pixel_threshold 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 c84448f4c7d..6569194cd3a 100644 --- a/docs/source/guide/tutorials/base/how_to_train/classification.rst +++ b/docs/source/guide/tutorials/base/how_to_train/classification.rst @@ -177,7 +177,7 @@ 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 POT optimization. IR model consists of 2 files: ``openvino.xml`` for weights and ``openvino.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. You can run the below command line to export the trained model and save the exported model to the ``openvino_model`` folder: @@ -212,7 +212,7 @@ Optimization ************* 1. You can further optimize the model with ``otx optimize``. -It uses NNCF or POT depending on the model format. +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 for more details on model optimization. @@ -235,18 +235,18 @@ a PyTorch model (`.pth`) with OpenVINO™ NNCF. The optimization time relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 and Intel(R) Core(TM) i9-10980XE it took about 10 minutes. 3. Command example for optimizing -OpenVINO™ model (.xml) with OpenVINO™ POT. +OpenVINO™ model (.xml) with OpenVINO™ PTQ. .. code-block:: (otx) ...$ otx optimize --load-weights openvino_model/openvino.xml \ - --output pot_model + --output ptq_model ... Performance(score: 0.9577656675749319, dashboard: (3 metric groups)) -Please note, that POT will take some time (generally less than NNCF optimization) without logging to optimize the model. +Please note, that PTQ will take some time (generally less than NNCF optimization) without logging to optimize the model. 4. Now you have fully trained, optimized and exported an efficient model representation ready-to-use classification model. 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 f6235e76d84..b2434830c45 100644 --- a/docs/source/guide/tutorials/base/how_to_train/detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/detection.rst @@ -330,7 +330,7 @@ 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 POT optimization in the section below. IR model contains 2 files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. +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. 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. @@ -384,11 +384,11 @@ Optimization ************* 1. We can further optimize the model with ``otx optimize``. -It uses NNCF or POT depending on the model format. +It uses NNCF or PTQ depending on the model and transforms it to ``INT8`` format. ``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. -``POT`` 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. +``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. 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`): @@ -420,20 +420,20 @@ with OpenVINO NNCF. 3. Command example for optimizing OpenVINOâ„¢ model (.xml) -with OpenVINOâ„¢ POT. +with OpenVINOâ„¢ PTQ. .. code-block:: (otx) ...$ otx optimize --load-weights ../outputs/openvino/openvino.xml \ - --output ../outputs/pot \ - --output ../outputs/pot + --output ../outputs/ptq \ + --output ../outputs/ptq ... 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 POT optimization - 2023-01-10 06:34:29,304 | INFO : POT optimization 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 @@ -441,7 +441,7 @@ with OpenVINOâ„¢ POT. Performance(score: 0.5389435989256938, 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 POT will take some time without logging to optimize the model. +Please note, that PTQ will take some time without logging to optimize the model. 4. Finally, we can also evaluate the optimized model by passing it to the ``otx eval`` function. 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 925bf9a257a..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 @@ -327,7 +327,7 @@ 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 POT optimization. IR model consists of 2 files: ``openvino.xml`` for weights and ``openvino.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 ``outputs/**_export/openvino`` folder. @@ -354,7 +354,7 @@ Optimization ************* 1. We can further optimize the model with ``otx optimize``. -It uses NNCF or POT depending on the model format. +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. @@ -371,13 +371,13 @@ a PyTorch model (`.pth`) with OpenVINOâ„¢ `NNCF ` section to get the intuition of what we use under the hood for optimization purposes. 2. Command example for optimizing @@ -234,18 +235,18 @@ a PyTorch model (`.pth`) with OpenVINOâ„¢ NNCF. The optimization time relies on the hardware characteristics, for example on 1 NVIDIA GeForce RTX 3090 and Intel(R) Core(TM) i9-10980XE it took about 15 minutes. 3. Command example for optimizing -OpenVINOâ„¢ model (.xml) with OpenVINOâ„¢ POT. +OpenVINOâ„¢ model (.xml) with OpenVINOâ„¢ PTQ. .. code-block:: (otx) ...$ otx optimize --load-weights openvino_model/openvino.xml \ - --output pot_model + --output ptq_model ... Performance(score: 0.9577656675749319, dashboard: (1 metric groups)) -Please note, that POT will take some time (generally less than NNCF optimization) without logging to optimize the model. +Please note, that PTQ will take some time (generally less than NNCF optimization) without logging to optimize the model. 4. Now we have fully trained, optimized and exported an efficient model representation ready-to-use semantic segmentation model. diff --git a/src/otx/cli/tools/eval.py b/src/otx/cli/tools/eval.py index 2ed3b22a477..f210ba4a395 100644 --- a/src/otx/cli/tools/eval.py +++ b/src/otx/cli/tools/eval.py @@ -49,8 +49,8 @@ def get_args(): ) parser.add_argument( "--load-weights", - help="Load model weights from previously saved checkpoint." - "It could be a trained/optimized model (POT only) or exported model.", + 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", From 555df97c1d12dad8ece0916eb0b7812e7aeb5b9e Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 27 Nov 2023 10:22:45 +0900 Subject: [PATCH 133/146] Update regression tests for develop (#2652) * Update regression tests (#2556) * update reg tests * update test suit * update regression criteria --------- Co-authored-by: Eunwoo Shin --- .github/workflows/run_tests_in_tox.yml | 3 +- .github/workflows/weekly.yml | 9 +- .../action/test_action_classification.py | 27 +- .../action/test_action_detection.py | 14 +- .../anomaly/test_anomaly_classificaiton.py | 55 +- .../anomaly/test_anomaly_detection.py | 56 +- .../anomaly/test_anomaly_segmentation.py | 55 +- .../classification/test_classification.py | 161 +- tests/regression/conftest.py | 9 +- tests/regression/detection/test_detection.py | 52 +- .../detection/test_tiling_detection.py | 39 +- .../test_instance_segmentation.py | 43 +- .../test_tiling_instance_segmentation.py | 40 +- tests/regression/regression_command.py | 129 +- tests/regression/regression_config.json | 2963 +++++++---------- tests/regression/regression_test_helpers.py | 106 +- .../test_segmentation.py | 69 +- tests/regression/summarize_test_results.py | 142 +- tests/test_suite/run_test_command.py | 195 +- tox.ini | 1 + 20 files changed, 1846 insertions(+), 2322 deletions(-) diff --git a/.github/workflows/run_tests_in_tox.yml b/.github/workflows/run_tests_in_tox.yml index aaa00970b74..fac265aede3 100644 --- a/.github/workflows/run_tests_in_tox.yml +++ b/.github/workflows/run_tests_in_tox.yml @@ -56,7 +56,6 @@ jobs: 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/tests-reg_tiling_${{ inputs.task }}_*.csv + .tox/tests-reg_${{ inputs.task }}*.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/weekly.yml b/.github/workflows/weekly.yml index 875dfc1e0f0..e492737431b 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -14,31 +14,24 @@ jobs: include: - toxenv_task: "iseg" test_dir: "tests/regression/instance_segmentation/test_instance_segmentation.py" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "instance_segmentation" - toxenv_task: "iseg_t" test_dir: "tests/regression/instance_segmentation/test_tiling_instance_segmentation.py" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "instance_segmentation" - toxenv_task: "seg" test_dir: "tests/regression/semantic_segmentation" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "segmentation" - toxenv_task: "det" test_dir: "tests/regression/detection" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "detection" - toxenv_task: "ano" test_dir: "tests/regression/anomaly" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "anomaly" - toxenv_task: "act" test_dir: "tests/regression/action" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "action" - toxenv_task: "cls" test_dir: "tests/regression/classification" - runs_on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: "classification" name: Regression-Test-py310-${{ matrix.toxenv_task }} uses: ./.github/workflows/run_tests_in_tox.yml @@ -47,7 +40,7 @@ jobs: toxenv-pyver: "py310" toxenv-task: ${{ matrix.toxenv_task }} tests-dir: ${{ matrix.test_dir }} - runs-on: ${{ matrix.runs_on }} + runs-on: "['self-hosted', 'Linux', 'X64', 'dmount']" task: ${{ matrix.task }} timeout-minutes: 8640 upload-artifact: true diff --git a/tests/regression/action/test_action_classification.py b/tests/regression/action/test_action_classification.py index c953ad9a9cc..6f2595700ba 100644 --- a/tests/regression/action/test_action_classification.py +++ b/tests/regression/action/test_action_classification.py @@ -45,20 +45,19 @@ class TestRegressionActionClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -66,6 +65,7 @@ def setup_method(self): @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 @@ -79,14 +79,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -94,6 +94,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -115,6 +117,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): 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 @@ -129,7 +132,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -137,10 +140,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) - + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @e2e_pytest_component @@ -148,6 +148,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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 @@ -161,7 +162,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -169,6 +170,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + 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 index 14196f8e2fe..f7dd494dcf8 100644 --- a/tests/regression/action/test_action_detection.py +++ b/tests/regression/action/test_action_detection.py @@ -47,20 +47,19 @@ class TestRegressionActionDetection: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -68,6 +67,7 @@ def setup_method(self): @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 @@ -81,14 +81,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -96,6 +96,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], diff --git a/tests/regression/anomaly/test_anomaly_classificaiton.py b/tests/regression/anomaly/test_anomaly_classificaiton.py index 1cff676fa91..ae928e7997b 100644 --- a/tests/regression/anomaly/test_anomaly_classificaiton.py +++ b/tests/regression/anomaly/test_anomaly_classificaiton.py @@ -11,6 +11,15 @@ 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, @@ -25,16 +34,6 @@ 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 TestRegressionAnomalyClassification: # Configurations for regression test. @@ -44,7 +43,7 @@ class TestRegressionAnomalyClassification: LABEL_TYPE = None TRAIN_PARAMS = None - SAMPLED_ANOMALY_DATASET_CATEGORIES = random.sample(ANOMALY_DATASET_CATEGORIES, 3) + 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] @@ -54,20 +53,19 @@ class TestRegressionAnomalyClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + 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 = {} @@ -87,6 +85,7 @@ def _apply_category(self, data_dict, category): @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) @@ -101,14 +100,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - reg_cfg.config_dict["regression_criteria"]["train"][category], + 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.result_dict[reg_cfg.task_type]["train"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -118,6 +117,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): 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( @@ -142,6 +143,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template, category): 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) @@ -157,7 +159,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor reg_cfg.otx_dir, category_data_args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -165,7 +167,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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.result_dict[reg_cfg.task_type]["export"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -175,6 +177,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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) @@ -190,7 +193,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ reg_cfg.otx_dir, category_data_args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -198,7 +201,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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.result_dict[reg_cfg.task_type]["deploy"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -208,6 +211,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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) @@ -226,7 +230,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): reg_cfg.otx_dir, category_data_args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -234,7 +238,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["nncf"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -242,6 +246,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): @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) @@ -256,7 +261,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -264,6 +269,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["ptq"][category].append(self.performance) + 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 index 688f140e3dd..e638a88ad28 100644 --- a/tests/regression/anomaly/test_anomaly_detection.py +++ b/tests/regression/anomaly/test_anomaly_detection.py @@ -11,6 +11,15 @@ 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, @@ -25,16 +34,6 @@ 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 TestRegressionAnomalyDetection: # Configurations for regression test. @@ -44,7 +43,7 @@ class TestRegressionAnomalyDetection: LABEL_TYPE = None TRAIN_PARAMS = None - SAMPLED_ANOMALY_DATASET_CATEGORIES = random.sample(ANOMALY_DATASET_CATEGORIES, 3) + 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] @@ -54,20 +53,19 @@ class TestRegressionAnomalyDetection: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + 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 = {} @@ -87,6 +85,7 @@ def _apply_category(self, data_dict, category): @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) @@ -101,14 +100,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - reg_cfg.config_dict["regression_criteria"]["train"][category], + 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.result_dict[reg_cfg.task_type]["train"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -118,6 +117,9 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): 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], @@ -141,6 +143,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template, category): 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) @@ -156,7 +159,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor reg_cfg.otx_dir, category_data_args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -164,7 +167,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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.result_dict[reg_cfg.task_type]["export"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -174,6 +177,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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) @@ -189,7 +193,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ reg_cfg.otx_dir, category_data_args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -197,7 +201,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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.result_dict[reg_cfg.task_type]["deploy"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -207,6 +211,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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) @@ -225,7 +230,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): reg_cfg.otx_dir, category_data_args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -233,7 +238,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["nncf"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -243,6 +248,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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) @@ -257,7 +263,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -265,6 +271,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["ptq"][category].append(self.performance) + 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 index 83383569898..13f90320aab 100644 --- a/tests/regression/anomaly/test_anomaly_segmentation.py +++ b/tests/regression/anomaly/test_anomaly_segmentation.py @@ -11,6 +11,15 @@ 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, @@ -25,16 +34,6 @@ 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 TestRegressionAnomalySegmentation: # Configurations for regression test. @@ -44,7 +43,7 @@ class TestRegressionAnomalySegmentation: LABEL_TYPE = None TRAIN_PARAMS = None - SAMPLED_ANOMALY_DATASET_CATEGORIES = random.sample(ANOMALY_DATASET_CATEGORIES, 3) + 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] @@ -54,20 +53,19 @@ class TestRegressionAnomalySegmentation: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + 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 = {} @@ -87,6 +85,7 @@ def _apply_category(self, data_dict, category): @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) @@ -101,14 +100,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - reg_cfg.config_dict["regression_criteria"]["train"][category], + 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.result_dict[reg_cfg.task_type]["train"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -118,6 +117,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path, category): 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( @@ -142,6 +143,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template, category): 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) @@ -157,7 +159,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor reg_cfg.otx_dir, category_data_args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -165,7 +167,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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.result_dict[reg_cfg.task_type]["export"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -175,6 +177,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path, categor 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) @@ -190,7 +193,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ reg_cfg.otx_dir, category_data_args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -198,7 +201,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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.result_dict[reg_cfg.task_type]["deploy"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -208,6 +211,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path, categ 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) @@ -226,7 +230,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): reg_cfg.otx_dir, category_data_args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -234,7 +238,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["nncf"][category].append(self.performance) + reg_cfg.update_result(test_type, self.performance, is_anomaly=True, category=category) assert test_result["passed"] is True, test_result["log"] @@ -244,6 +248,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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) @@ -258,7 +263,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): tmp_dir_path, reg_cfg.otx_dir, category_data_args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"][category], + criteria=reg_cfg.config_dict["regression_criteria"][test_type][category], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -266,6 +271,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path, category): 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.result_dict[reg_cfg.task_type]["ptq"][category].append(self.performance) + 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 index b03c91d2f70..83a7c26627f 100644 --- a/tests/regression/classification/test_classification.py +++ b/tests/regression/classification/test_classification.py @@ -53,20 +53,19 @@ class TestRegressionMultiClassClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -74,6 +73,7 @@ def setup_method(self): @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" @@ -87,14 +87,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -102,6 +102,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -121,7 +123,10 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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") @@ -146,14 +151,14 @@ def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, args_cls_incr, - config_cls_incr["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -164,6 +169,8 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): 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"], @@ -184,6 +191,7 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): @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" @@ -211,14 +219,14 @@ def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, args_semisl, - config_semisl["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -229,6 +237,8 @@ def test_otx_train_semisl_kpi_test(self, reg_cfg, template): 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"], @@ -251,6 +261,7 @@ 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" @@ -295,14 +306,14 @@ def test_otx_train_selfsl(self, reg_cfg, template, tmp_dir_path): new_tmp_dir_path, reg_cfg.otx_dir, args_selfsl, - config_selfsl["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -313,6 +324,8 @@ def test_otx_train_selfsl_kpi_test(self, reg_cfg, template): 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"], @@ -332,6 +345,7 @@ def test_otx_train_selfsl_kpi_test(self, reg_cfg, template): @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" @@ -346,7 +360,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -354,15 +368,14 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + 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" @@ -377,7 +390,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -385,15 +398,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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" @@ -411,7 +423,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -419,13 +431,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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" @@ -439,7 +452,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -447,7 +460,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -467,20 +480,19 @@ class TestRegressionMultiLabelClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -488,6 +500,7 @@ def setup_method(self): @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" @@ -501,14 +514,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -516,6 +529,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -536,6 +551,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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") @@ -560,14 +576,14 @@ def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, args_cls_incr, - config_cls_incr["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -578,6 +594,8 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): 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"], @@ -597,6 +615,7 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): @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" @@ -611,7 +630,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -619,15 +638,14 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + 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" @@ -642,7 +660,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -650,15 +668,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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" @@ -676,7 +693,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -684,13 +701,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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" @@ -704,7 +722,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -712,7 +730,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -732,20 +750,19 @@ class TestRegressionHierarchicalLabelClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -753,6 +770,7 @@ def setup_method(self): @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" @@ -766,14 +784,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -781,6 +799,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -800,6 +820,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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" @@ -814,7 +835,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -822,15 +843,13 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) - + 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" @@ -845,7 +864,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -853,15 +872,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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" @@ -879,7 +897,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -887,13 +905,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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" @@ -907,7 +926,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -915,7 +934,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -940,20 +959,19 @@ class TestRegressionSupconClassification: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -961,6 +979,9 @@ def setup_method(self): @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" @@ -977,14 +998,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -992,6 +1013,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], diff --git a/tests/regression/conftest.py b/tests/regression/conftest.py index c1747a5dc53..37f1bc0d379 100644 --- a/tests/regression/conftest.py +++ b/tests/regression/conftest.py @@ -8,10 +8,13 @@ @pytest.fixture(autouse=True, scope="session") def run_regression_tests(tmp_dir_path): - print(f"tmp dir path = {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 - input_path = os.path.join(tmp_dir_path, "regression_test_results") output_path = os.environ.get("TOX_WORK_DIR", os.getcwd()) - summarize_results_data(input_path, output_path) + summarize_results_data(result_path, output_path) diff --git a/tests/regression/detection/test_detection.py b/tests/regression/detection/test_detection.py index 6ab44ced7fa..c6b5a508d26 100644 --- a/tests/regression/detection/test_detection.py +++ b/tests/regression/detection/test_detection.py @@ -52,20 +52,19 @@ class TestRegressionDetection: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -73,6 +72,7 @@ def setup_method(self): @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 @@ -86,14 +86,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -101,6 +101,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -121,6 +123,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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) @@ -152,7 +155,7 @@ def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -162,6 +165,8 @@ 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"], @@ -181,9 +186,10 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): @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] = {} - train_type = "semi_supervised" 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"] @@ -215,7 +221,7 @@ def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -225,6 +231,8 @@ 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"], @@ -244,6 +252,7 @@ def test_otx_train_semisl_kpi_test(self, reg_cfg, template): @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 @@ -258,7 +267,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -266,15 +275,14 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + 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 @@ -289,7 +297,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -297,15 +305,16 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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 @@ -323,7 +332,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -331,13 +340,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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 @@ -351,7 +361,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -359,6 +369,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + 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 index 4155580f947..b25f8990675 100644 --- a/tests/regression/detection/test_tiling_detection.py +++ b/tests/regression/detection/test_tiling_detection.py @@ -58,21 +58,19 @@ class TestRegressionTilingDetection: @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, - result_dir="tiling", - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -80,6 +78,7 @@ def setup_method(self): @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 @@ -93,14 +92,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -108,6 +107,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -127,6 +128,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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 @@ -141,7 +143,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -149,15 +151,14 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + 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 @@ -172,7 +173,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -180,15 +181,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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 @@ -206,7 +206,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -214,13 +214,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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 @@ -234,7 +235,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -242,6 +243,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + 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 index 45666dedcc5..14029c56b7c 100644 --- a/tests/regression/instance_segmentation/test_instance_segmentation.py +++ b/tests/regression/instance_segmentation/test_instance_segmentation.py @@ -52,20 +52,19 @@ class TestRegressionInstanceSegmentation: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -73,6 +72,7 @@ def setup_method(self): @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 @@ -86,14 +86,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -101,6 +101,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -121,6 +123,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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) @@ -152,7 +155,7 @@ def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -162,6 +165,8 @@ 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"], @@ -182,6 +187,7 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): @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 @@ -196,7 +202,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -204,9 +210,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -214,6 +218,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): @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 @@ -228,7 +233,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -236,15 +241,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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 @@ -262,7 +266,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -270,13 +274,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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 @@ -290,7 +295,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -298,6 +303,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + 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 index 0dda9f7729c..5c45a0983cd 100644 --- a/tests/regression/instance_segmentation/test_tiling_instance_segmentation.py +++ b/tests/regression/instance_segmentation/test_tiling_instance_segmentation.py @@ -58,21 +58,19 @@ class TestRegressionTilingInstanceSegmentation: @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, - result_dir="tiling", - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -81,6 +79,7 @@ def setup_method(self): @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 @@ -94,14 +93,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -110,6 +109,9 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"]], @@ -129,6 +131,7 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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 @@ -143,7 +146,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -151,9 +154,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -161,6 +162,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): @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 @@ -175,7 +177,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -183,9 +185,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -193,6 +193,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): @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 @@ -210,7 +211,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -218,7 +219,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -226,6 +227,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): @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 @@ -239,7 +241,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -247,6 +249,6 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + 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 index 121f2c33efb..1be19e172cd 100644 --- a/tests/regression/regression_command.py +++ b/tests/regression/regression_command.py @@ -44,20 +44,22 @@ def regression_eval_testing( with open(performance_json_path) as read_file: trained_performance = json.load(read_file) - model_criteria = criteria[template.name] if template.name in criteria.keys() else 0.0 - modified_criteria = model_criteria - (model_criteria * threshold) for k in trained_performance.keys(): result_dict[k] = round(trained_performance[k], 3) - if trained_performance[k] < modified_criteria: + model_criteria = 0.0 + if template.name not in criteria.keys(): regression_result["passed"] = False - regression_result["log"] = f"Performance: ({trained_performance[k]}) < Criteria: ({modified_criteria})." - regression_result["raw"] = { - "metric": k, - "performance": trained_performance[k], - "template": template.name, - "criteria": model_criteria, - "threshold": threshold, - } + 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}), 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 @@ -115,36 +117,18 @@ def regression_openvino_testing( with open(perf_path) as read_file: exported_performance = json.load(read_file) - model_criteria = 0.0 # set default model critera for not existing reg config - if template.name not in criteria.keys(): - regression_result["passed"] = False - log_msg = ( - f"Cannot find regression criteria for the template '{template.name}'. " - + f"train_performance = {trained_performance}, export_performance = {exported_performance}" - ) - regression_result["log"] = log_msg - print(log_msg) - return regression_result - - if isinstance(criteria, dict): - model_criteria = criteria[template.name] * (1.0 - reg_threshold) - 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] < model_criteria: - regression_result["passed"] = False - regression_result[ - "log" - ] = f"Export performance: ({exported_performance[k]}) < Criteria: ({model_criteria})." - 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"{trained_performance[k]=}, {exported_performance[k]=}" + regression_result[ + "log" + ] = f"[{template.name}] {trained_performance[k]=}, {exported_performance[k]=}, {threshold=}" return regression_result @@ -177,26 +161,18 @@ def regression_deployment_testing( with open(f"{template_work_dir}/deployed_{template.model_template_id}/performance.json") as read_file: deployed_performance = json.load(read_file) - if isinstance(criteria, dict) and template.name in criteria.keys(): - model_criteria = criteria[template.name] - modified_criteria = model_criteria - (model_criteria * reg_threshold) - for k in exported_performance.keys(): if k == "avg_time_per_image": continue - if isinstance(criteria, dict) and template.name in criteria.keys(): - result_dict[k] = round(deployed_performance[k], 3) - if deployed_performance[k] < modified_criteria: - regression_result["passed"] = False - regression_result[ - "log" - ] = f"Deploy performance: ({deployed_performance[k]}) < Criteria: ({modified_criteria})." + 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"{exported_performance[k]=}, {deployed_performance[k]=}" + regression_result[ + "log" + ] = f"[{template.name}] {exported_performance[k]=}, {deployed_performance[k]=}, {threshold=}" return regression_result @@ -229,24 +205,32 @@ def regression_nncf_eval_testing( with open(f"{template_work_dir}/nncf_{template.model_template_id}/performance.json") as read_file: evaluated_performance = json.load(read_file) - if isinstance(criteria, dict) and template.name in criteria.keys(): - model_criteria = criteria[template.name] - modified_criteria = model_criteria - (model_criteria * reg_threshold) - for k in trained_performance.keys(): - if isinstance(criteria, dict) and template.name in criteria.keys(): - result_dict[k] = round(evaluated_performance[k], 3) - if evaluated_performance[k] < modified_criteria: + 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"NNCF performance: ({evaluated_performance[k]}) < Criteria: ({modified_criteria})." - if ( - evaluated_performance[k] < trained_performance[k] - and abs(trained_performance[k] - evaluated_performance[k]) / (trained_performance[k] + 1e-10) > threshold - ): - regression_result["passed"] = False - regression_result["log"] = f"{trained_performance[k]=}, {evaluated_performance[k]=}" + ] = f"[{template.name}] NNCF performance is lower than criteria: {evaluated_performance[k]=}, {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]=}, {train_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]=}, {trained_performance[k]=}, {threshold=}" return regression_result @@ -276,16 +260,21 @@ def regression_ptq_eval_testing(template, root, otx_dir, args, criteria=None, re with open(f"{template_work_dir}/ptq_{template.model_template_id}/performance.json") as read_file: ptq_performance = json.load(read_file) - if isinstance(criteria, dict) and template.name in criteria.keys(): - model_criteria = criteria[template.name] - modified_criteria = model_criteria - (model_criteria * reg_threshold) - for k in ptq_performance.keys(): - if isinstance(criteria, dict) and template.name in criteria.keys(): - result_dict[k] = round(ptq_performance[k], 3) - if ptq_performance[k] < modified_criteria: + 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"POT performance: ({ptq_performance[k]}) < Criteria: ({modified_criteria})." + regression_result[ + "log" + ] = f"[{template.name}] ptq performance: {ptq_performance[k]=}, {model_criteria=}, {reg_threshold=}" return regression_result @@ -305,7 +294,9 @@ def regression_train_time_testing(train_time_criteria, e2e_train_time, template, if e2e_train_time > modified_train_criteria: regression_result["passed"] = False - regression_result["log"] = f"Train time: ({e2e_train_time}) < Criteria: ({modified_train_criteria})." + regression_result[ + "log" + ] = f"[{template.name}] Train time: ({e2e_train_time}) < Criteria: ({modified_train_criteria})." return regression_result @@ -325,6 +316,8 @@ def regression_eval_time_testing(eval_time_criteria, e2e_eval_time, template, th if e2e_eval_time > modified_eval_criteria: regression_result["passed"] = False - regression_result["log"] = f"Eval time: ({e2e_eval_time}) < criteria: ({modified_eval_criteria})." + 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 index 20d3e3a50e5..6429a5fd1e2 100644 --- a/tests/regression/regression_config.json +++ b/tests/regression/regression_config.json @@ -3,10 +3,10 @@ "classification": { "supervised": { "multi_class": { - "--train-data-roots": "classification/cifar10_subset_cls_decr/train", - "--val-data-roots": "classification/cifar10_subset_cls_decr/test", - "--test-data-roots": "classification/cifar10_subset_cls_decr/test", - "--input": "classification/cifar10_subset/test/airplane" + "--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", @@ -21,16 +21,16 @@ "--input": "classification/h_label_cifar10_subset/images/test/airplane" }, "supcon": { - "--train-data-roots": "classification/cifar10_subset_cls_decr/train", - "--val-data-roots": "classification/cifar10_subset_cls_decr/test", - "--test-data-roots": "classification/cifar10_subset_cls_decr/test" + "--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/cifar10_subset/train", - "--val-data-roots": "classification/cifar10_subset/test", - "--test-data-roots": "classification/cifar10_subset/test" + "--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", @@ -40,15 +40,15 @@ }, "semi_supervised": { "multi_class": { - "--train-data-roots": "classification/cifar10_subset_cls_decr/train", - "--val-data-roots": "classification/cifar10_subset_cls_decr/test", - "--test-data-roots": "classification/cifar10_subset_cls_decr/test", - "--unlabeled-data-roots": "classification/cifar10_unlabeled" + "--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/cifar10_subset_cls_decr/train" + "--train-data-roots": "classification/multiclass_CUB_cls_decr/train" } } }, @@ -185,336 +185,716 @@ "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.772, - "MobileNet-V3-large-1x": 0.728, - "EfficientNet-B0": 0.806, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.699, + "MobileNet-V3-large-1x": 0.687, + "DeiT-Tiny": 0.596 }, "export": { - "EfficientNet-V2-S": 0.772, - "MobileNet-V3-large-1x": 0.728, - "EfficientNet-B0": 0.806, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.698, + "MobileNet-V3-large-1x": 0.686, + "DeiT-Tiny": 0.597 }, "deploy": { - "EfficientNet-V2-S": 0.772, - "MobileNet-V3-large-1x": 0.728, - "EfficientNet-B0": 0.806, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.778, + "EfficientNet-B0": 0.698, + "MobileNet-V3-large-1x": 0.686, + "DeiT-Tiny": 0.597 }, "nncf": { - "EfficientNet-V2-S": 0.764, - "MobileNet-V3-large-1x": 0.733, - "EfficientNet-B0": 0.742, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.776, + "EfficientNet-B0": 0.691, + "MobileNet-V3-large-1x": 0.677, + "DeiT-Tiny": 0.0 }, "ptq": { - "EfficientNet-V2-S": 0.711, - "MobileNet-V3-large-1x": 0.692, - "EfficientNet-B0": 0.725, - "Deti-Tiny": 0.0 + "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, - "MobileNet-V3-large-1x": 0.966, - "EfficientNet-B0": 0.965, - "Deti-Tiny": 0.0 + "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, - "EfficientNet-B0": 0.965, - "Deti-Tiny": 0.0 + "DeiT-Tiny": 0.952 }, "deploy": { "EfficientNet-V2-S": 0.968, + "EfficientNet-B0": 0.958, "MobileNet-V3-large-1x": 0.965, - "EfficientNet-B0": 0.965, - "Deti-Tiny": 0.0 + "DeiT-Tiny": 0.952 }, "nncf": { - "EfficientNet-V2-S": 0.967, + "EfficientNet-V2-S": 0.971, + "EfficientNet-B0": 0.961, "MobileNet-V3-large-1x": 0.965, - "EfficientNet-B0": 0.965, - "Deti-Tiny": 0.0 + "DeiT-Tiny": 0.0 }, "ptq": { - "EfficientNet-V2-S": 0.968, + "EfficientNet-V2-S": 0.971, + "EfficientNet-B0": 0.96, "MobileNet-V3-large-1x": 0.965, - "EfficientNet-B0": 0.964, - "Deti-Tiny": 0.0 + "DeiT-Tiny": 0.952 } }, "h_label": { "train": { - "EfficientNet-V2-S": 0.602, - "MobileNet-V3-large-1x": 0.547, - "EfficientNet-B0": 0.636, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 }, "export": { - "EfficientNet-V2-S": 0.602, - "MobileNet-V3-large-1x": 0.547, - "EfficientNet-B0": 0.636, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.761, + "EfficientNet-B0": 0.737, + "MobileNet-V3-large-1x": 0.736, + "DeiT-Tiny": 0.768 }, "deploy": { - "EfficientNet-V2-S": 0.602, - "MobileNet-V3-large-1x": 0.547, - "EfficientNet-B0": 0.636, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.761, + "EfficientNet-B0": 0.737, + "MobileNet-V3-large-1x": 0.736, + "DeiT-Tiny": 0.768 }, "nncf": { - "EfficientNet-V2-S": 0.594, - "MobileNet-V3-large-1x": 0.622, - "EfficientNet-B0": 0.638, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 }, "ptq": { - "EfficientNet-V2-S": 0.577, - "MobileNet-V3-large-1x": 0.472, - "EfficientNet-B0": 0.605, - "Deti-Tiny": 0.0 + "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.783, - "MobileNet-V3-large-1x": 0.728, - "EfficientNet-B0": 0.772, - "Deti-Tiny": 0.0 - } - } - }, - "class_incr": { - "multi_class": { - "train": { - "EfficientNet-V2-S": 0.79, - "MobileNet-V3-large-1x": 0.76, - "EfficientNet-B0": 0.81, - "Deti-Tiny": 0.0 - } - }, - "multi_label": { - "train": { - "EfficientNet-V2-S": 0.97, - "MobileNet-V3-large-1x": 0.97, - "EfficientNet-B0": 0.964, - "Deti-Tiny": 0.0 - } - }, - "h_label": { - "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, - "MobileNet-V3-large-1x": 0.0, "EfficientNet-B0": 0.0, - "Deti-Tiny": 0.0 + "MobileNet-V3-large-1x": 0.0 } } }, "semi_supervised": { "multi_class": { "train": { - "EfficientNet-V2-S": 0.822, - "MobileNet-V3-large-1x": 0.764, - "EfficientNet-B0": 0.797, - "Deti-Tiny": 0.0 - } - } - }, - "self_supervised": { - "multi_class": { - "train": { - "EfficientNet-V2-S": 0.767, - "MobileNet-V3-large-1x": 0.744, - "EfficientNet-B0": 0.794, - "Deti-Tiny": 0.0 - } - } - } - }, - "detection": { - "supervised": { - "multi_class": { - "train": { - "YOLOX": 0.537, - "SSD": 0.179, - "MobileNetV2-ATSS": 0.446 + "EfficientNet-V2-S": 0.758, + "EfficientNet-B0": 0.674, + "MobileNet-V3-large-1x": 0.658, + "DeiT-Tiny": 0.656 }, "export": { - "YOLOX": 0.534, - "SSD": 0.179, - "MobileNetV2-ATSS": 0.455 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 }, "deploy": { - "YOLOX": 0.534, - "SSD": 0.179, - "MobileNetV2-ATSS": 0.455 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 }, "nncf": { - "YOLOX": 0.517, - "SSD": 0.181, - "MobileNetV2-ATSS": 0.446 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 }, "ptq": { - "YOLOX": 0.531, - "SSD": 0.18, - "MobileNetV2-ATSS": 0.458 - } - } - }, - "class_incr": { - "multi_class": { - "train": { - "YOLOX": 0.532, - "SSD": 0.2, - "MobileNetV2-ATSS": 0.443 - } - } - }, - "semi_supervised": { - "multi_class": { - "train": { - "YOLOX": 0.398, - "SSD": 0.142, - "MobileNetV2-ATSS": 0.414 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0, + "DeiT-Tiny": 0.0 } } }, - "tiling": { + "self_supervised": { "multi_class": { "train": { - "YOLOX": 0.545, - "SSD": 0.19, - "MobileNetV2-ATSS": 0.459 + "EfficientNet-V2-S": 0.776, + "EfficientNet-B0": 0.687, + "MobileNet-V3-large-1x": 0.662 }, "export": { - "YOLOX": 0.549, - "SSD": 0.19, - "MobileNetV2-ATSS": 0.469 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "deploy": { - "YOLOX": 0.549, - "SSD": 0.19, - "MobileNetV2-ATSS": 0.469 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "nncf": { - "YOLOX": 0.0, - "SSD": 0.0, - "MobileNetV2-ATSS": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "ptq": { - "YOLOX": 0.531, - "SSD": 0.192, - "MobileNetV2-ATSS": 0.459 - } + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 + } } - } - }, - "segmentation": { - "supervised": { + }, + "class_incr": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 0.636, - "Lite-HRNet-18-mod2": 0.692, - "Lite-HRNet-18": 0.801, - "Lite-HRNet-x-mod3": 0.693, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "EfficientNet-V2-S": 0.783, + "EfficientNet-B0": 0.702, + "MobileNet-V3-large-1x": 0.687 }, "export": { - "Lite-HRNet-s-mod2": 0.636, - "Lite-HRNet-18-mod2": 0.691, - "Lite-HRNet-18": 0.8, - "Lite-HRNet-x-mod3": 0.692, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "deploy": { - "Lite-HRNet-s-mod2": 0.636, - "Lite-HRNet-18-mod2": 0.691, - "Lite-HRNet-18": 0.8, - "Lite-HRNet-x-mod3": 0.692, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "nncf": { - "Lite-HRNet-s-mod2": 0.678, - "Lite-HRNet-18-mod2": 0.707, - "Lite-HRNet-18": 0.79, - "Lite-HRNet-x-mod3": 0.668, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 }, "ptq": { - "Lite-HRNet-s-mod2": 0.725, - "Lite-HRNet-18-mod2": 0.269, - "Lite-HRNet-18": 0.115, - "Lite-HRNet-x-mod3": 0.041, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "EfficientNet-V2-S": 0.0, + "EfficientNet-B0": 0.0, + "MobileNet-V3-large-1x": 0.0 } }, - "supcon": { + "multi_label": { "train": { - "Lite-HRNet-s-mod2": 0.691, - "Lite-HRNet-18-mod2": 0.676, - "Lite-HRNet-18": 0.682, - "Lite-HRNet-x-mod3": 0.698, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "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 } } }, - "class_incr": { + "supervised": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 0.66, - "Lite-HRNet-18-mod2": 0.768, - "Lite-HRNet-18": 0.669, - "Lite-HRNet-x-mod3": 0.712, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "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": { - "Lite-HRNet-s-mod2": 0.681, - "Lite-HRNet-18-mod2": 0.696, - "Lite-HRNet-18": 0.742, - "Lite-HRNet-x-mod3": 0.802, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "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 } } }, - "self_supervised": { + "class_incr": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 0.785, - "Lite-HRNet-18-mod2": 0.798, - "Lite-HRNet-18": 0.782, - "Lite-HRNet-x-mod3": 0.785, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "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 } } } @@ -523,79 +903,285 @@ "supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 0.466, - "MaskRCNN-EfficientNetB2B": 0.27, - "MaskRCNN-SwinT-FP16": 0.438 + "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-ResNet50": 0.0, + "MaskRCNN-SwinT-FP16": 0.0, "MaskRCNN-EfficientNetB2B": 0.0, - "MaskRCNN-SwinT-FP16": 0.438 + "MaskRCNN-ResNet50": 0.0 }, "deploy": { - "MaskRCNN-ResNet50": 0.0, + "MaskRCNN-SwinT-FP16": 0.0, "MaskRCNN-EfficientNetB2B": 0.0, - "MaskRCNN-SwinT-FP16": 0.438 + "MaskRCNN-ResNet50": 0.0 }, "nncf": { - "MaskRCNN-ResNet50": 0.437, - "MaskRCNN-EfficientNetB2B": 0.286, - "MaskRCNN-SwinT-FP16": 0.438 + "MaskRCNN-SwinT-FP16": 0.0, + "MaskRCNN-EfficientNetB2B": 0.0, + "MaskRCNN-ResNet50": 0.0 }, "ptq": { - "MaskRCNN-ResNet50": 0.0, + "MaskRCNN-SwinT-FP16": 0.0, "MaskRCNN-EfficientNetB2B": 0.0, - "MaskRCNN-SwinT-FP16": 0.438 + "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 } } }, - "class_incr": { + "semi_supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 0.481, - "MaskRCNN-EfficientNetB2B": 0.307 + "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 } } }, - "tiling": { + "self_supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 0.471, - "MaskRCNN-EfficientNetB2B": 0.304 + "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": { - "MaskRCNN-ResNet50": 0.0, - "MaskRCNN-EfficientNetB2B": 0.0 + "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": { - "MaskRCNN-ResNet50": 0.0, - "MaskRCNN-EfficientNetB2B": 0.0 + "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": { - "MaskRCNN-ResNet50": 0.0, - "MaskRCNN-EfficientNetB2B": 0.0 + "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": { - "MaskRCNN-ResNet50": 0.0, - "MaskRCNN-EfficientNetB2B": 0.0 + "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 } } - } - }, - "action_classification": { - "supervised": { + }, + "class_incr": { "multi_class": { "train": { - "X3D": 0.612, - "MoViNet": 0.492 + "Lite-HRNet-18": 0.635, + "Lite-HRNet-18-mod2": 0.656, + "Lite-HRNet-s-mod2": 0.639, + "Lite-HRNet-x-mod3": 0.565 }, "export": { - "X3D": 0.589, - "MoViNet": 0.0 + "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": { - "X3D": 0.58, - "MoViNet": 0.0 + "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 } } } @@ -604,1166 +1190,271 @@ "supervised": { "multi_class": { "train": { - "X3D_FAST_RCNN": 0.59 + "X3D_FAST_RCNN": 1324.578 } } } }, - "anomaly_classification": { + "anomaly_segmentation": { "train": { - "bottle": { - "STFPM": 0.759, - "PADIM": 0.94 - }, - "cable": { - "STFPM": 0.613, - "PADIM": 0.68 - }, - "capsule": { - "STFPM": 0.826, - "PADIM": 0.856 - }, "carpet": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "grid": { - "STFPM": 0.756, - "PADIM": 0.744 - }, - "hazelnut": { - "STFPM": 0.827, - "PADIM": 0.891 - }, - "leather": { - "STFPM": 0.887, - "PADIM": 0.815 - }, - "metal_nut": { - "STFPM": 0.809, - "PADIM": 0.835 - }, - "pill": { - "STFPM": 0.844, - "PADIM": 0.844 - }, - "screw": { - "STFPM": 0.762, - "PADIM": 0.744 - }, - "tile": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "toothbrush": { - "STFPM": 0.714, - "PADIM": 0.762 - }, - "transistor": { - "STFPM": 0.53, - "PADIM": 0.66 + "STFPM": 606.461, + "PADIM": 51.744 }, "wood": { - "STFPM": 0.899, - "PADIM": 0.949 + "STFPM": 843.012, + "PADIM": 54.485 }, "zipper": { - "STFPM": 0.854, - "PADIM": 0.848 + "STFPM": 570.453, + "PADIM": 52.987 } - }, - "export": { - "bottle": { - "STFPM": 0.759, - "PADIM": 0.94 - }, - "cable": { - "STFPM": 0.613, - "PADIM": 0.68 - }, - "capsule": { - "STFPM": 0.826, - "PADIM": 0.856 - }, + } + }, + "anomaly_detection": { + "train": { "carpet": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "grid": { - "STFPM": 0.756, - "PADIM": 0.744 - }, - "hazelnut": { - "STFPM": 0.827, - "PADIM": 0.891 - }, - "leather": { - "STFPM": 0.887, - "PADIM": 0.815 - }, - "metal_nut": { - "STFPM": 0.809, - "PADIM": 0.835 - }, - "pill": { - "STFPM": 0.844, - "PADIM": 0.844 - }, - "screw": { - "STFPM": 0.762, - "PADIM": 0.744 - }, - "tile": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "toothbrush": { - "STFPM": 0.714, - "PADIM": 0.762 - }, - "transistor": { - "STFPM": 0.53, - "PADIM": 0.66 + "STFPM": 380.418, + "PADIM": 35.742 }, "wood": { - "STFPM": 0.899, - "PADIM": 0.949 + "STFPM": 233.758, + "PADIM": 35.502 }, "zipper": { - "STFPM": 0.854, - "PADIM": 0.848 - } - }, - "deploy": { - "bottle": { - "STFPM": 0.759, - "PADIM": 0.94 - }, - "cable": { - "STFPM": 0.613, - "PADIM": 0.68 - }, - "capsule": { - "STFPM": 0.826, - "PADIM": 0.856 - }, - "carpet": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "grid": { - "STFPM": 0.756, - "PADIM": 0.744 - }, - "hazelnut": { - "STFPM": 0.827, - "PADIM": 0.891 - }, - "leather": { - "STFPM": 0.887, - "PADIM": 0.815 - }, - "metal_nut": { - "STFPM": 0.809, - "PADIM": 0.835 - }, - "pill": { - "STFPM": 0.844, - "PADIM": 0.844 - }, - "screw": { - "STFPM": 0.762, - "PADIM": 0.744 - }, - "tile": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "toothbrush": { - "STFPM": 0.714, - "PADIM": 0.762 - }, - "transistor": { - "STFPM": 0.53, - "PADIM": 0.66 - }, - "wood": { - "STFPM": 0.899, - "PADIM": 0.949 - }, - "zipper": { - "STFPM": 0.854, - "PADIM": 0.848 - } - }, - "nncf": { - "bottle": { - "STFPM": 0.759, - "PADIM": 0.94 - }, - "cable": { - "STFPM": 0.613, - "PADIM": 0.68 - }, - "capsule": { - "STFPM": 0.826, - "PADIM": 0.856 - }, - "carpet": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "grid": { - "STFPM": 0.756, - "PADIM": 0.744 - }, - "hazelnut": { - "STFPM": 0.827, - "PADIM": 0.891 - }, - "leather": { - "STFPM": 0.887, - "PADIM": 0.815 - }, - "metal_nut": { - "STFPM": 0.809, - "PADIM": 0.835 - }, - "pill": { - "STFPM": 0.844, - "PADIM": 0.844 - }, - "screw": { - "STFPM": 0.762, - "PADIM": 0.744 - }, - "tile": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "toothbrush": { - "STFPM": 0.714, - "PADIM": 0.762 - }, - "transistor": { - "STFPM": 0.53, - "PADIM": 0.66 - }, - "wood": { - "STFPM": 0.899, - "PADIM": 0.949 - }, - "zipper": { - "STFPM": 0.854, - "PADIM": 0.848 - } - }, - "ptq": { - "bottle": { - "STFPM": 0.759, - "PADIM": 0.94 - }, - "cable": { - "STFPM": 0.613, - "PADIM": 0.68 - }, - "capsule": { - "STFPM": 0.826, - "PADIM": 0.856 - }, - "carpet": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "grid": { - "STFPM": 0.756, - "PADIM": 0.744 - }, - "hazelnut": { - "STFPM": 0.827, - "PADIM": 0.891 - }, - "leather": { - "STFPM": 0.887, - "PADIM": 0.815 - }, - "metal_nut": { - "STFPM": 0.809, - "PADIM": 0.835 - }, - "pill": { - "STFPM": 0.844, - "PADIM": 0.844 - }, - "screw": { - "STFPM": 0.762, - "PADIM": 0.744 - }, - "tile": { - "STFPM": 0.829, - "PADIM": 0.761 - }, - "toothbrush": { - "STFPM": 0.714, - "PADIM": 0.762 - }, - "transistor": { - "STFPM": 0.53, - "PADIM": 0.66 - }, - "wood": { - "STFPM": 0.899, - "PADIM": 0.949 - }, - "zipper": { - "STFPM": 0.854, - "PADIM": 0.848 + "STFPM": 389.025, + "PADIM": 36.59 } } }, - "anomaly_detection": { + "anomaly_classification": { "train": { - "bottle": { - "STFPM": 0.029, - "PADIM": 0.156 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.061 - }, - "capsule": { - "STFPM": 0.004, - "PADIM": 0.127 - }, - "carpet": { - "STFPM": 0.185, - "PADIM": 0.244 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.012 - }, - "hazelnut": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "leather": { - "STFPM": 0.049, - "PADIM": 0.18 - }, - "metal_nut": { - "STFPM": 0.021, - "PADIM": 0.129 - }, - "pill": { - "STFPM": 0.032, - "PADIM": 0.028 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.023 - }, - "tile": { - "STFPM": 0.016, - "PADIM": 0.125 - }, - "toothbrush": { - "STFPM": 0.015, - "PADIM": 0.176 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "wood": { - "STFPM": 0.062, - "PADIM": 0.152 - }, - "zipper": { - "STFPM": 0.191, - "PADIM": 0.08 - } - }, - "export": { - "bottle": { - "STFPM": 0.029, - "PADIM": 0.156 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.061 - }, - "capsule": { - "STFPM": 0.004, - "PADIM": 0.127 - }, - "carpet": { - "STFPM": 0.185, - "PADIM": 0.244 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.012 - }, - "hazelnut": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "leather": { - "STFPM": 0.049, - "PADIM": 0.18 - }, - "metal_nut": { - "STFPM": 0.021, - "PADIM": 0.129 - }, - "pill": { - "STFPM": 0.032, - "PADIM": 0.028 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.023 - }, - "tile": { - "STFPM": 0.016, - "PADIM": 0.125 - }, - "toothbrush": { - "STFPM": 0.015, - "PADIM": 0.176 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "wood": { - "STFPM": 0.062, - "PADIM": 0.152 - }, - "zipper": { - "STFPM": 0.191, - "PADIM": 0.08 - } - }, - "deploy": { - "bottle": { - "STFPM": 0.029, - "PADIM": 0.156 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.061 - }, - "capsule": { - "STFPM": 0.004, - "PADIM": 0.127 - }, - "carpet": { - "STFPM": 0.185, - "PADIM": 0.244 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.012 - }, - "hazelnut": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "leather": { - "STFPM": 0.049, - "PADIM": 0.18 - }, - "metal_nut": { - "STFPM": 0.021, - "PADIM": 0.129 - }, - "pill": { - "STFPM": 0.032, - "PADIM": 0.028 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.023 - }, - "tile": { - "STFPM": 0.016, - "PADIM": 0.125 - }, - "toothbrush": { - "STFPM": 0.015, - "PADIM": 0.176 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "wood": { - "STFPM": 0.062, - "PADIM": 0.152 - }, - "zipper": { - "STFPM": 0.191, - "PADIM": 0.08 - } - }, - "nncf": { - "bottle": { - "STFPM": 0.029, - "PADIM": 0.156 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.061 - }, - "capsule": { - "STFPM": 0.004, - "PADIM": 0.127 - }, - "carpet": { - "STFPM": 0.185, - "PADIM": 0.244 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.012 - }, - "hazelnut": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "leather": { - "STFPM": 0.049, - "PADIM": 0.18 - }, - "metal_nut": { - "STFPM": 0.021, - "PADIM": 0.129 - }, - "pill": { - "STFPM": 0.032, - "PADIM": 0.028 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.023 - }, - "tile": { - "STFPM": 0.016, - "PADIM": 0.125 - }, - "toothbrush": { - "STFPM": 0.015, - "PADIM": 0.176 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "wood": { - "STFPM": 0.062, - "PADIM": 0.152 - }, - "zipper": { - "STFPM": 0.191, - "PADIM": 0.08 - } - }, - "ptq": { - "bottle": { - "STFPM": 0.029, - "PADIM": 0.156 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.061 - }, - "capsule": { - "STFPM": 0.004, - "PADIM": 0.127 - }, "carpet": { - "STFPM": 0.185, - "PADIM": 0.244 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.012 - }, - "hazelnut": { - "STFPM": 0.0, - "PADIM": 0.116 - }, - "leather": { - "STFPM": 0.049, - "PADIM": 0.18 - }, - "metal_nut": { - "STFPM": 0.021, - "PADIM": 0.129 - }, - "pill": { - "STFPM": 0.032, - "PADIM": 0.028 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.023 - }, - "tile": { - "STFPM": 0.016, - "PADIM": 0.125 - }, - "toothbrush": { - "STFPM": 0.015, - "PADIM": 0.176 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.116 + "STFPM": 483.17, + "PADIM": 33.738 }, "wood": { - "STFPM": 0.062, - "PADIM": 0.152 + "STFPM": 391.582, + "PADIM": 33.527 }, "zipper": { - "STFPM": 0.191, - "PADIM": 0.08 + "STFPM": 284.238, + "PADIM": 33.347 } } }, - "anomaly_segmentation": { - "train": { - "bottle": { - "STFPM": 0.281, - "PADIM": 0.614 - }, - "cable": { - "STFPM": 0.189, - "PADIM": 0.212 - }, - "capsule": { - "STFPM": 0.131, - "PADIM": 0.307 - }, - "carpet": { - "STFPM": 0.225, - "PADIM": 0.348 - }, - "grid": { - "STFPM": 0.045, - "PADIM": 0.037 - }, - "hazelnut": { - "STFPM": 0.453, - "PADIM": 0.515 - }, - "leather": { - "STFPM": 0.262, - "PADIM": 0.417 - }, - "metal_nut": { - "STFPM": 0.389, - "PADIM": 0.428 - }, - "pill": { - "STFPM": 0.166, - "PADIM": 0.262 - }, - "screw": { - "STFPM": 0.029, - "PADIM": 0.11 - }, - "tile": { - "STFPM": 0.304, - "PADIM": 0.223 - }, - "toothbrush": { - "STFPM": 0.166, - "PADIM": 0.379 - }, - "transistor": { - "STFPM": 0.158, - "PADIM": 0.412 - }, - "wood": { - "STFPM": 0.275, - "PADIM": 0.389 - }, - "zipper": { - "STFPM": 0.352, - "PADIM": 0.235 - } - }, - "export": { - "bottle": { - "STFPM": 0.281, - "PADIM": 0.614 - }, - "cable": { - "STFPM": 0.189, - "PADIM": 0.212 - }, - "capsule": { - "STFPM": 0.131, - "PADIM": 0.307 - }, - "carpet": { - "STFPM": 0.225, - "PADIM": 0.348 - }, - "grid": { - "STFPM": 0.045, - "PADIM": 0.037 - }, - "hazelnut": { - "STFPM": 0.453, - "PADIM": 0.515 - }, - "leather": { - "STFPM": 0.262, - "PADIM": 0.417 - }, - "metal_nut": { - "STFPM": 0.389, - "PADIM": 0.428 - }, - "pill": { - "STFPM": 0.166, - "PADIM": 0.262 - }, - "screw": { - "STFPM": 0.029, - "PADIM": 0.11 - }, - "tile": { - "STFPM": 0.304, - "PADIM": 0.223 - }, - "toothbrush": { - "STFPM": 0.166, - "PADIM": 0.379 - }, - "transistor": { - "STFPM": 0.158, - "PADIM": 0.412 - }, - "wood": { - "STFPM": 0.275, - "PADIM": 0.389 - }, - "zipper": { - "STFPM": 0.352, - "PADIM": 0.235 - } - }, - "deploy": { - "bottle": { - "STFPM": 0.281, - "PADIM": 0.614 - }, - "cable": { - "STFPM": 0.189, - "PADIM": 0.212 - }, - "capsule": { - "STFPM": 0.131, - "PADIM": 0.307 - }, - "carpet": { - "STFPM": 0.225, - "PADIM": 0.348 - }, - "grid": { - "STFPM": 0.045, - "PADIM": 0.037 - }, - "hazelnut": { - "STFPM": 0.453, - "PADIM": 0.515 - }, - "leather": { - "STFPM": 0.262, - "PADIM": 0.417 - }, - "metal_nut": { - "STFPM": 0.389, - "PADIM": 0.428 - }, - "pill": { - "STFPM": 0.166, - "PADIM": 0.262 - }, - "screw": { - "STFPM": 0.029, - "PADIM": 0.11 - }, - "tile": { - "STFPM": 0.304, - "PADIM": 0.223 - }, - "toothbrush": { - "STFPM": 0.166, - "PADIM": 0.379 - }, - "transistor": { - "STFPM": 0.158, - "PADIM": 0.412 - }, - "wood": { - "STFPM": 0.275, - "PADIM": 0.389 - }, - "zipper": { - "STFPM": 0.352, - "PADIM": 0.235 - } - }, - "nncf": { - "bottle": { - "STFPM": 0.281, - "PADIM": 0.614 - }, - "cable": { - "STFPM": 0.189, - "PADIM": 0.212 - }, - "capsule": { - "STFPM": 0.131, - "PADIM": 0.307 - }, - "carpet": { - "STFPM": 0.225, - "PADIM": 0.348 - }, - "grid": { - "STFPM": 0.045, - "PADIM": 0.037 - }, - "hazelnut": { - "STFPM": 0.453, - "PADIM": 0.515 - }, - "leather": { - "STFPM": 0.262, - "PADIM": 0.417 - }, - "metal_nut": { - "STFPM": 0.389, - "PADIM": 0.428 - }, - "pill": { - "STFPM": 0.166, - "PADIM": 0.262 - }, - "screw": { - "STFPM": 0.029, - "PADIM": 0.11 - }, - "tile": { - "STFPM": 0.304, - "PADIM": 0.223 - }, - "toothbrush": { - "STFPM": 0.166, - "PADIM": 0.379 - }, - "transistor": { - "STFPM": 0.158, - "PADIM": 0.412 - }, - "wood": { - "STFPM": 0.275, - "PADIM": 0.389 - }, - "zipper": { - "STFPM": 0.352, - "PADIM": 0.235 - } - }, - "ptq": { - "bottle": { - "STFPM": 0.281, - "PADIM": 0.614 - }, - "cable": { - "STFPM": 0.189, - "PADIM": 0.212 - }, - "capsule": { - "STFPM": 0.131, - "PADIM": 0.307 - }, - "carpet": { - "STFPM": 0.225, - "PADIM": 0.348 - }, - "grid": { - "STFPM": 0.045, - "PADIM": 0.037 - }, - "hazelnut": { - "STFPM": 0.453, - "PADIM": 0.515 - }, - "leather": { - "STFPM": 0.262, - "PADIM": 0.417 - }, - "metal_nut": { - "STFPM": 0.389, - "PADIM": 0.428 - }, - "pill": { - "STFPM": 0.166, - "PADIM": 0.262 - }, - "screw": { - "STFPM": 0.029, - "PADIM": 0.11 - }, - "tile": { - "STFPM": 0.304, - "PADIM": 0.223 - }, - "toothbrush": { - "STFPM": 0.166, - "PADIM": 0.379 - }, - "transistor": { - "STFPM": 0.158, - "PADIM": 0.412 - }, - "wood": { - "STFPM": 0.275, - "PADIM": 0.389 - }, - "zipper": { - "STFPM": 0.352, - "PADIM": 0.235 - } - } - } - }, - "kpi_e2e_train_time_criteria": { - "classification": { - "supervised": { - "multi_class": { - "train": { - "EfficientNet-V2-S": 141.137, - "MobileNet-V3-large-1x": 72.732, - "EfficientNet-B0": 82.521, - "Deti-Tiny": 0.0 - } - }, - "multi_label": { - "train": { - "EfficientNet-V2-S": 214.508, - "MobileNet-V3-large-1x": 185.442, - "EfficientNet-B0": 187.332, - "Deti-Tiny": 0.0 - } - }, - "h_label": { - "train": { - "EfficientNet-V2-S": 149.627, - "MobileNet-V3-large-1x": 76.041, - "EfficientNet-B0": 87.159, - "Deti-Tiny": 0.0 - } - }, - "supcon": { - "train": { - "EfficientNet-V2-S": 136.036, - "MobileNet-V3-large-1x": 134.635, - "EfficientNet-B0": 124.474, - "Deti-Tiny": 0.0 - } - } - }, - "class_incr": { + "classification": { + "supervised": { "multi_class": { "train": { - "EfficientNet-V2-S": 141.137, - "MobileNet-V3-large-1x": 80.603, - "EfficientNet-B0": 82.521, - "Deti-Tiny": 0.0 + "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": 214.508, - "MobileNet-V3-large-1x": 185.442, - "EfficientNet-B0": 187.332, - "Deti-Tiny": 0.0 + "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": 0.0, - "MobileNet-V3-large-1x": 0.0, - "EfficientNet-B0": 0.0, - "Deti-Tiny": 0.0 + "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": 423.184, - "MobileNet-V3-large-1x": 227.445, - "EfficientNet-B0": 230.227, - "Deti-Tiny": 0.0 + "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": 141.137, - "MobileNet-V3-large-1x": 72.239, - "EfficientNet-B0": 82.521, - "Deti-Tiny": 0.0 + "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": { - "supervised": { + "tiling": { "multi_class": { "train": { - "YOLOX": 4226.84, - "SSD": 1992.196, - "MobileNetV2-ATSS": 1843.627 + "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 } } }, - "class_incr": { + "supervised": { "multi_class": { "train": { - "YOLOX": 3059.355, - "SSD": 1439.16, - "MobileNetV2-ATSS": 1531.103 + "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": { - "YOLOX": 4962.307, - "SSD": 6410.017, - "MobileNetV2-ATSS": 6263.435 + "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 } } }, - "tiling": { + "class_incr": { "multi_class": { "train": { - "YOLOX": 1689.117, - "SSD": 2052.821, - "MobileNetV2-ATSS": 2074.501 + "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 } } } }, - "segmentation": { + "instance_segmentation": { "supervised": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 393.137, - "Lite-HRNet-18-mod2": 443.94, - "Lite-HRNet-18": 446.99, - "Lite-HRNet-x-mod3": 667.493, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 - } - }, - "supcon": { - "train": { - "Lite-HRNet-s-mod2": 241.88, - "Lite-HRNet-18-mod2": 279.19, - "Lite-HRNet-18": 279.71, - "Lite-HRNet-x-mod3": 454.31, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "MaskRCNN-SwinT-FP16": 1364.765, + "MaskRCNN-EfficientNetB2B": 605.386, + "MaskRCNN-ResNet50": 1284.017 } } }, "class_incr": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 447.762, - "Lite-HRNet-18-mod2": 474.511, - "Lite-HRNet-18": 486.038, - "Lite-HRNet-x-mod3": 759.906, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "MaskRCNN-SwinT-FP16": 1369.209, + "MaskRCNN-EfficientNetB2B": 984.011, + "MaskRCNN-ResNet50": 1285.941 } } - }, - "semi_supervised": { + } + }, + "segmentation": { + "supervised": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 564.257, - "Lite-HRNet-18-mod2": 715.882, - "Lite-HRNet-18": 601.665, - "Lite-HRNet-x-mod3": 902.047, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "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 } - } - }, - "self_supervised": { - "multi_class": { + }, + "supcon": { "train": { - "Lite-HRNet-s-mod2": 727.372, - "Lite-HRNet-18-mod2": 756.383, - "Lite-HRNet-18": 750.812, - "Lite-HRNet-x-mod3": 1017.624, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "Lite-HRNet-18": 130.635, + "Lite-HRNet-18-mod2": 129.879, + "Lite-HRNet-s-mod2": 117.903, + "Lite-HRNet-x-mod3": 181.323 } } - } - }, - "instance_segmentation": { - "supervised": { + }, + "semi_supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 7234.698, - "MaskRCNN-EfficientNetB2B": 3450.991 + "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 } } }, - "class_incr": { + "self_supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 5154.329, - "MaskRCNN-EfficientNetB2B": 4342.104 + "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 } } }, - "tiling": { + "class_incr": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 8086.079, - "MaskRCNN-EfficientNetB2B": 7274.234 + "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": { - "X3D": 3604.046, - "MoViNet": 2091.719 + "MoViNet": 39.944, + "X3D": 42.252 } } } @@ -1772,626 +1463,262 @@ "supervised": { "multi_class": { "train": { - "X3D_FAST_RCNN": 11349.635 + "X3D_FAST_RCNN": 94.44 } } } }, - "anomaly_classification": { + "anomaly_segmentation": { "train": { - "bottle": { - "STFPM": 473.916, - "PADIM": 94.682 - }, - "cable": { - "STFPM": 782.332, - "PADIM": 91.326 - }, - "capsule": { - "STFPM": 485.159, - "PADIM": 91.746 - }, "carpet": { - "STFPM": 903.789, - "PADIM": 92.068 - }, - "grid": { - "STFPM": 473.575, - "PADIM": 90.083 - }, - "hazelnut": { - "STFPM": 1070.262, - "PADIM": 92.554 - }, - "leather": { - "STFPM": 1632.646, - "PADIM": 91.489 - }, - "metal_nut": { - "STFPM": 471.863, - "PADIM": 83.262 - }, - "pill": { - "STFPM": 474.212, - "PADIM": 97.604 - }, - "screw": { - "STFPM": 1435.737, - "PADIM": 93.66 - }, - "tile": { - "STFPM": 1282.178, - "PADIM": 95.382 - }, - "toothbrush": { - "STFPM": 798.572, - "PADIM": 77.555 - }, - "transistor": { - "STFPM": 844.123, - "PADIM": 80.574 + "STFPM": 11.986, + "PADIM": 10.973 }, "wood": { - "STFPM": 472.098, - "PADIM": 86.524 + "STFPM": 14.704, + "PADIM": 13.279 }, "zipper": { - "STFPM": 1176.295, - "PADIM": 82.333 + "STFPM": 14.002, + "PADIM": 13.41 } } }, "anomaly_detection": { "train": { - "bottle": { - "STFPM": 471.13, - "PADIM": 91.643 - }, - "cable": { - "STFPM": 412.08, - "PADIM": 95.515 - }, - "capsule": { - "STFPM": 409.549, - "PADIM": 91.765 - }, "carpet": { - "STFPM": 1007.763, - "PADIM": 93.797 - }, - "grid": { - "STFPM": 639.249, - "PADIM": 95.983 - }, - "hazelnut": { - "STFPM": 397.998, - "PADIM": 91.448 - }, - "leather": { - "STFPM": 418.21, - "PADIM": 92.509 - }, - "metal_nut": { - "STFPM": 435.304, - "PADIM": 85.566 - }, - "pill": { - "STFPM": 419.626, - "PADIM": 92.345 - }, - "screw": { - "STFPM": 736.772, - "PADIM": 89.448 - }, - "tile": { - "STFPM": 584.306, - "PADIM": 83.021 - }, - "toothbrush": { - "STFPM": 602.712, - "PADIM": 88.732 - }, - "transistor": { - "STFPM": 413.1, - "PADIM": 89.572 + "STFPM": 8.138, + "PADIM": 8.243 }, "wood": { - "STFPM": 963.139, - "PADIM": 95.301 + "STFPM": 7.5, + "PADIM": 7.6 }, "zipper": { - "STFPM": 1313.441, - "PADIM": 91.329 + "STFPM": 8.728, + "PADIM": 8.825 } } }, - "anomaly_segmentation": { + "anomaly_classification": { "train": { - "bottle": { - "STFPM": 2423.196, - "PADIM": 122.496 - }, - "cable": { - "STFPM": 1061.665, - "PADIM": 134.752 - }, - "capsule": { - "STFPM": 673.885, - "PADIM": 126.904 - }, "carpet": { - "STFPM": 1053.201, - "PADIM": 126.328 - }, - "grid": { - "STFPM": 946.958, - "PADIM": 115.046 - }, - "hazelnut": { - "STFPM": 1362.821, - "PADIM": 129.905 - }, - "leather": { - "STFPM": 1317.973, - "PADIM": 117.569 - }, - "metal_nut": { - "STFPM": 1420.045, - "PADIM": 120.641 - }, - "pill": { - "STFPM": 682.699, - "PADIM": 113.66 - }, - "screw": { - "STFPM": 1298.055, - "PADIM": 98.299 - }, - "tile": { - "STFPM": 942.646, - "PADIM": 124.92 - }, - "toothbrush": { - "STFPM": 1180.837, - "PADIM": 105.42 - }, - "transistor": { - "STFPM": 710.488, - "PADIM": 107.729 + "STFPM": 5.954, + "PADIM": 6.075 }, "wood": { - "STFPM": 1202.055, - "PADIM": 117.939 + "STFPM": 5.839, + "PADIM": 5.853 }, "zipper": { - "STFPM": 1456.587, - "PADIM": 108.119 + "STFPM": 5.778, + "PADIM": 5.972 } } - } - }, - "kpi_e2e_eval_time_criteria": { + }, "classification": { "supervised": { "multi_class": { "train": { - "EfficientNet-V2-S": 17.8, - "MobileNet-V3-large-1x": 16.453, - "EfficientNet-B0": 15.145, - "Deti-Tiny": 0.0 + "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": 18.706, - "MobileNet-V3-large-1x": 20.311, - "EfficientNet-B0": 19.497, - "Deti-Tiny": 0.0 + "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": 17.99, - "MobileNet-V3-large-1x": 18.068, - "EfficientNet-B0": 13.558, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 10.636, + "EfficientNet-B0": 9.836, + "MobileNet-V3-large-1x": 9.835, + "DeiT-Tiny": 26.898 } }, "supcon": { "train": { - "EfficientNet-V2-S": 17.1, - "MobileNet-V3-large-1x": 17.988, - "EfficientNet-B0": 14.586, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 15.597, + "EfficientNet-B0": 14.843, + "MobileNet-V3-large-1x": 14.996 } } }, - "class_incr": { + "semi_supervised": { "multi_class": { "train": { - "EfficientNet-V2-S": 16.715, - "MobileNet-V3-large-1x": 16.722, - "EfficientNet-B0": 15.488, - "Deti-Tiny": 0.0 - } - }, - "multi_label": { - "train": { - "EfficientNet-V2-S": 19.618, - "MobileNet-V3-large-1x": 20.686, - "EfficientNet-B0": 17.046, - "Deti-Tiny": 0.0 - } - }, - "h_label": { - "train": { - "EfficientNet-V2-S": 0.0, - "MobileNet-V3-large-1x": 0.0, - "EfficientNet-B0": 0.0, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 16.646, + "EfficientNet-B0": 16.055, + "MobileNet-V3-large-1x": 15.865, + "DeiT-Tiny": 66.312 } } }, - "semi_supervised": { + "self_supervised": { "multi_class": { "train": { - "EfficientNet-V2-S": 16.885, - "MobileNet-V3-large-1x": 17.905, - "EfficientNet-B0": 15.107, - "Deti-Tiny": 0.0 + "EfficientNet-V2-S": 15.71, + "EfficientNet-B0": 15.043, + "MobileNet-V3-large-1x": 15.007 } } }, - "self_supervised": { + "class_incr": { "multi_class": { "train": { - "EfficientNet-V2-S": 15.552, - "MobileNet-V3-large-1x": 14.267, - "EfficientNet-B0": 15.179, - "Deti-Tiny": 0.0 + "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": { - "supervised": { + "tiling": { "multi_class": { "train": { - "YOLOX": 82.169, - "SSD": 86.989, - "MobileNetV2-ATSS": 73.888 + "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 } } }, - "class_incr": { + "supervised": { "multi_class": { "train": { - "YOLOX": 59.154, - "SSD": 89.412, - "MobileNetV2-ATSS": 62.492 + "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": { - "YOLOX": 47.199, - "SSD": 76.703, - "MobileNetV2-ATSS": 50.34 + "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 } } }, - "tiling": { + "class_incr": { "multi_class": { "train": { - "YOLOX": 146.561, - "SSD": 169.937, - "MobileNetV2-ATSS": 84.882 + "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 } } } }, - "segmentation": { + "instance_segmentation": { "supervised": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 36.656, - "Lite-HRNet-18-mod2": 42.2, - "Lite-HRNet-18": 43.046, - "Lite-HRNet-x-mod3": 59.053, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 - } - }, - "supcon": { - "train": { - "Lite-HRNet-s-mod2": 13.835, - "Lite-HRNet-18-mod2": 16.39, - "Lite-HRNet-18": 16.23, - "Lite-HRNet-x-mod3": 26.549, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "MaskRCNN-SwinT-FP16": 47.091, + "MaskRCNN-EfficientNetB2B": 30.544, + "MaskRCNN-ResNet50": 40.727 } } }, "class_incr": { "multi_class": { "train": { - "Lite-HRNet-s-mod2": 43.425, - "Lite-HRNet-18-mod2": 48.536, - "Lite-HRNet-18": 43.817, - "Lite-HRNet-x-mod3": 74.163, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 - } - } - }, - "semi_supervised": { - "multi_class": { - "train": { - "Lite-HRNet-s-mod2": 43.347, - "Lite-HRNet-18-mod2": 51.605, - "Lite-HRNet-18": 47.816, - "Lite-HRNet-x-mod3": 56.125, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 - } - } - }, - "self_supervised": { - "multi_class": { - "train": { - "Lite-HRNet-s-mod2": 37.616, - "Lite-HRNet-18-mod2": 36.786, - "Lite-HRNet-18": 44.935, - "Lite-HRNet-x-mod3": 52.59, - "SegNext-B": 0.0, - "SegNext-s": 0.0, - "SegNext-t": 0.0 + "MaskRCNN-SwinT-FP16": 47.186, + "MaskRCNN-EfficientNetB2B": 30.885, + "MaskRCNN-ResNet50": 39.559 } } } }, - "instance_segmentation": { + "segmentation": { "supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 443.869, - "MaskRCNN-EfficientNetB2B": 451.211 + "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 } - } - }, - "class_incr": { - "multi_class": { + }, + "supcon": { "train": { - "MaskRCNN-ResNet50": 103.552, - "MaskRCNN-EfficientNetB2B": 118.353 + "Lite-HRNet-18": 15.703, + "Lite-HRNet-18-mod2": 16.187, + "Lite-HRNet-s-mod2": 12.206, + "Lite-HRNet-x-mod3": 27.72 } } }, - "tiling": { + "semi_supervised": { "multi_class": { "train": { - "MaskRCNN-ResNet50": 522.271, - "MaskRCNN-EfficientNetB2B": 321.017 + "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 } } - } - }, - "action_classification": { - "supervised": { + }, + "self_supervised": { "multi_class": { "train": { - "X3D": 384.374, - "MoViNet": 244.952 + "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 } } - } - }, - "action_detection": { - "supervised": { + }, + "class_incr": { "multi_class": { "train": { - "X3D_FAST_RCNN": 289.228 + "Lite-HRNet-18": 17.608, + "Lite-HRNet-18-mod2": 14.745, + "Lite-HRNet-s-mod2": 13.395, + "Lite-HRNet-x-mod3": 23.336 } } } - }, - "anomaly_classification": { - "train": { - "bottle": { - "STFPM": 13.302, - "PADIM": 14.602 - }, - "cable": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "capsule": { - "STFPM": 13.433, - "PADIM": 14.363 - }, - "carpet": { - "STFPM": 13.341, - "PADIM": 14.946 - }, - "grid": { - "STFPM": 13.273, - "PADIM": 15.915 - }, - "hazelnut": { - "STFPM": 13.385, - "PADIM": 15.577 - }, - "leather": { - "STFPM": 12.521, - "PADIM": 15.257 - }, - "metal_nut": { - "STFPM": 12.02, - "PADIM": 13.63 - }, - "pill": { - "STFPM": 11.368, - "PADIM": 15.067 - }, - "screw": { - "STFPM": 13.29, - "PADIM": 15.839 - }, - "tile": { - "STFPM": 12.391, - "PADIM": 15.928 - }, - "toothbrush": { - "STFPM": 11.391, - "PADIM": 14.91 - }, - "transistor": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "wood": { - "STFPM": 12.308, - "PADIM": 13.998 - }, - "zipper": { - "STFPM": 12.046, - "PADIM": 14.165 - } - } - }, - "anomaly_detection": { - "train": { - "bottle": { - "STFPM": 12.329, - "PADIM": 15.616 - }, - "cable": { - "STFPM": 11.788, - "PADIM": 15.858 - }, - "capsule": { - "STFPM": 12.491, - "PADIM": 14.235 - }, - "carpet": { - "STFPM": 12.582, - "PADIM": 14.774 - }, - "grid": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "hazelnut": { - "STFPM": 12.366, - "PADIM": 14.574 - }, - "leather": { - "STFPM": 12.924, - "PADIM": 13.225 - }, - "metal_nut": { - "STFPM": 11.543, - "PADIM": 16.109 - }, - "pill": { - "STFPM": 14.454, - "PADIM": 13.279 - }, - "screw": { - "STFPM": 13.163, - "PADIM": 15.934 - }, - "tile": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "toothbrush": { - "STFPM": 13.33, - "PADIM": 15.593 - }, - "transistor": { - "STFPM": 13.294, - "PADIM": 14.333 - }, - "wood": { - "STFPM": 13.265, - "PADIM": 15.228 - }, - "zipper": { - "STFPM": 13.609, - "PADIM": 14.914 - } - } - }, - "anomaly_segmentation": { - "train": { - "bottle": { - "STFPM": 11.796, - "PADIM": 14.88 - }, - "cable": { - "STFPM": 12.265, - "PADIM": 13.511 - }, - "capsule": { - "STFPM": 11.977, - "PADIM": 15.504 - }, - "carpet": { - "STFPM": 13.315, - "PADIM": 14.945 - }, - "grid": { - "STFPM": 12.072, - "PADIM": 15.049 - }, - "hazelnut": { - "STFPM": 12.515, - "PADIM": 15.165 - }, - "leather": { - "STFPM": 12.547, - "PADIM": 13.179 - }, - "metal_nut": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "pill": { - "STFPM": 12.404, - "PADIM": 17.594 - }, - "screw": { - "STFPM": 0.0, - "PADIM": 0.0 - }, - "tile": { - "STFPM": 11.851, - "PADIM": 15.183 - }, - "toothbrush": { - "STFPM": 13.301, - "PADIM": 14.932 - }, - "transistor": { - "STFPM": 13.137, - "PADIM": 15.278 - }, - "wood": { - "STFPM": 14.124, - "PADIM": 13.865 - }, - "zipper": { - "STFPM": 11.624, - "PADIM": 13.555 - } - } } } } diff --git a/tests/regression/regression_test_helpers.py b/tests/regression/regression_test_helpers.py index 1b1b34e41e4..5cc308720c0 100644 --- a/tests/regression/regression_test_helpers.py +++ b/tests/regression/regression_test_helpers.py @@ -14,6 +14,7 @@ import json import os +from copy import copy from pathlib import Path from typing import Any, Dict, List, Union @@ -31,25 +32,30 @@ "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", + # "bottle", + # "cable", + # "capsule", "carpet", - "grid", - "hazelnut", - "leather", - "metal_nut", - "pill", - "screw", - "tile", - "toothbrush", - "transistor", + # "grid", + # "hazelnut", + # "leather", + # "metal_nut", + # "pill", + # "screw", + # "tile", + # "toothbrush", + # "transistor", "wood", "zipper", ] @@ -78,14 +84,15 @@ def __init__(self, task_type, train_type, label_type, otx_dir, **kwargs): self.label_type = label_type self.otx_dir = otx_dir - self.result_dict = self._init_result_dict() - result_dir_prefix = kwargs.get("result_dir", "") - if len(result_dir_prefix) > 0: - result_dir_prefix = result_dir_prefix + "_" - tmp_results_root = kwargs.get("tmp_results_root", "tmp/regression_test_results") - self.result_dir = os.path.join(tmp_results_root, "regression_test_results", f"{result_dir_prefix}{task_type}") + 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") @@ -94,8 +101,24 @@ def __init__(self, task_type, train_type, label_type, otx_dir, **kwargs): 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: @@ -167,22 +190,31 @@ def load_config(self, **kwargs) -> Dict[str, Union[int, float]]: return result - def _init_result_dict(self) -> Dict[str, Any]: - result_dict = {self.task_type: {}} - if "anomaly" not in self.task_type: - for label_type in LABEL_TYPES: - result_dict[self.task_type][label_type] = {} - for train_type in TRAIN_TYPES: - result_dict[self.task_type][label_type][train_type] = {} - for test_type in TEST_TYPES: - result_dict[self.task_type][label_type][train_type][test_type] = [] + 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: - for test_type in TEST_TYPES: - result_dict[self.task_type][test_type] = {} - for category in ANOMALY_DATASET_CATEGORIES: - result_dict[self.task_type][test_type][category] = [] - - return result_dict + 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.""" @@ -196,15 +228,13 @@ def get_template_performance(self, template: ModelTemplate, **kwargs): category = kwargs.get("category") if category is None: raise RuntimeError("missing required keyword arg 'category'") - results = self.result_dict[task_type]["train"][category] + results = self._result_dict[task_type]["train"][category] else: - results = self.result_dict[task_type][label_type][train_type]["train"] + 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 - if performance is None: - raise ValueError("Performance is None.") return performance diff --git a/tests/regression/semantic_segmentation/test_segmentation.py b/tests/regression/semantic_segmentation/test_segmentation.py index dee04b4c8f3..e8870fa54c6 100644 --- a/tests/regression/semantic_segmentation/test_segmentation.py +++ b/tests/regression/semantic_segmentation/test_segmentation.py @@ -53,20 +53,19 @@ class TestRegressionSegmentation: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -74,6 +73,7 @@ def setup_method(self): @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 @@ -87,14 +87,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -102,6 +102,8 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): @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"], @@ -121,7 +123,10 @@ def test_otx_train_kpi_test(self, reg_cfg, template): @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) @@ -153,7 +158,7 @@ def test_otx_train_cls_incr(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -163,6 +168,8 @@ 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"], @@ -183,6 +190,7 @@ def test_otx_train_cls_incr_kpi_test(self, reg_cfg, template): @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" @@ -217,7 +225,7 @@ def test_otx_train_semisl(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -227,6 +235,8 @@ 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"], @@ -247,6 +257,7 @@ def test_otx_train_semisl_kpi_test(self, reg_cfg, template): @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" @@ -290,7 +301,7 @@ def test_otx_train_selfsl(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance, train_type=train_type) assert test_result["passed"] is True, test_result["log"] @@ -300,6 +311,8 @@ 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"], @@ -319,6 +332,7 @@ def test_otx_train_selfsl_kpi_test(self, reg_cfg, template): @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 @@ -333,7 +347,7 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.05, - criteria=reg_cfg.config_dict["regression_criteria"]["export"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -341,15 +355,14 @@ def test_otx_export_eval_openvino(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["export"].append( - self.performance - ) + 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 @@ -364,7 +377,7 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.0, - criteria=reg_cfg.config_dict["regression_criteria"]["deploy"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -372,15 +385,14 @@ def test_otx_deploy_eval_deployment(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["deploy"].append( - self.performance - ) + 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 @@ -398,7 +410,7 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): reg_cfg.otx_dir, reg_cfg.args, threshold=0.01, - criteria=reg_cfg.config_dict["regression_criteria"]["nncf"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -406,13 +418,14 @@ def test_nncf_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["nncf"].append(self.performance) + 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 @@ -426,7 +439,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - criteria=reg_cfg.config_dict["regression_criteria"]["ptq"], + criteria=reg_cfg.config_dict["regression_criteria"][test_type], reg_threshold=0.10, result_dict=self.performance[template.name], ) @@ -434,7 +447,7 @@ def test_ptq_optimize_eval(self, reg_cfg, template, tmp_dir_path): 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["ptq"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -460,20 +473,19 @@ class TestRegressionSupconSegmentation: @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, - tmp_results_root=tmp_dir_path, + results_root=results_root, ) yield cls.reg_cfg - print(f"writting regression result to {cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json") - with open(f"{cls.reg_cfg.result_dir}/result_{cls.TRAIN_TYPE}_{cls.LABEL_TYPE}.json", "w") as result_file: - json.dump(cls.reg_cfg.result_dict, result_file, indent=4) + cls.reg_cfg.dump_result_dict() def setup_method(self): self.performance = {} @@ -483,6 +495,7 @@ def setup_method(self): 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" @@ -499,14 +512,14 @@ def test_otx_train(self, reg_cfg, template, tmp_dir_path): tmp_dir_path, reg_cfg.otx_dir, reg_cfg.args, - reg_cfg.config_dict["regression_criteria"]["train"], + 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.result_dict[reg_cfg.task_type][reg_cfg.label_type][reg_cfg.train_type]["train"].append(self.performance) + reg_cfg.update_result(test_type, self.performance) assert test_result["passed"] is True, test_result["log"] @@ -517,6 +530,8 @@ def test_otx_train_kpi_test(self, reg_cfg, template): 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"], diff --git a/tests/regression/summarize_test_results.py b/tests/regression/summarize_test_results.py index 73cc43198de..6dec6f6d088 100644 --- a/tests/regression/summarize_test_results.py +++ b/tests/regression/summarize_test_results.py @@ -132,31 +132,77 @@ def fill_model_performance(items: Union[list, str], test_type: str, result_data: result_data[f"{test_type} Eval Time (Sec.)"].append(items) -def summarize_non_anomaly_data(task: str, task_key: str, json_data: dict, result_data: dict) -> dict: +def summarize_non_anomaly_data(json_data: dict, result_data: dict) -> dict: """Make DataFrame by gathering all results.""" - for label_type in LABEL_TYPES: - for train_type in TRAIN_TYPES: - 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 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_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) - result_data["Train type"].append(train_type) - result_data["Label type"].append(label_type) + 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) @@ -166,44 +212,6 @@ def summarize_non_anomaly_data(task: str, task_key: str, json_data: dict, result fill_model_performance(ptq_items, "ptq", result_data) -def summarize_anomaly_data(task: str, task_key: str, json_data: dict, result_data: dict) -> dict: - """Make DataFrame by gathering all results.""" - 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) - 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) - 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): @@ -241,7 +249,7 @@ def summarize_results_data(input_path: str, output_path: str): for entity in os.listdir(input_path): entity_path = os.path.join(input_path, entity) if os.path.isdir(entity_path): - task_key, task = filter_task(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) @@ -250,9 +258,11 @@ def summarize_results_data(input_path: str, output_path: str): 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(task, task_key, json_data, ANOMALY_DATA) - save_file(ANOMALY_DATA, output_path, f"tests-reg_{task}_{task_key}.csv") + summarize_anomaly_data(json_data, ANOMALY_DATA) + save_file(ANOMALY_DATA, output_path, f"tests-reg_{task}.csv") else: - summarize_non_anomaly_data(task, task_key, json_data, NON_ANOMALY_DATA) - save_file(NON_ANOMALY_DATA, output_path, f"tests-reg_{task}_{task_key}.csv") + 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/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index c40d092bffd..5ca1624854e 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -8,13 +8,13 @@ import os import shutil import sys -import torch 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 @@ -129,6 +129,8 @@ def otx_train_testing(template, root, otx_dir, args, deterministic=True): 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"]]) @@ -164,6 +166,10 @@ def otx_resume_testing(template, root, otx_dir, args): 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"]) @@ -209,13 +215,18 @@ def otx_hpo_testing(template, root, otx_dir, args): 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", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--output", save_path, ] @@ -295,6 +306,10 @@ def otx_export_testing(template, root, dump_features=False, half_precision=False 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", @@ -302,7 +317,7 @@ def otx_eval_testing(template, root, otx_dir, args): "--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", + weights_path, "--output", f"{template_work_dir}/trained_{template.model_template_id}", ] @@ -330,6 +345,9 @@ def otx_eval_openvino_testing( 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", @@ -354,12 +372,17 @@ def otx_eval_openvino_testing( 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", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--input", os.path.join(otx_dir, args["--input"]), "--delay", @@ -373,12 +396,17 @@ def otx_demo_testing(template, root, otx_dir, args): 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", - f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml", + weights_path, "--input", os.path.join(otx_dir, args["--input"]), "--delay", @@ -392,13 +420,18 @@ def otx_demo_openvino_testing(template, root, otx_dir, args): 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", - f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml", + weights_path, "--output", deployment_dir, ] @@ -471,6 +504,11 @@ def otx_deploy_openvino_testing(template, root, otx_dir, args): 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", @@ -478,7 +516,7 @@ def otx_eval_deployment_testing(template, root, otx_dir, args, threshold=0.0): "--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", + weights_path, "--output", f"{template_work_dir}/deployed_{template.model_template_id}", ] @@ -496,12 +534,17 @@ def otx_eval_deployment_testing(template, root, otx_dir, args, threshold=0.0): 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", - f"{deployment_dir}/openvino.zip", + weights_path, "--input", os.path.join(otx_dir, args["--input"]), "--delay", @@ -515,6 +558,13 @@ def otx_demo_deployment_testing(template, root, otx_dir, args): 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", @@ -525,21 +575,9 @@ def ptq_optimize_testing(template, root, otx_dir, args, is_visual_prompting=Fals f'{os.path.join(otx_dir, args["--val-data-roots"])}', "--output", f"{template_work_dir}/ptq_{template.model_template_id}", + "--load-weights", + weights_path, ] - if is_visual_prompting: - command_line.extend( - [ - "--load-weights", - f"{template_work_dir}/exported_{template.model_template_id}/visual_prompting_decoder.xml", - ] - ) - else: - command_line.extend( - [ - "--load-weights", - f"{template_work_dir}/exported_{template.model_template_id}/openvino.xml", - ] - ) command_line.extend(["--workspace", f"{template_work_dir}"]) check_run(command_line) @@ -575,11 +613,17 @@ def _validate_fq_in_xml(xml_path, path_to_ref_data, compression_type, test_name, def ptq_validate_fq_testing(template, root, otx_dir, task_type, test_name): template_work_dir = get_template_dir(template, root) + xml_paths = [f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml"] if task_type == "visual_prompting": 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", ] + 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 task_type == "visual_prompting": paths_to_ref_data = [ os.path.join( otx_dir, @@ -601,7 +645,6 @@ def ptq_validate_fq_testing(template, root, otx_dir, task_type, test_name): ), ] else: - xml_paths = [f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml"] paths_to_ref_data = [ os.path.join( otx_dir, "tests", "e2e/cli", task_type, "reference", template.model_template_id, "compressed_model.yml" @@ -614,6 +657,13 @@ def ptq_validate_fq_testing(template, root, otx_dir, task_type, test_name): def ptq_eval_testing(template, root, otx_dir, args, is_visual_prompting=False): template_work_dir = get_template_dir(template, root) + + weights_path = f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml" + if is_visual_prompting: + weights_path = f"{template_work_dir}/ptq_{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", "eval", @@ -622,34 +672,24 @@ def ptq_eval_testing(template, root, otx_dir, args, is_visual_prompting=False): f'{os.path.join(otx_dir, args["--test-data-roots"])}', "--output", f"{template_work_dir}/ptq_{template.model_template_id}", + "--load-weights", + weights_path, ] - if is_visual_prompting: - command_line.extend( - [ - "--load-weights", - f"{template_work_dir}/ptq_{template.model_template_id}/visual_prompting_decoder.xml", - ] - ) - else: - command_line.extend( - [ - "--load-weights", - f"{template_work_dir}/ptq_{template.model_template_id}/openvino.xml", - ] - ) 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) - 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", @@ -659,7 +699,7 @@ def nncf_optimize_testing(template, root, otx_dir, args): "--val-data-roots", f'{os.path.join(otx_dir, args["--val-data-roots"])}', "--load-weights", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--output", f"{template_work_dir}/nncf_{template.model_template_id}", ] @@ -673,13 +713,19 @@ def nncf_optimize_testing(template, root, otx_dir, args): 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", - f"{template_work_dir}/nncf_{template.model_template_id}/weights.pth", + weights_path, "--output", f"{template_work_dir}/exported_nncf_{template.model_template_id}", ] @@ -705,8 +751,13 @@ def nncf_export_testing(template, root): 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" ) @@ -717,7 +768,13 @@ def nncf_validate_fq_testing(template, root, otx_dir, task_type, 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", @@ -725,7 +782,7 @@ def nncf_eval_testing(template, root, otx_dir, args, threshold=0.01): "--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", + weights_path, "--output", f"{template_work_dir}/nncf_{template.model_template_id}", ] @@ -743,7 +800,13 @@ def nncf_eval_testing(template, root, otx_dir, args, threshold=0.01): 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", @@ -751,7 +814,7 @@ def nncf_eval_openvino_testing(template, root, otx_dir, args): "--test-data-roots", f'{os.path.join(otx_dir, args["--test-data-roots"])}', "--load-weights", - f"{template_work_dir}/exported_nncf_{template.model_template_id}/openvino.xml", + weights_path, "--output", f"{template_work_dir}/exported_nncf_{template.model_template_id}", ] @@ -779,6 +842,11 @@ def xfail_templates(templates, xfail_template_ids_reasons): 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", "") @@ -797,7 +865,7 @@ def otx_explain_testing(template, root, otx_dir, args, trained=False): "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", @@ -814,6 +882,11 @@ def otx_explain_testing(template, root, otx_dir, args, trained=False): 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", "") @@ -832,7 +905,7 @@ def otx_explain_testing_all_classes(template, root, otx_dir, args): "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", @@ -855,6 +928,11 @@ def otx_explain_testing_all_classes(template, root, otx_dir, args): 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", "") @@ -873,7 +951,7 @@ def otx_explain_testing_process_saliency_maps(template, root, otx_dir, args, tra "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/trained_{template.model_template_id}/models/weights.pth", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", @@ -891,6 +969,11 @@ def otx_explain_testing_process_saliency_maps(template, root, otx_dir, args, tra 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", "") @@ -909,7 +992,7 @@ def otx_explain_openvino_testing(template, root, otx_dir, args, trained=False): "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", @@ -927,6 +1010,11 @@ def otx_explain_openvino_testing(template, root, otx_dir, args, trained=False): 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", "") @@ -945,7 +1033,7 @@ def otx_explain_all_classes_openvino_testing(template, root, otx_dir, args): "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", @@ -969,6 +1057,11 @@ def otx_explain_all_classes_openvino_testing(template, root, otx_dir, args): 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", "") @@ -987,7 +1080,7 @@ def otx_explain_process_saliency_maps_openvino_testing(template, root, otx_dir, "explain", template.model_template_path, "--load-weights", - f"{template_work_dir}/exported_{template.model_template_id}_w_features/openvino.xml", + weights_path, "--explain-data-root", explain_data_root, "--save-explanation-to", diff --git a/tox.ini b/tox.ini index 22983697622..1d38a6e391b 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ passenv = HTTPS_PROXY CUDA_VISIBLE_DEVICES CI_DATA_ROOT + REG_RESULTS_ROOT test_dir = all: cli ano: cli/anomaly From 921c4b32aff051f9d2c8345d5747595ca1f53bd6 Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Mon, 27 Nov 2023 10:23:17 +0900 Subject: [PATCH 134/146] Exclude py37 target config for cibuildwheel (#2673) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09381c67cb1..7cc86086753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ build-backend = "setuptools.build_meta" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # CIBUILDWHEEL CONFIGURATION. # [tool.cibuildwheel] -build = "cp37-manylinux_x86_64 cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64" +build = "cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # From 0fed1ac5a032ddf960d47504e975242581c07c53 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 27 Nov 2023 11:39:44 +0900 Subject: [PATCH 135/146] Add `--dryrun` option to tools/experiment.py (#2674) * Fix variable override bug * Add --dryrun option to see experiment list --------- Signed-off-by: Songki Choi --- tools/experiment.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/experiment.py b/tools/experiment.py index 311b7641c2d..4b7f1aa7870 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -31,6 +31,7 @@ def get_parser() -> argparse.ArgumentParser: 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 @@ -594,9 +595,11 @@ def _replace_var_in_target( 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: @@ -680,19 +683,23 @@ def fail_logs(self) -> List[CommandFailInfo]: """Information of all failed cases.""" return self._fail_logs - def run_command_list(self): + 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): + if not self._prepare_run_command(command) and not dryrun: print(f"otx {command[1]} is skipped.") continue - self._run_otx_command(command) + if not dryrun: + self._run_otx_command(command) + else: + print(" ".join(command)) self._previous_cmd_entry = command[1] - organize_exp_result(self._workspace, self._command_var) + 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)) @@ -755,11 +762,12 @@ def set_arguments_to_cmd(command: List[str], key: str, value: Optional[str] = No command.insert(index, key) -def run_experiment_recipe(recipe_file: Union[str, Path]): +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. """ exp_recipe = ExpRecipeParser(recipe_file) output_path = exp_recipe.output_path @@ -771,7 +779,7 @@ def run_experiment_recipe(recipe_file: Union[str, Path]): for command_ins in exp_recipe.commands: for repeat_idx in range(exp_recipe.repeat): otx_cmd_runner = OtxCommandRunner(command_ins, repeat_idx) - otx_cmd_runner.run_command_list() + otx_cmd_runner.run_command_list(dryrun) fail_cases.extend(otx_cmd_runner.fail_logs) os.chdir(current_dir) @@ -779,7 +787,8 @@ def run_experiment_recipe(recipe_file: Union[str, Path]): if fail_cases: log_fail_cases(fail_cases, output_path) - aggregate_all_exp_result(output_path) + if not dryrun: + aggregate_all_exp_result(output_path) def main(): @@ -790,7 +799,7 @@ def main(): 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) + run_experiment_recipe(args.file, args.dryrun) elif args.parse is not None: organize_exp_result(args.parse) else: From de0f5aec036162c285d8ec8dda8fbfd54ce96b75 Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Tue, 28 Nov 2023 09:15:01 +0200 Subject: [PATCH 136/146] Update OTX explain CLI arguments (#2671) * Change int8 to uint8 to XAI tests * Add probabilities for CLI demo * Rename arguments for explain * Fix pre-commit * Remove extra changes * Fix integration tests * Fix integration "explain_all_classes" test for OV --- .../source/guide/get_started/cli_commands.rst | 16 +++--- docs/source/guide/tutorials/base/explain.rst | 8 +-- src/otx/cli/tools/explain.py | 19 ++++--- src/otx/cli/tools/utils/demo/visualization.py | 6 +-- tests/test_suite/run_test_command.py | 52 +++++++++---------- 5 files changed, 50 insertions(+), 51 deletions(-) diff --git a/docs/source/guide/get_started/cli_commands.rst b/docs/source/guide/get_started/cli_commands.rst index a280da898f5..9ef1e7b3ebe 100644 --- a/docs/source/guide/get_started/cli_commands.rst +++ b/docs/source/guide/get_started/cli_commands.rst @@ -449,7 +449,7 @@ With the ``--help`` command, you can list additional information, such as its pa .. code-block:: (otx) ...$ otx explain --help - usage: otx explain [-h] --explain-data-roots EXPLAIN_DATA_ROOTS [--save-explanation-to SAVE_EXPLANATION] --load-weights LOAD_WEIGHTS [--explain-algorithm EXPLAIN_ALGORITHM] [--overlay-weight OVERLAY_WEIGHT] [template] {params} ... + 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. @@ -459,9 +459,9 @@ With the ``--help`` command, you can list additional information, such as its pa optional arguments: -h, --help show this help message and exit - --explain-data-roots EXPLAIN_DATA_ROOTS + -i INPUT, --input INPUT Comma-separated paths to explain data folders. - --save-explanation-to SAVE_EXPLANATION_TO + -o OUTPUT, --output OUTPUT Output path for explanation images. --load-weights LOAD_WEIGHTS Load model weights from previously saved checkpoint. @@ -475,13 +475,13 @@ With the ``--help`` command, you can list additional information, such as its pa 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 ``save-explanation-to`` path: +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:: - (otx) ...$ otx explain SSD --explain-data-roots \ + (otx) ...$ otx explain SSD --input \ --load-weights \ - --save-explanation-to \ + --output \ --explain-algorithm classwisesaliencymap \ --overlay-weight 0.5 @@ -496,9 +496,9 @@ By default, the model is exported to the OpenVINOâ„¢ IR format without extra fea (otx) ...$ otx export SSD --load-weights \ --output outputs/openvino/with_features \ --dump-features - (otx) ...$ otx explain SSD --explain-data-roots \ + (otx) ...$ otx explain SSD --input \ --load-weights outputs/openvino/with_features \ - --save-explanation-to \ + --output \ --explain-algorithm classwisesaliencymap \ --overlay-weight 0.5 diff --git a/docs/source/guide/tutorials/base/explain.rst b/docs/source/guide/tutorials/base/explain.rst index a9367f19887..fa03aea9bea 100644 --- a/docs/source/guide/tutorials/base/explain.rst +++ b/docs/source/guide/tutorials/base/explain.rst @@ -22,12 +22,12 @@ created in the previous step. . venv/otx/bin/activate 2. ``otx explain`` returns saliency maps (heatmaps with red colored areas of focus) -at the path specified by ``--save-explanation-to``. +at the path specified by ``--output``. .. code-block:: - otx explain --explain-data-roots otx-workspace-DETECTION/splitted_dataset/val/ \ - --save-explanation-to outputs/explanation \ + 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, @@ -48,7 +48,7 @@ For detection task, we can choose between the following methods: 4. As a result we will get a folder with a pair of generated -images for each image in ``--explain-data-roots``: +images for each image in ``--input``: - saliency map - where red color means more attention of the model - overlay - where the saliency map is combined with the original image: diff --git a/src/otx/cli/tools/explain.py b/src/otx/cli/tools/explain.py index b6e70cb9dc7..7cb276eed2f 100644 --- a/src/otx/cli/tools/explain.py +++ b/src/otx/cli/tools/explain.py @@ -50,12 +50,14 @@ def get_args(): parser, hyper_parameters, params = get_parser_and_hprams_data() parser.add_argument( - "--explain-data-roots", + "-i", + "--input", required=True, help="Comma-separated paths to explain data folders.", ) parser.add_argument( - "--save-explanation-to", + "-o", + "--output", default="saliency_dump", help="Output path for explanation images.", ) @@ -123,10 +125,7 @@ def _log_after_saving(explain_predicted_classes, explained_image_counter, args, "Please adjust training pipeline or use different model-data pair." ) if explained_image_counter > 0: - logger.info( - f"Saliency maps saved to {args.save_explanation_to} for {explained_image_counter} " - f"out of {num_images} images." - ) + logger.info(f"Saliency maps saved to {args.output} for {explained_image_counter} out of {num_images} images.") def main(): @@ -169,10 +168,10 @@ def main(): f"{args.explain_algorithm} currently not supported. \ Currently only support {SUPPORTED_EXPLAIN_ALGORITHMS}" ) - if not Path(args.save_explanation_to).exists(): - Path(args.save_explanation_to).mkdir(parents=True) + if not Path(args.output).exists(): + Path(args.output).mkdir(parents=True) - image_files = get_image_files(args.explain_data_roots) + 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( @@ -201,7 +200,7 @@ def main(): process_saliency_maps=explain_parameters.process_saliency_maps, img=explained_data.numpy, saliency_map=saliency_data.numpy, - save_dir=args.save_explanation_to, + save_dir=args.output, fname=fname, weight=args.overlay_weight, ) diff --git a/src/otx/cli/tools/utils/demo/visualization.py b/src/otx/cli/tools/utils/demo/visualization.py index 09edd4abfe2..e405d61ff7c 100644 --- a/src/otx/cli/tools/utils/demo/visualization.py +++ b/src/otx/cli/tools/utils/demo/visualization.py @@ -71,7 +71,7 @@ def draw_masks(frame: Mat, predictions, put_object_count: bool = False): 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, label.name, (rect[0], rect[1]), color=color) + 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, @@ -110,7 +110,7 @@ def put_labels(frame: Mat, predictions: List[Annotation]): 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, label.name, (0, 0), color=color) + put_text_on_rect_bg(frame, f"{label.name} {label.probability*100:.1f}%", (0, 0), color=color) return frame @@ -129,7 +129,7 @@ def draw_bounding_boxes(frame: Mat, predictions: List[Annotation], put_object_co 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, label.name, (x1, y1), color=color) + 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)}." diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index 5ca1624854e..93bf558bf34 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -859,16 +859,16 @@ def otx_explain_testing(template, root, otx_dir, args, trained=False): save_dir = f"explain_{template.model_template_id}/{test_algorithm}/{train_type}/" output_dir = os.path.join(template_work_dir, save_dir) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, @@ -899,16 +899,16 @@ def otx_explain_testing_all_classes(template, root, otx_dir, args): save_dir = f"explain_all_classes_{template.model_template_id}/{test_algorithm}/{train_type}/" output_dir = os.path.join(template_work_dir, save_dir) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, @@ -923,7 +923,7 @@ def otx_explain_testing_all_classes(template, root, otx_dir, args): 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] == ".tiff" for fname in os.listdir(output_dir)]) + 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): @@ -945,16 +945,16 @@ def otx_explain_testing_process_saliency_maps(template, root, otx_dir, args, tra 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) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, @@ -986,16 +986,16 @@ def otx_explain_openvino_testing(template, root, otx_dir, args, trained=False): save_dir = f"explain_ov_{template.model_template_id}/{test_algorithm}/{train_type}/" output_dir = os.path.join(template_work_dir, save_dir) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, @@ -1027,16 +1027,16 @@ def otx_explain_all_classes_openvino_testing(template, root, otx_dir, args): 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) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, @@ -1052,7 +1052,7 @@ def otx_explain_all_classes_openvino_testing(template, root, otx_dir, args): 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] == ".tiff" for fname in os.listdir(output_dir)]) + 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): @@ -1074,16 +1074,16 @@ def otx_explain_process_saliency_maps_openvino_testing(template, root, otx_dir, 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) - explain_data_root = os.path.join(otx_dir, args["--input"]) + data_input = os.path.join(otx_dir, args["--input"]) command_line = [ "otx", "explain", template.model_template_path, "--load-weights", weights_path, - "--explain-data-root", - explain_data_root, - "--save-explanation-to", + "--input", + data_input, + "--output", output_dir, "--explain-algorithm", test_algorithm, From 1c084ecb5cbf9b0238f56bd2f29ef356620b71ed Mon Sep 17 00:00:00 2001 From: Galina Zalesskaya Date: Fri, 1 Dec 2023 09:28:54 +0200 Subject: [PATCH 137/146] Fix e2e tests for explain (#2681) --- tests/test_suite/run_test_command.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_suite/run_test_command.py b/tests/test_suite/run_test_command.py index 93bf558bf34..2375467399c 100644 --- a/tests/test_suite/run_test_command.py +++ b/tests/test_suite/run_test_command.py @@ -877,7 +877,7 @@ def otx_explain_testing(template, root, otx_dir, args, trained=False): assert os.path.exists(output_dir) if trained: assert len(os.listdir(output_dir)) > 0 - assert all([os.path.splitext(fname)[1] == ".tiff" for fname in os.listdir(output_dir)]) + 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): @@ -964,7 +964,7 @@ def otx_explain_testing_process_saliency_maps(template, root, otx_dir, args, tra assert os.path.exists(output_dir) if trained: assert len(os.listdir(output_dir)) > 0 - assert all([os.path.splitext(fname)[1] == ".png" for fname in os.listdir(output_dir)]) + 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): @@ -1005,7 +1005,7 @@ def otx_explain_openvino_testing(template, root, otx_dir, args, trained=False): assert os.path.exists(output_dir) if trained: assert len(os.listdir(output_dir)) > 0 - assert all([os.path.splitext(fname)[1] == ".tiff" for fname in os.listdir(output_dir)]) + 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): @@ -1094,7 +1094,7 @@ def otx_explain_process_saliency_maps_openvino_testing(template, root, otx_dir, assert os.path.exists(output_dir) if trained: assert len(os.listdir(output_dir)) > 0 - assert all([os.path.splitext(fname)[1] == ".png" for fname in os.listdir(output_dir)]) + assert all([os.path.splitext(fname)[1] in [".png", ".log"] for fname in os.listdir(output_dir)]) def otx_find_testing(): From 802dc0f10c73a265f804a7f4890c531280a73df5 Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Tue, 5 Dec 2023 10:02:57 +0900 Subject: [PATCH 138/146] Add README.md for experiment.py (#2688) * write draft readme * refine readme * align with pre-commit --- tools/README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tools/README.md diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000000..0f5dcd3c187 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,94 @@ +# 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" 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. + +Sample 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} + +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. + +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" 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. From 5333049536ff49e43d83b037dad2e466d323167a Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 5 Dec 2023 12:56:59 +0900 Subject: [PATCH 139/146] Fix typo in reg test cmd (#2691) --- tests/regression/regression_command.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/regression/regression_command.py b/tests/regression/regression_command.py index 1be19e172cd..8fa0b1e919a 100644 --- a/tests/regression/regression_command.py +++ b/tests/regression/regression_command.py @@ -59,7 +59,8 @@ def regression_eval_testing( regression_result["passed"] = False regression_result[ "log" - ] = f"[{template.name}] Performance: ({trained_performance[k]}) < Criteria: ({model_criteria}), threshold: {threshold}." + ] = 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 @@ -220,17 +221,20 @@ def regression_nncf_eval_testing( regression_result["passed"] = False regression_result[ "log" - ] = f"[{template.name}] NNCF performance is lower than criteria: {evaluated_performance[k]=}, {model_criteria=}, {threshold=}" + ] = 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]=}, {train_performance=}" + ] = 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]=}, {trained_performance[k]=}, {threshold=}" + ] = f"[{template.name}] NNCF train & eval delta is too big: {evaluated_performance[k]=}, " + f"{trained_performance[k]=}, {threshold=}" return regression_result From 73a7442cca3afced9b8bf1749197e08aedd2c07a Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 6 Dec 2023 12:50:49 +0900 Subject: [PATCH 140/146] Select more proper model weight file according to commands run just before (#2696) * consider more complex case when prepare eval and optimize * update readme * align with pre-commit * add comment --- tools/README.md | 8 ++++---- tools/experiment.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/tools/README.md b/tools/README.md index 0f5dcd3c187..dcc82a39007 100644 --- a/tools/README.md +++ b/tools/README.md @@ -10,7 +10,7 @@ The primary goal is to reduce the manual effort required in running experiments ### 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" command is executed, based on the preceding command. +- Proper model files are selected automatically when the "otx eval" or "otx optimize" command is executed, based on the preceding command. ### Fault Tolerance @@ -59,7 +59,7 @@ Sample Experiment Recipe YAML File: Arguments for recipe -- output*path (optional) : Output path where all experiment outputs are saved. Default is "./experiment*{executed_time}" +- 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. @@ -80,9 +80,9 @@ If there are failed cases, variables and error logs are both printed and saved a 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" command is executed, the model file (model weight or exported model, etc.) +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.}/" +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 diff --git a/tools/experiment.py b/tools/experiment.py index 4b7f1aa7870..abae60a9f40 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -667,7 +667,10 @@ class OtxCommandRunner: repeat_idx (int): repeat index. """ - OUTPUT_FILE_NAME = {"export": "openvino.bin", "optimize": "weights.pth"} + OUTPUT_FILE_NAME: Dict[str, List[str]] = { + "export": ["openvino.bin"], + "optimize": ["weights.pth", "openvino.bin"] + } def __init__(self, command_ins: Command, repeat_idx: int): self._command_ins = command_ins @@ -676,7 +679,7 @@ def __init__(self, command_ins: Command, repeat_idx: int): self._workspace = Path("_".join(self._command_var.values()).replace("/", "_") + f"_repeat_{repeat_idx}") self._command_var["repeat"] = str(repeat_idx) self._fail_logs: List[CommandFailInfo] = [] - self._previous_cmd_entry: Optional[str] = None + self._previous_cmd_entry: Optional[List[str]] = [] @property def fail_logs(self) -> List[CommandFailInfo]: @@ -696,7 +699,7 @@ def run_command_list(self, dryrun: bool = False): else: print(" ".join(command)) - self._previous_cmd_entry = command[1] + self._previous_cmd_entry.append(command[1]) if not dryrun: organize_exp_result(self._workspace, self._command_var) @@ -704,11 +707,16 @@ def run_command_list(self, dryrun: bool = False): 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 self._previous_cmd_entry in self.OUTPUT_FILE_NAME: - file_path = self._find_model_path(self._previous_cmd_entry) + 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)) @@ -716,6 +724,12 @@ def _prepare_run_command(self, command: List[str]) -> bool: 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 @@ -731,11 +745,13 @@ def _find_model_path(self, cmd_entry: str): if output_dir is None: print(f"There is no {cmd_entry} output directory.") return None - file_path = list(output_dir.rglob(self.OUTPUT_FILE_NAME[cmd_entry])) - if not file_path: - print(f"{self.OUTPUT_FILE_NAME[cmd_entry]} can't be found.") - return None - return file_path[0] + 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): From e3e1498966b316a76864fd65c0db57a4e624337a Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Mon, 11 Dec 2023 10:37:00 +0900 Subject: [PATCH 141/146] Add visual prompting zero-shot learning (`learn` & `infer`) (#2616) * Add algobackend & temp configs * Update config * WIP * Fix to enable `algo_backend` * (WIP) Update dataset * (WIP) Update configs * (WIP) Update tasks * (WIP) Update models * Enable `learn` task through otx.train * (WIP) enable `infer` (TODO : normalize points) * Fix when `state_dict` is None * Enable `ZeroShotInferenceCallback` * Enable otx infer * Enable to independently use processor * Revert max_steps * Change `postprocess_masks` to `staticmethod` * Add `PromptGetter` & Enable `learn` and `infer` * precommit * Fix args * Fix typo * Change `id` to `id_` * Fix import * Fix args * precommit * (WIP) Add unit tests * Fix * Add unit tests * Fix * Add integration tests * precommit * Update CHANGELOG.md * Update docstring and type annotations * Fix * precommit * Fix unused args * precommit * Fix --- CHANGELOG.md | 4 + .../common/configs/training_base.py | 1 + .../pytorch_lightning/callbacks/__init__.py | 2 +- .../pytorch_lightning/callbacks/inference.py | 38 ++ .../config/visual_prompting_config.py | 8 +- .../pytorch_lightning/datasets/dataset.py | 93 ++- .../datasets/pipelines/sam_transforms.py | 19 +- .../pytorch_lightning/models/__init__.py | 2 +- .../models/visual_prompters/__init__.py | 1 + .../visual_prompters/segment_anything.py | 2 +- .../zero_shot_segment_anything.py | 611 ++++++++++++++++++ .../configs/base/configuration.py | 6 + .../configs/configuration.yaml | 62 ++ .../zero_shot_sam_tiny_vit/__init__.py | 6 + .../zero_shot_sam_tiny_vit/config.yaml | 78 +++ .../zero_shot_sam_tiny_vit/configuration.py | 14 + .../zero_shot_sam_tiny_vit/configuration.yaml | 210 ++++++ .../ptq_optimization_config.py | 22 + .../template_experimental.yaml | 38 ++ .../visual_prompting/tasks/__init__.py | 2 +- .../visual_prompting/tasks/inference.py | 125 +++- .../visual_prompting/tasks/train.py | 4 +- .../visual_prompting/test_visual_prompting.py | 10 +- .../cli/visual_prompting/test_zero_shot.py | 53 ++ .../callbacks/test_inference_callback.py | 61 ++ .../config/test_visual_prompting_config.py | 8 +- .../datasets/pipelines/test_sam_transforms.py | 2 +- .../datasets/test_dataset.py | 84 ++- .../visual_prompters/test_segment_anything.py | 37 +- .../test_zero_shot_segment_anything.py | 321 +++++++++ .../visual_prompting/tasks/test_inference.py | 95 ++- .../visual_prompting/test_helpers.py | 74 ++- 32 files changed, 1999 insertions(+), 94 deletions(-) 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/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_experimental.yaml create mode 100644 tests/integration/cli/visual_prompting/test_zero_shot.py create mode 100644 tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 83aa891d441..d63284cd749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## \[unreleased\] +### New features + +- Add zero-shot visual prompting (https://github.com/openvinotoolkit/training_extensions/pull/2616) + ## \[v1.5.0\] ### New features diff --git a/src/otx/algorithms/common/configs/training_base.py b/src/otx/algorithms/common/configs/training_base.py index 929c7f8b7ad..8e924899ef7 100644 --- a/src/otx/algorithms/common/configs/training_base.py +++ b/src/otx/algorithms/common/configs/training_base.py @@ -31,6 +31,7 @@ class TrainType(ConfigurableEnum): Semisupervised = "Semisupervised" Selfsupervised = "Selfsupervised" Incremental = "Incremental" + Zeroshot = "Zeroshot" Futurework = "Futurework" 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 index cefcf725417..0b8f9b0c619 100644 --- a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/__init__.py @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions # and limitations under the License. -from .inference import InferenceCallback # noqa: F401 +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 index 6d077123f68..9aec96bde56 100644 --- a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/callbacks/inference.py @@ -17,6 +17,7 @@ from typing import Any, List import numpy as np +import torch from bson import ObjectId from pytorch_lightning import LightningModule, Trainer from pytorch_lightning.callbacks import Callback @@ -25,6 +26,7 @@ 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, @@ -94,3 +96,39 @@ def on_predict_epoch_end(self, _trainer: Trainer, _pl_module: LightningModule, o 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(): + hard_prediction = torch.where(torch.stack(masks, dim=0).sum(dim=0) > 0, 1, 0) + hard_prediction = hard_prediction.numpy() + + # TODO (sungchul): consider use_mask + # generate polygon annotations + annotation = create_annotation_from_segmentation_map( + hard_prediction=hard_prediction, + soft_prediction=hard_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/visual_prompting_config.py b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/config/visual_prompting_config.py index 6a212c9cbb8..3e4cbe8b574 100644 --- 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 @@ -96,7 +96,13 @@ def update_visual_prompting_config( groups = getattr(otx_config, "groups", None) if groups: for group in groups: - if group in ["learning_parameters", "nncf_optimization", "pot_parameters", "postprocessing"]: + 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.") 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 index 9f79eeda019..51b78e56880 100644 --- a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/dataset.py @@ -24,6 +24,7 @@ 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, @@ -129,6 +130,13 @@ def generate_bbox_from_mask(gt_mask: np.ndarray, width: int, height: int) -> Lis return generate_bbox(x_min, y_min, x_max, y_max, width, height) +def generate_point_from_mask(gt_mask: np.ndarray) -> np.ndarray: + """Randomly generate point from given mask.""" + candidates = np.where(gt_mask == 1) + index = np.random.permutation(len(candidates))[0] + return candidates[index] + + class OTXVisualPromptingDataset(Dataset): """Visual Prompting Dataset Adaptor. @@ -236,6 +244,27 @@ def __getitem__(self, index: int) -> Dict[str, Union[int, List, Tensor]]: 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.generate_point, self.generate_bbox) + item.update({**prompts, "path": dataset_item.media.path}) + item = self.transform(item) + return item + + class OTXVisualPromptingDataModule(LightningDataModule): """Visual Prompting DataModule. @@ -244,10 +273,39 @@ class OTXVisualPromptingDataModule(LightningDataModule): dataset (DatasetEntity): Dataset entity. """ - def __init__(self, config: Union[DictConfig, ListConfig], dataset: DatasetEntity) -> None: + 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( + # { + # "generate_point": self.config.get("generate_point", False), + # "generate_bbox": self.config.get("generate_bbox", False), + # } + # ) self.train_otx_dataset: DatasetEntity self.val_otx_dataset: DatasetEntity @@ -267,21 +325,34 @@ def setup(self, stage: Optional[str] = None) -> None: mean = self.config.normalize.mean std = self.config.normalize.std if stage == "fit" or stage is None: - train_otx_dataset = self.dataset.get_subset(Subset.TRAINING) - val_otx_dataset = self.dataset.get_subset(Subset.VALIDATION) - - self.train_dataset = OTXVisualPromptingDataset( - train_otx_dataset, image_size, mean, std, offset_bbox=self.config.offset_bbox + self.train_dataset = self.DATASETS[self.train_type]( + 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 = OTXVisualPromptingDataset(val_otx_dataset, image_size, mean, std) + + # self.val_dataset = None + if self.train_type == TrainType.Incremental: + self.val_dataset = self.DATASETS[self.train_type]( + dataset=self.dataset.get_subset(Subset.VALIDATION), image_size=image_size, mean=mean, std=std + ) if stage == "test": - test_otx_dataset = self.dataset.get_subset(Subset.TESTING) - self.test_dataset = OTXVisualPromptingDataset(test_otx_dataset, image_size, mean, std) + self.test_dataset = self.DATASETS[self.train_type]( + dataset=self.dataset.get_subset(Subset.TESTING), image_size=image_size, mean=mean, std=std + ) if stage == "predict": - predict_otx_dataset = self.dataset - self.predict_dataset = OTXVisualPromptingDataset(predict_otx_dataset, image_size, mean, std) + self.predict_dataset = self.DATASETS[self.train_type]( + dataset=self.dataset, + image_size=image_size, + mean=mean, + std=std, + # **self.kwargs + ) def summary(self): """Print size of the dataset, number of images.""" 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 index aeb0cc98baf..fd9b1a3057b 100644 --- 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 @@ -40,7 +40,7 @@ def __call__(self, item: Dict[str, Union[List, Tensor]]) -> Dict[str, Union[List item["gt_masks"] = [torch.as_tensor(gt_mask) for gt_mask in item["gt_masks"]] item["bboxes"] = self.apply_boxes(item["bboxes"], item["original_size"]) if item["points"]: - item["points"] = self.apply_coords(item["points"], item["original_size"]) + item["points"] = self.apply_coords(item["points"], item["original_size"], self.target_length) return item @classmethod @@ -57,21 +57,28 @@ def apply_image(cls, image: np.ndarray, target_length: int) -> np.ndarray: target_size = cls.get_preprocess_shape(image.shape[0], image.shape[1], target_length) return np.array(resize(to_pil_image(image), target_size)) - def apply_coords(self, coords: np.ndarray, original_size: Union[List[Any], Tensor]) -> np.ndarray: + @classmethod + def apply_coords( + cls, coords: Union[np.ndarray, Tensor], original_size: Union[List[Any], Tensor], target_length: int + ) -> np.ndarray: """Expects a numpy array of length 2 in the final dimension. Requires the original image size in (H, W) format. Args: - coords (np.ndarray): Coordinates array. + coords (Union[np.ndarray, Tensor]): Coordinates array. original_size (Union[List[Any], Tensor]): Original size of image. + target_length (int): The length of the longest side of the image. Returns: np.ndarray: Resized coordinates. """ old_h, old_w = original_size - new_h, new_w = self.get_preprocess_shape(original_size[0], original_size[1], self.target_length) - coords = deepcopy(coords).astype(float) + new_h, new_w = cls.get_preprocess_shape(original_size[0], original_size[1], target_length) + if isinstance(coords, np.ndarray): + coords = deepcopy(coords).astype(np.float32) + else: + coords = deepcopy(coords).to(torch.float32) coords[..., 0] = coords[..., 0] * (new_w / old_w) coords[..., 1] = coords[..., 1] * (new_h / old_h) return coords @@ -86,7 +93,7 @@ def apply_boxes(self, boxes: np.ndarray, original_size: Union[List[Any], Tensor] Returns: np.ndarray: Resized boxes. """ - boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size) + boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size, self.target_length) return boxes.reshape(-1, 4) @staticmethod 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 index 7f9dcc70d88..49caf262735 100644 --- a/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/__init__.py @@ -3,4 +3,4 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from .visual_prompters import SegmentAnything # noqa: F401 +from .visual_prompters import SegmentAnything, ZeroShotSegmentAnything # noqa: F401 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 index 9d6eec48e1f..c7493b86fa6 100644 --- 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 @@ -4,3 +4,4 @@ # 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 index 12672dd939c..3b84daa72b8 100644 --- 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 @@ -552,8 +552,8 @@ def predict_step(self, batch, batch_idx) -> Dict[str, Tensor]: return dict(masks=masks, iou_predictions=iou_predictions, path=batch["path"], labels=batch["labels"]) + @staticmethod def postprocess_masks( - self, masks: Tensor, input_size: Tuple[int, int], padding: Tuple[int, ...], 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..a915862523c --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/zero_shot_segment_anything.py @@ -0,0 +1,611 @@ +"""SAM module for visual prompting zero-shot learning.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from collections import OrderedDict, defaultdict +from copy import deepcopy +from typing import Any, DefaultDict, Dict, List, Optional, Tuple + +import torch +from omegaconf import DictConfig +from torch import nn +from torch.nn import functional as F + +from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ResizeLongestSide +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) -> None: + super().__init__() + self.image_size = image_size + self.initialize() + + def initialize(self) -> None: + """Initialize reference features and prompts.""" + self.reference_feats: Dict[int, torch.Tensor] = {} + self.reference_prompts: Dict[int, torch.Tensor] = {} + + 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 set_reference(self, label: ScoredLabel, reference_feats: torch.Tensor, reference_prompts: torch.Tensor) -> None: + """Set reference features and prompts.""" + self.reference_feats[int(label.id_)] = reference_feats + self.reference_prompts[int(label.id_)] = reference_prompts + + def forward( + self, + image_embeddings: torch.Tensor, + padding: Tuple[int, ...], + original_size: Tuple[int, int], + ) -> Dict[int, Tuple[torch.Tensor, torch.Tensor]]: + """Get prompt candidates.""" + target_feat = image_embeddings.squeeze() + c_feat, h_feat, w_feat = target_feat.shape + target_feat = self._preprocess_target_feat(target_feat, c_feat, h_feat, w_feat) + + prompts = {} + for label, reference_feat in self.reference_feats.items(): + sim = reference_feat.to(target_feat.device) @ target_feat + sim = sim.reshape(1, 1, h_feat, w_feat) + sim = ZeroShotSegmentAnything.postprocess_masks( + sim, (self.image_size, self.image_size), padding, original_size + ).squeeze() + + # threshold = 0.85 * sim.max() if num_classes > 1 else self.default_threshold_target + threshold = self.default_threshold_target + points_scores, bg_coords = self._point_selection(sim, original_size, threshold) + if points_scores is None: + # skip if there is no point with score > threshold + continue + prompts[label] = (points_scores, bg_coords) + return prompts + + def _preprocess_target_feat(self, target_feat: torch.Tensor, c_feat: int, h_feat: int, w_feat: int) -> torch.Tensor: + target_feat = target_feat / target_feat.norm(dim=0, keepdim=True) + target_feat = target_feat.reshape(c_feat, h_feat * w_feat) + return target_feat + + def _point_selection( + self, + mask_sim: torch.Tensor, + original_size: Tuple[int, int], + threshold: float, + num_bg_points: int = 1, + downsizing: int = 16, + ) -> Tuple[torch.Tensor, torch.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 + + point_coords = torch.where(mask_sim > threshold) + if len(point_coords[0]) == 0: + return None, None + + fg_coords_scores = torch.stack(point_coords[::-1] + (mask_sim[point_coords],), dim=0).T + + max_len = max(original_size) + ratio = self.image_size / max_len + _, width = map(lambda x: int(x * ratio), original_size) + n_w = width // downsizing + + res = (fg_coords_scores[:, 1] * ratio // downsizing * n_w + fg_coords_scores[:, 0] * ratio // downsizing).to( + torch.int32 + ) + points_scores = torch.stack([fg_coords_scores[res == r][0] for r in torch.unique(res)], dim=0) + points_scores = points_scores[torch.argsort(points_scores[:, -1], descending=True)] + + return points_scores, bg_coords + + +class ZeroShotSegmentAnything(SegmentAnything): + """Zero-shot learning module using Segment Anything.""" + + def __init__(self, config: Optional[DictConfig] = None, state_dict: Optional[OrderedDict] = None) -> None: + if config is None: + config = self.set_default_config() + + if not config.model.freeze_image_encoder: + logger.warning("config.model.freeze_image_encoder(=False) must be set to True, changed.") + config.model.freeze_image_encoder = True + + if not config.model.freeze_prompt_encoder: + logger.warning("config.model.freeze_prompt_encoder(=False) must be set to True, changed.") + config.model.freeze_prompt_encoder = True + + if not config.model.freeze_mask_decoder: + logger.warning("config.model.freeze_mask_decoder(=False) must be set to True, changed.") + config.model.freeze_mask_decoder = True + + prompt_getter_reference_feats = None + prompt_getter_reference_prompts = None + if state_dict: + if "prompt_getter.reference_feats" in state_dict: + prompt_getter_reference_feats = state_dict.pop("prompt_getter.reference_feats") + if "prompt_getter.reference_prompts" in state_dict: + prompt_getter_reference_prompts = state_dict.pop("prompt_getter.reference_prompts") + + super().__init__(config, state_dict) + + self.prompt_getter = PromptGetter(image_size=config.model.image_size) + self.prompt_getter.initialize() + self.prompt_getter.set_default_thresholds( + config.model.default_threshold_reference, config.model.default_threshold_target + ) + + if prompt_getter_reference_feats: + self.prompt_getter.reference_feats = prompt_getter_reference_feats + if prompt_getter_reference_prompts: + self.prompt_getter.reference_prompts = prompt_getter_reference_prompts + + 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, + } + } + ) + + @torch.no_grad() + def learn( + self, + images: torch.Tensor, + processed_prompts: Dict[ScoredLabel, List[Dict[str, torch.Tensor]]], + padding: Tuple[int, ...], + original_size: Tuple[int, int], + ) -> None: + """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: + images (torch.Tensor): Given images for reference features. + processed_prompts (Dict[ScoredLabel, List[Dict[str, torch.Tensor]]]): The whole class-wise prompts + processed at _preprocess_prompts. + padding (Tuple[int, ...]): Padding size. + original_size (Tuple[int, int]): Original image size. + """ + assert images.shape[0] == 1, "Only single batch is supported." + + self.prompt_getter.initialize() + + image_embeddings = self.image_encoder(images) + ref_feat = image_embeddings.squeeze().permute(1, 2, 0) + + for label, input_prompts in processed_prompts.items(): + if label.name.lower() == "background": + # skip background + # TODO (sungchul): how to skip background class + continue + + # generate reference mask + # TODO (sungchul): ensemble multi reference features (current : use merged masks) + reference_prompt = torch.zeros(original_size, dtype=torch.uint8, device=images.device) + for input_prompt in input_prompts: + if "annotation" in input_prompt: + # directly use annotation information as a mask + reference_prompt[input_prompt.get("annotation") == 1] += 1 + else: + merged_input_prompts = self._merge_prompts(label, input_prompt, processed_prompts) + masks, scores, logits = self._predict_mask( + image_embeddings=image_embeddings, + input_prompts=merged_input_prompts, + padding=padding, + original_size=original_size, + multimask_output=True, + ) + best_idx = torch.argmax(scores) + reference_prompt[masks[0, best_idx]] += 1 + reference_prompt = torch.clip(reference_prompt, 0, 1) + + ref_mask = torch.tensor(reference_prompt, dtype=torch.float32) + reference_feat = None + default_threshold_reference = deepcopy(self.prompt_getter.default_threshold_reference) + while reference_feat is None: + logger.info(f"[*] default_threshold_reference : {default_threshold_reference:.4f}") + reference_feat = self._generate_masked_features( + ref_feat, ref_mask, default_threshold_reference, padding=padding + ) + default_threshold_reference -= 0.05 + + self.prompt_getter.set_reference(label, reference_feat.detach().cpu(), reference_prompt.detach().cpu()) + + @torch.no_grad() + def infer( + self, images: torch.Tensor, padding: Tuple[int, ...], original_size: Tuple[int, int] + ) -> List[List[DefaultDict[int, List[torch.Tensor]]]]: + """Zero-shot inference with reference features. + + Get target results by using reference features and target images' features. + + Args: + images (torch.Tensor): Given images for target results. + padding (Tuple[int, ...]): Padding size. + original_size (Tuple[int, int]): Original image size. + + Returns: + (List[List[DefaultDict[int, List[torch.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 + """ + assert images.shape[0] == 1, "Only single batch is supported." + + total_results = [] + # num_classes = len(self.reference_feats.keys()) + for image in images: + if image.ndim == 3: + image = image.unsqueeze(0) + + image_embeddings = self.image_encoder(images) + + prompts = self.prompt_getter( + image_embeddings=image_embeddings, padding=padding, original_size=original_size + ) + predicted_masks: defaultdict = defaultdict(list) + used_points: defaultdict = defaultdict(list) + for label, (points_scores, bg_coords) in prompts.items(): + 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 + + mask, used_point_score = self( + image_embeddings=image_embeddings, + points_score=points_score, + bg_coords=bg_coords, + padding=padding, + original_size=original_size, + ) + predicted_masks[label].append(mask) + used_points[label].append(used_point_score) + + total_results.append([predicted_masks, used_points]) + return total_results + + @torch.no_grad() + def forward( + self, + image_embeddings: torch.Tensor, + points_score: torch.Tensor, + bg_coords: torch.Tensor, + padding: Tuple[int, ...], + original_size: Tuple[int, int], + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Predict point prompts and predicted masks. + + Args: + image_embeddings (torch.Tensor): The image embedding with a batch index of length 1. + points_score (torch.Tensor): Foreground point prompts from point selection algorithm. + bg_coords (torch.Tensor): Background point prompts from point selection algorithm. + padding (Tuple[int, ...]): Padding size. + original_size (Tuple[int, int]): Original image size. + + Returns: + (Tuple[torch.Tensor, torch.Tensor]): Predicted masks and used points with corresponding score. + """ + point_coords = torch.cat((points_score[:2].unsqueeze(0), bg_coords), dim=0).unsqueeze(0) + point_coords = ResizeLongestSide.apply_coords(point_coords, original_size, self.config.model.image_size) + point_labels = torch.tensor([1] + [0] * len(bg_coords), dtype=torch.int32).unsqueeze(0) + mask = self._predict_target_mask( + image_embeddings=image_embeddings, + input_prompts={"points": (point_coords, point_labels)}, + padding=padding, + original_size=original_size, + ) + + return mask.detach().cpu().to(torch.uint8), points_score.detach().cpu() + + def training_step(self, batch, batch_idx) -> None: + """Training step for `learn`.""" + # TODO (sungchul): each prompt will be assigned with each label + bboxes = batch["bboxes"] + labels = batch["labels"] + # TODO (sungchul): support other below prompts + # points = batch["points"] + # annotations = batch["annotations"] + + # organize prompts based on label + processed_prompts = self._preprocess_prompts(bboxes=bboxes[0], labels=labels[0]) + + self.learn( + images=batch["images"], + processed_prompts=processed_prompts, + padding=batch.get("padding")[0], + original_size=batch.get("original_size")[0], + ) + + def predict_step(self, batch, batch_idx): + """Predict step for `infer`.""" + results = self.infer( + images=batch["images"], padding=batch.get("padding")[0], original_size=batch.get("original_size")[0] + ) + return [result[0] for result in results] # tmp: only mask + + def _preprocess_prompts( + self, + bboxes: Optional[torch.Tensor] = None, + points: Optional[torch.Tensor] = None, + annotations: Optional[torch.Tensor] = None, + labels: Optional[torch.Tensor] = None, + ) -> Dict[ScoredLabel, List[Dict[str, torch.Tensor]]]: + """Preprocess prompts. + + Currently, preprocessing for bounding boxes is only supported. + + Args: + bboxes (torch.Tensor, optional): Bounding box prompts to be preprocessed. + points (torch.Tensor, optional): Point prompts to be preprocessed, to be supported. + annotations (torch.Tensor, optional): annotation prompts to be preprocessed, to be supported. + labels (torch.Tensor, optional): Assigned labels according to given prompts. + Currently, it is only matched to bboxes, and it will be deprecated. + + Returns: + (defaultdict[ScoredLabel, List[Dict[str, torch.Tensor]]]): 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) + # TODO (sungchul): will be updated + if bboxes is not None: + for bbox, label in zip(bboxes, labels): + processed_prompts[label].append({"box": bbox.reshape(-1, 4)}) + + if points: + pass + + if annotations: + pass + + processed_prompts = dict(sorted(processed_prompts.items(), key=lambda x: x[0].id_)) # type: ignore[assignment] + return processed_prompts + + def _generate_masked_features( + self, feats: torch.Tensor, masks: torch.Tensor, threshold_mask: float, padding: Optional[Tuple[int, ...]] = None + ) -> Tuple[torch.Tensor, ...]: + """Generate masked features. + + Args: + feats (torch.Tensor): Raw reference features. It will be filtered with masks. + masks (torch.Tensor): Reference masks used to filter features. + threshold_mask (float): Threshold to control masked region. + padding (Tuple[int, ...], optional): Padding size. + + Returns: + (torch.Tensor): Masked features. + """ + if padding: + resized_size = ( + self.config.model.image_size - padding[1] - padding[3], + self.config.model.image_size - padding[0] - padding[2], + ) + else: + resized_size = (self.config.model.image_size, self.config.model.image_size) + + # Post-process masks + masks = F.interpolate(masks.unsqueeze(0).unsqueeze(0), size=resized_size, mode="bilinear").squeeze() + masks = self._preprocess_mask(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, 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 _preprocess_mask(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input. + + Args: + x (torch.Tensor): Mask to be padded. + + Returns: + (torch.Tensor): Padded mask. + """ + # Pad + 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 _update_value(self, target: Dict[str, Any], key: str, value: torch.Tensor) -> None: + """Update tensor to target dictionary. + + Args: + target (Dict[str, Any]): Target dictionary to be updated. + key (str): Key to be used for update. + value (torch.Tensor): Value to be used for update. + """ + if key in target: + target[key] = torch.cat((target[key], value)) + else: + target[key] = value + + def _merge_prompts( + self, + label: ScoredLabel, + input_prompts: Dict[str, torch.Tensor], + processed_prompts: Dict[ScoredLabel, List[Dict[str, torch.Tensor]]], + use_only_background: bool = True, + ) -> Dict[str, torch.Tensor]: + """Merge target prompt and other prompts. + + Merge a foreground prompt and other prompts (background or prompts with other classes). + + Args: + label (ScoredLabel): Label information. Background is 0 and other foregrounds are >= 0. + input_prompts (Dict[str, torch.Tensor]): A foreground prompt to be merged with other prompts. + processed_prompts (Dict[ScoredLabel, List[Dict[str, torch.Tensor]]]): The whole class-wise prompts + processed at _preprocess_prompts. + use_only_background (bool): Whether merging only background prompt, defaults to True. + It is applied to only point_coords. + + Returns: + (Dict[str, torch.Tensor]): Merged prompts. + """ + merged_input_prompts = deepcopy(input_prompts) + for other_label, other_input_prompts in processed_prompts.items(): + if other_label.id_ == label.id_: + continue + if (use_only_background and other_label.id_ == 0) or (not use_only_background): + # only add point (and scribble) prompts + # use_only_background=True -> background prompts are only added as background + # use_only_background=False -> other prompts are added as background + for other_input_prompt in other_input_prompts: + if "point_coords" in other_input_prompt: + # point, scribble + self._update_value(merged_input_prompts, "point_coords", other_input_prompt.get("point_coords")) + self._update_value( + merged_input_prompts, + "point_labels", + torch.zeros_like(other_input_prompt.get("point_labels")), + ) + return merged_input_prompts + + def _predict_target_mask( + self, + image_embeddings: torch.Tensor, + input_prompts: Dict[str, Tuple[torch.Tensor, torch.Tensor]], + padding: Tuple[int, ...], + original_size: Tuple[int, int], + ) -> torch.Tensor: + """Predict target masks. + + Args: + image_embeddings (torch.Tensor): The image embedding with a batch index of length 1. + input_prompts (Dict[str, Tuple[torch.Tensor, torch.Tensor]]): Dictionary including point, box, + and mask prompts. index=1 of tuple is point labels which indicate whether foreground or background. + padding (Tuple[int, ...]): Padding size. + original_size (Tuple[int, int]): Original image size. + + Return: + (torch.Tensor): Predicted mask. + """ + # First-step prediction + _, _, logits = self._predict_mask( + image_embeddings, input_prompts, padding, original_size, multimask_output=False + ) + best_idx = 0 + + # Cascaded Post-refinement-1 + input_prompts.update({"masks": logits[:, best_idx : best_idx + 1, :, :]}) + masks, scores, logits = self._predict_mask( + image_embeddings, input_prompts, padding, original_size, multimask_output=True + ) + best_idx = torch.argmax(scores) + + # Cascaded Post-refinement-2 + coords = torch.nonzero(masks[0, best_idx]) + y, x = coords[:, 0], coords[:, 1] + x_min = x.min() + x_max = x.max() + y_min = y.min() + y_max = y.max() + input_prompts.update( + { + "masks": logits[:, best_idx : best_idx + 1, :, :], + "box": torch.tensor([x_min, y_min, x_max, y_max], device=logits.device), + } + ) + masks, scores, _ = self._predict_mask( + image_embeddings, input_prompts, padding, original_size, multimask_output=True + ) + best_idx = torch.argmax(scores) + + return masks[0, best_idx] + + def _predict_mask( + self, + image_embeddings: torch.Tensor, + input_prompts: Dict[str, torch.Tensor], + padding: Tuple[int, ...], + original_size: Tuple[int, int], + multimask_output: bool = True, + ) -> Tuple[torch.Tensor, ...]: + """Predict target masks. + + Args: + image_embeddings (torch.Tensor): The image embedding with a batch index of length 1. + input_prompts (Dict[str, torch.Tensor]): Dictionary including point, box, and mask prompts. + padding (Tuple[int, ...]): Padding size. + original_size (Tuple[int, int]): Original image size. + multimask_output (bool): Whether getting multi mask outputs or not. Defaults to True. + + Return: + (Tuple[torch.Tensor, ...]): Predicted mask, score, and logit. + """ + sparse_embeddings, dense_embeddings = self.prompt_encoder( + points=input_prompts.get("points", None), + boxes=input_prompts.get("box", None), # TODO (sungchul): change key box -> boxes to use **input_prompts + masks=input_prompts.get("masks", None), + ) + + low_res_masks, scores = self.mask_decoder( + image_embeddings=image_embeddings, + image_pe=self.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + ) + high_res_masks = self.postprocess_masks( + low_res_masks, (self.config.model.image_size, self.config.model.image_size), padding, original_size + ) + masks = high_res_masks > self.config.model.mask_threshold + + return masks, scores, low_res_masks + + 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 training_epoch_end(self, outputs) -> None: + """Skip training_epoch_end unused in zero-shot learning.""" + pass diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration.py b/src/otx/algorithms/visual_prompting/configs/base/configuration.py index d9cdae0eaeb..44998684aec 100644 --- a/src/otx/algorithms/visual_prompting/configs/base/configuration.py +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration.py @@ -43,6 +43,11 @@ 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") @@ -112,5 +117,6 @@ class __POTParameter(BaseConfig.BasePOTParameter): ) 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/configuration.yaml b/src/otx/algorithms/visual_prompting/configs/configuration.yaml index 3c216dc6220..86ea7154d7d 100644 --- a/src/otx/algorithms/visual_prompting/configs/configuration.yaml +++ b/src/otx/algorithms/visual_prompting/configs/configuration.yaml @@ -169,5 +169,67 @@ postprocessing: 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/__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..bd923e0b6b7 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/config.yaml @@ -0,0 +1,78 @@ +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 + +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 + +# 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..e88c783c396 --- /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: 100000 + 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_experimental.yaml b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template_experimental.yaml new file mode 100644 index 00000000000..63ff5d3d9d4 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/configs/zero_shot_sam_tiny_vit/template_experimental.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.OpenVINOVisualPromptingTask + +# 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.95 +size: 47 diff --git a/src/otx/algorithms/visual_prompting/tasks/__init__.py b/src/otx/algorithms/visual_prompting/tasks/__init__.py index 9efa2d6ddf9..a4c0a3e0366 100644 --- a/src/otx/algorithms/visual_prompting/tasks/__init__.py +++ b/src/otx/algorithms/visual_prompting/tasks/__init__.py @@ -3,6 +3,6 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from .inference import InferenceTask # noqa: F401 +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 index 9358bd93242..ea8a1fbf869 100644 --- a/src/otx/algorithms/visual_prompting/tasks/inference.py +++ b/src/otx/algorithms/visual_prompting/tasks/inference.py @@ -30,10 +30,13 @@ from omegaconf import DictConfig, ListConfig 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, @@ -55,6 +58,7 @@ ) 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 @@ -84,6 +88,8 @@ def __init__(self, task_environment: TaskEnvironment, output_path: Optional[str] 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)) @@ -128,8 +134,6 @@ def get_config(self) -> Union[DictConfig, ListConfig]: Returns: Union[DictConfig, ListConfig]: Visual Prompting config. """ - self.hyper_parameters: VisualPromptingBaseConfig = self.task_environment.get_hyper_parameters() - # set checkpoints model_checkpoint: Optional[str] = None resume_from_checkpoint: Optional[str] = None @@ -167,13 +171,18 @@ def load_model(self, otx_model: Optional[ModelEntity] = None) -> LightningModule LightningModule: Visual prompting model with/without weights. """ - def get_model(config: DictConfig, state_dict: Optional[OrderedDict] = None): + def get_model(config: DictConfig, train_type: TrainType, state_dict: Optional[OrderedDict] = None): if config.model.name == "SAM": - from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models import ( - SegmentAnything, - ) + 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 ( + ZeroShotSegmentAnything as VisualPrompter, + ) - model = SegmentAnything(config=config, state_dict=state_dict) + 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.") @@ -216,7 +225,7 @@ def get_model(config: DictConfig, state_dict: Optional[OrderedDict] = None): state_dict = model_data try: - model = get_model(config=self.config, state_dict=state_dict) + 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 @@ -238,7 +247,9 @@ def infer(self, dataset: DatasetEntity, inference_parameters: InferenceParameter """ 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) + datamodule = OTXVisualPromptingDataModule( + config=self.config.dataset, dataset=dataset, train_type=self.train_type + ) logger.info("Inference Configs '%s'", self.config) @@ -464,3 +475,99 @@ 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() + # TODO (sungchul): is there more efficient way not to manually add properties? + model_info.update( + { + "prompt_getter.reference_feats": self.model.prompt_getter.reference_feats, + "prompt_getter.reference_prompts": self.model.prompt_getter.reference_prompts, + } + ) + + 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/train.py b/src/otx/algorithms/visual_prompting/tasks/train.py index 344601b7b01..fc2b5311d2d 100644 --- a/src/otx/algorithms/visual_prompting/tasks/train.py +++ b/src/otx/algorithms/visual_prompting/tasks/train.py @@ -71,7 +71,9 @@ def train( # noqa: D102 self.model = self.load_model(otx_model=self.task_environment.model) - datamodule = OTXVisualPromptingDataModule(config=self.config.dataset, dataset=dataset) + 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(), diff --git a/tests/integration/cli/visual_prompting/test_visual_prompting.py b/tests/integration/cli/visual_prompting/test_visual_prompting.py index 9e78e92c531..18d220376a1 100644 --- a/tests/integration/cli/visual_prompting/test_visual_prompting.py +++ b/tests/integration/cli/visual_prompting/test_visual_prompting.py @@ -50,9 +50,13 @@ otx_dir = os.getcwd() -templates = ( - Registry("src/otx/algorithms/visual_prompting", experimental=True).filter(task_type="VISUAL_PROMPTING").templates -) +templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting", experimental=True) + .filter(task_type="VISUAL_PROMPTING") + .templates + if "Zero_Shot" not in template.name +] templates_ids = [template.model_template_id for template in templates] 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..8d403f27999 --- /dev/null +++ b/tests/integration/cli/visual_prompting/test_zero_shot.py @@ -0,0 +1,53 @@ +"""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, +) + +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", + ], +} + +otx_dir = os.getcwd() + + +templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting", experimental=True) + .filter(task_type="VISUAL_PROMPTING") + .templates + if "Zero_Shot" 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 / "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) 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 index 5e572aa464d..39aec2025d4 100644 --- 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 @@ -13,6 +13,7 @@ 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 @@ -23,6 +24,7 @@ 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, ) @@ -99,3 +101,62 @@ def test_on_predict_epoch_end(self, use_mask: bool, expected: Any): 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.5, 0.0), Point(0.0, 0.5), Point(0.5, 1.0), Point(1.0, 0.5)]], + ) + 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 == 1.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 index 105047526b8..32f3e9c6fe7 100644 --- 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 @@ -61,15 +61,16 @@ def test_update_visual_prompting_config(): """Test update_visual_prompting_config.""" otx_config = OmegaConf.create( { - "groups": ["learning_parameters", "pot_parameters", "postprocessing"], + "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"} + {"param1": "value1", "param2": "value2", "param3": "value3", "param4": "value4", "param5": "value5"} ) update_visual_prompting_config(visual_prompting_config, otx_config) @@ -77,4 +78,5 @@ def test_update_visual_prompting_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"] == "value4" + 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/pipelines/test_sam_transforms.py b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/datasets/pipelines/test_sam_transforms.py index 35c00c0198b..68e06b14482 100644 --- 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 @@ -47,7 +47,7 @@ def test_apply_image(self, image: np.ndarray, expected: Tuple[int, int, int]): ) def test_apply_coords(self, coords: np.ndarray, original_size: Tuple[int, int], expected: np.ndarray): """Test apply_coords.""" - result = self.resize_longest_side.apply_coords(coords, original_size) + result = self.resize_longest_side.apply_coords(coords, original_size, self.resize_longest_side.target_length) assert np.array_equal(result, expected) 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 index ec16356882a..c3701eb3f58 100644 --- 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 @@ -5,17 +5,21 @@ # 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, + # generate_point_from_mask, ) from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.pipelines import ( MultipleInputsCompose, @@ -142,6 +146,11 @@ def test_generate_bbox_from_mask(mocker) -> None: assert bbox[3] >= 0 and bbox[3] <= height +@e2e_pytest_unit +def test_generate_point_from_mask() -> None: + """TODO""" + + class TestOTXVIsualPromptingDataset: @e2e_pytest_unit def test_len(self, mocker, dataset_polygon, transform, image_size, mean, std) -> None: @@ -183,20 +192,69 @@ def test_getitem( assert item["points"] == [] +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(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 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 item["points"] == [] + + class TestOTXVisualPromptingDataModule: @pytest.fixture - def datamodule(self) -> OTXVisualPromptingDataModule: - dataset = generate_visual_prompting_dataset() + 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 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) - # Create an instance of OTXVisualPromptingDataModule - return OTXVisualPromptingDataModule(config, dataset) + assert datamodule.config.get("train_batch_size") == 1 + # assert "generate_point" in datamodule.kwargs + # assert "generate_bbox" in datamodule.kwargs @e2e_pytest_unit - def test_setup(self, mocker, datamodule) -> None: + def test_setup(self, mocker, set_datamodule) -> None: """Test setup.""" + datamodule = set_datamodule() mocker.patch.object(datamodule, "summary", return_value=None) datamodule.setup() @@ -205,8 +263,9 @@ def test_setup(self, mocker, datamodule) -> None: assert isinstance(datamodule.val_dataset, OTXVisualPromptingDataset) @e2e_pytest_unit - def test_train_dataloader(self, mocker, datamodule) -> None: + 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") @@ -219,8 +278,9 @@ def test_train_dataloader(self, mocker, datamodule) -> None: assert dataloader.collate_fn == collate_fn @e2e_pytest_unit - def test_val_dataloader(self, mocker, datamodule) -> None: + 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") @@ -233,8 +293,9 @@ def test_val_dataloader(self, mocker, datamodule) -> None: assert dataloader.collate_fn == collate_fn @e2e_pytest_unit - def test_test_dataloader(self, mocker, datamodule) -> None: + 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") @@ -247,8 +308,9 @@ def test_test_dataloader(self, mocker, datamodule) -> None: assert dataloader.collate_fn == collate_fn @e2e_pytest_unit - def test_predict_dataloader(self, datamodule) -> None: + def test_predict_dataloader(self, set_datamodule) -> None: """Test predict_dataloader.""" + datamodule = set_datamodule() datamodule.setup(stage="predict") # Call the predict_dataloader method 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 index 27258658808..799d06f846b 100644 --- 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 @@ -16,44 +16,9 @@ from otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything import ( SegmentAnything, - CKPT_PATHS, ) from tests.test_suite.e2e_test_system import e2e_pytest_unit - - -class MockImageEncoder(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 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]]) +from tests.unit.algorithms.visual_prompting.test_helpers import MockImageEncoder, MockPromptEncoder, MockMaskDecoder class TestSegmentAnything: 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..b4ac5343147 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/pytorch_lightning/models/visual_prompters/test_zero_shot_segment_anything.py @@ -0,0 +1,321 @@ +"""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 +from omegaconf import DictConfig + +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 + + +class TestPromptGetter: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.prompt_getter = PromptGetter(image_size=3) + + @e2e_pytest_unit + def test_initialize(self) -> None: + """Test initialize.""" + assert not self.prompt_getter.reference_feats + assert not self.prompt_getter.reference_prompts + + @e2e_pytest_unit + def test_set_default_thresholds(self) -> None: + """Test set_default_thresholds.""" + assert self.prompt_getter.default_threshold_reference == 0.3 + assert self.prompt_getter.default_threshold_target == 0.65 + + self.prompt_getter.set_default_thresholds(default_threshold_reference=0.5, default_threshold_target=0.7) + + assert self.prompt_getter.default_threshold_reference == 0.5 + assert self.prompt_getter.default_threshold_target == 0.7 + + @e2e_pytest_unit + def test_set_reference(self) -> None: + """Test set_reference.""" + self.prompt_getter.set_reference( + label=MockScoredLabel(label=1), + reference_feats=torch.ones((self.prompt_getter.image_size, self.prompt_getter.image_size)), + reference_prompts=torch.zeros((self.prompt_getter.image_size, self.prompt_getter.image_size)), + ) + + assert self.prompt_getter.reference_feats[1].sum() == 9 + assert self.prompt_getter.reference_prompts[1].sum() == 0 + + @e2e_pytest_unit + def test_forward(self, mocker) -> None: + """Test forward.""" + mocker.patch( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.zero_shot_segment_anything.ZeroShotSegmentAnything" + ) + mocker.patch.object(self.prompt_getter, "_point_selection", return_value=("points_scores", "bg_coords")) + + image_embeddings = torch.rand(1, 2, self.prompt_getter.image_size, self.prompt_getter.image_size) + self.prompt_getter.reference_feats = {1: torch.rand(1, 2)} + + prompts = self.prompt_getter( + image_embeddings=image_embeddings, + padding=(0, 0, 0, 0), + original_size=(self.prompt_getter.image_size, self.prompt_getter.image_size), + ) + + assert 1 in prompts + assert prompts[1] == ("points_scores", "bg_coords") + + @e2e_pytest_unit + def test_preprocess_target_feat(self) -> None: + """Test _preprocess_target_feat.""" + old_target_feat = torch.arange(1, self.prompt_getter.image_size**2 + 1, dtype=torch.float).reshape( + 1, 1, self.prompt_getter.image_size, self.prompt_getter.image_size + ) + new_target_feat = self.prompt_getter._preprocess_target_feat( + target_feat=old_target_feat, + c_feat=1, + h_feat=self.prompt_getter.image_size, + w_feat=self.prompt_getter.image_size, + ) + + assert new_target_feat.sum() == 9 + assert new_target_feat.shape == (1, self.prompt_getter.image_size**2) + + @e2e_pytest_unit + def test_point_selection(self) -> None: + """Test _point_selection.""" + mask_sim = torch.arange(0.1, 1.0, 0.1).reshape(self.prompt_getter.image_size, self.prompt_getter.image_size) + + points_scores, bg_coords = self.prompt_getter._point_selection( + mask_sim=mask_sim, + original_size=(self.prompt_getter.image_size, self.prompt_getter.image_size), + threshold=0.5, + downsizing=1, + ) + + assert torch.equal(points_scores, torch.tensor([[2, 2, 0.9], [1, 2, 0.8], [0, 2, 0.7], [2, 1, 0.6]])) + assert torch.equal(bg_coords, torch.tensor([[0, 0]])) + + +class TestZeroShotSegmentAnything: + @pytest.fixture + def set_zero_shot_segment_anything(self, monkeypatch): + def zero_shot_segment_anything(state_dict: Optional[OrderedDict] = None): + monkeypatch.setattr( + "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SAMImageEncoder", + MockImageEncoder, + ) + return ZeroShotSegmentAnything(state_dict=state_dict) + + return zero_shot_segment_anything + + @e2e_pytest_unit + @pytest.mark.parametrize( + "state_dict", + [ + None, + { + "prompt_getter.reference_feats": "prompt_getter.reference_feats", + "prompt_getter.reference_prompts": "prompt_getter.reference_prompts", + }, + ], + ) + def test_init(self, set_zero_shot_segment_anything, state_dict: Dict[str, Any]) -> None: + """Test __init__.""" + 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: + zero_shot_segment_anything.prompt_getter.reference_feats = "prompt_getter.reference_feats" + zero_shot_segment_anything.prompt_getter.reference_prompts = "prompt_getter.reference_prompts" + + @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_learn(self, mocker, set_zero_shot_segment_anything) -> None: + """Test learn.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + mocker.patch.object( + zero_shot_segment_anything, + "_predict_mask", + return_value=( + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + torch.tensor([1, 0, 0]), + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + ), + ) + + processed_prompts = {MockScoredLabel(label=1, name="label"): [{"box": torch.tensor([[0, 0, 1, 1]])}]} + zero_shot_segment_anything.learn( + images=torch.ones((1, 3, 8, 8)), + processed_prompts=processed_prompts, + padding=(0, 0, 0, 0), + original_size=(8, 8), + ) + + assert zero_shot_segment_anything.prompt_getter.reference_feats.get(1).shape == (1, 2) + assert zero_shot_segment_anything.prompt_getter.reference_prompts.get(1).shape == (8, 8) + + @e2e_pytest_unit + @pytest.mark.parametrize( + "expected", [[torch.tensor([[0, 0, 0], [0, 1, 0], [0, 0, 0]]), 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() + zero_shot_segment_anything.prompt_getter.reference_feats = {1: torch.rand((1, 2))} + zero_shot_segment_anything.prompt_getter.reference_prompts = {1: torch.zeros((8, 8))} + mocker.patch.object( + zero_shot_segment_anything, + "_predict_mask", + return_value=( + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + torch.tensor([1, 0, 0]), + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + ), + ) + + total_results = zero_shot_segment_anything.infer( + images=torch.ones((1, 3, 8, 8)), padding=(0, 0, 0, 0), original_size=(8, 8) + ) + + 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_preprocess_prompts(self, set_zero_shot_segment_anything) -> None: + """Test _preprocess_prompts. + + TODO (sungchul) + - get inputs grouped as label and prompts + - use points and annotations. + """ + zero_shot_segment_anything = set_zero_shot_segment_anything() + bboxes = [torch.tensor([0, 0, 1, 1])] + labels = [MockScoredLabel(label=1)] + processed_prompts = zero_shot_segment_anything._preprocess_prompts( + bboxes=bboxes, + labels=labels, + ) + + # processed_prompts = {labels[0]: [{"box": torch.tensor([[0, 0, 1, 1]])}]} + assert torch.equal(processed_prompts[labels[0]][0].get("box")[0], bboxes[0]) + + @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_preprocess_mask(self, set_zero_shot_segment_anything) -> None: + """Test _preprocess_mask.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + zero_shot_segment_anything.config.model.image_size = 16 + + result = zero_shot_segment_anything._preprocess_mask(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("use_only_background", [True, False]) + def test_merge_prompts(self, set_zero_shot_segment_anything, use_only_background: bool) -> None: + """Test _merge_prompts.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + + input_prompts = {"point_coords": torch.tensor([1]), "point_labels": torch.tensor([1])} + processed_prompts = { + MockScoredLabel(label=0): [{"point_coords": torch.tensor([0]), "point_labels": torch.tensor([0])}], + MockScoredLabel(label=2): [{"point_coords": torch.tensor([2]), "point_labels": torch.tensor([1])}], + } + + merged_input_prompts = zero_shot_segment_anything._merge_prompts( + label=MockScoredLabel(label=1), + input_prompts=input_prompts, + processed_prompts=processed_prompts, + use_only_background=use_only_background, + ) + + if use_only_background: + assert torch.equal(merged_input_prompts.get("point_coords"), torch.tensor([1, 0])) + assert torch.equal(merged_input_prompts.get("point_labels"), torch.tensor([1, 0])) + else: + assert torch.equal(merged_input_prompts.get("point_coords"), torch.tensor([1, 0, 2])) + assert torch.equal(merged_input_prompts.get("point_labels"), torch.tensor([1, 0, 0])) + + @e2e_pytest_unit + def test_predict_target_mask(self, mocker, set_zero_shot_segment_anything) -> None: + """Test _predict_target_mask.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + mocker.patch.object( + zero_shot_segment_anything, + "_predict_mask", + return_value=( + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + torch.tensor([1, 0, 0]), + torch.tensor([[[[0, 0, 0], [0, 1, 0], [0, 0, 0]]]]), + ), + ) + + mask = zero_shot_segment_anything._predict_target_mask( + image_embeddings=torch.rand(1), input_prompts={}, padding=(0, 0, 0, 0), original_size=(1, 1) + ) + + assert mask.shape == (3, 3) + + @e2e_pytest_unit + def test_predict_mask(self, mocker, set_zero_shot_segment_anything) -> None: + """Test _predict_mask.""" + zero_shot_segment_anything = set_zero_shot_segment_anything() + mocker.patch.object(zero_shot_segment_anything, "postprocess_masks", return_value=torch.Tensor([[1]])) + + masks, scores, low_res_masks = zero_shot_segment_anything._predict_mask( + image_embeddings=torch.rand(1), input_prompts={}, padding=(0, 0, 0, 0), original_size=(1, 1) + ) + + assert masks.dtype == torch.bool + assert scores.shape[1] == 3 + assert low_res_masks.shape[1] == 3 diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_inference.py b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py index 12bfb36817c..996d1f97cd1 100644 --- a/tests/unit/algorithms/visual_prompting/tasks/test_inference.py +++ b/tests/unit/algorithms/visual_prompting/tasks/test_inference.py @@ -4,22 +4,24 @@ # SPDX-License-Identifier: Apache-2.0 # -from collections import OrderedDict from typing import Optional, Dict, Any import pytest from omegaconf import DictConfig -from otx.algorithms.visual_prompting.tasks.inference import InferenceTask +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, ) logger = get_logger() @@ -29,9 +31,10 @@ class TestInferenceTask: @pytest.fixture def load_inference_task(self, tmpdir, mocker): def _load_inference_task( - output_path: Optional[str] = str(tmpdir.mkdir("visual_prompting_training_test")), + 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 @@ -41,7 +44,7 @@ def _load_inference_task( mocker.patch.dict(mocker_model.model_adapters, {"path": path, "resume": resume}) mocker.patch("pathlib.Path.write_text") - self.task_environment = init_environment(mocker_model) + self.task_environment = init_environment(mocker_model, mode=mode) return InferenceTask(self.task_environment, output_path) @@ -108,6 +111,22 @@ def test_load_model(self, mocker, load_inference_task, path: str, resume: bool, 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.""" @@ -210,3 +229,71 @@ def test_export(self, mocker, load_inference_task, export_type: ExportType): 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") + + self.zero_shot_task.model.prompt_getter = mocker.MagicMock() + self.zero_shot_task.model.prompt_getter.reference_feats.return_value = "reference_feats" + self.zero_shot_task.model.prompt_getter.reference_prompts.return_value = "reference_prompts" + + 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/test_helpers.py b/tests/unit/algorithms/visual_prompting/test_helpers.py index c01dd322c0b..a9f22c7bf95 100644 --- a/tests/unit/algorithms/visual_prompting/test_helpers.py +++ b/tests/unit/algorithms/visual_prompting/test_helpers.py @@ -5,7 +5,9 @@ # import os -from typing import List, Optional, Tuple, Dict +import torch +import torch.nn as nn +from typing import List, Optional, Tuple, Any import numpy as np @@ -30,7 +32,10 @@ from otx.api.entities.task_environment import TaskEnvironment from tests.test_helpers import generate_random_annotated_image -DEFAULT_VISUAL_PROMPTING_TEMPLATE_DIR = os.path.join("src/otx/algorithms/visual_prompting/configs", "sam_vit_b") +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") @@ -103,9 +108,9 @@ def generate_visual_prompting_dataset(use_mask: bool = False) -> DatasetEntity: return DatasetEntity(items) -def init_environment(model: Optional[ModelEntity] = None): +def init_environment(model: Optional[ModelEntity] = None, mode: str = "visual_prompt"): model_template = parse_model_template( - os.path.join(DEFAULT_VISUAL_PROMPTING_TEMPLATE_DIR, "template_experimental.yaml") + os.path.join(DEFAULT_VISUAL_PROMPTING_TEMPLATE_DIR.get(mode), "template_experimental.yaml") ) hyper_parameters = create(model_template.hyper_parameters.data) labels_schema = generate_otx_label_schema() @@ -133,7 +138,68 @@ def __init__(self, use_mask: bool = False): 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 __init__(self, *args, **kwargs): + super().__init__() + self.backbone = nn.Linear(1, 1) + + def forward(self, *args, **kwargs): + # return torch.Tensor([[1]]) + return torch.ones((1, 2, 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]]) + + +class MockScoredLabel: + def __init__(self, label: int, name: str = "background"): + self.name = name + self.id_ = label + + +class MockPromptGetter(nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + + def initialize(self): + pass + + def set_default_thresholds(self, *args, **kwargs): + pass + + def forward(self, *args, **kwargs): + return { + MockScoredLabel(label=1, name="label"): (torch.tensor([[0, 0, 0.5], [1, 1, 0.7]]), torch.tensor([[2, 2]])) + } From 4ef86da4831ba446afc63a62f2f961a5156fdfb1 Mon Sep 17 00:00:00 2001 From: Vladislav Sovrasov Date: Tue, 12 Dec 2023 01:15:49 +0100 Subject: [PATCH 142/146] Fix unsupported dtype in ov graph constant converter (#2676) * Fix unsupported dtype in ov graph constant converter * Fix more ov-graph related unit tests --- src/otx/core/ov/ops/infrastructures.py | 2 ++ src/otx/core/ov/ops/type_conversions.py | 1 + tests/unit/core/ov/graph/test_ov_graph_utils.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/otx/core/ov/ops/infrastructures.py b/src/otx/core/ov/ops/infrastructures.py index 44b39b9d120..f0c9509d417 100644 --- a/src/otx/core/ov/ops/infrastructures.py +++ b/src/otx/core/ov/ops/infrastructures.py @@ -233,6 +233,8 @@ def from_ov(cls, ov_op): 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) diff --git a/src/otx/core/ov/ops/type_conversions.py b/src/otx/core/ov/ops/type_conversions.py index 25454053c22..267ae7ea37d 100644 --- a/src/otx/core/ov/ops/type_conversions.py +++ b/src/otx/core/ov/ops/type_conversions.py @@ -25,6 +25,7 @@ "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 diff --git a/tests/unit/core/ov/graph/test_ov_graph_utils.py b/tests/unit/core/ov/graph/test_ov_graph_utils.py index 7133f523da4..9e3a865dfc4 100644 --- a/tests/unit/core/ov/graph/test_ov_graph_utils.py +++ b/tests/unit/core/ov/graph/test_ov_graph_utils.py @@ -2,6 +2,7 @@ # 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, @@ -38,6 +39,7 @@ def test_handle_merging_into_batchnorm(): @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) From 89b62d72d891a44165e5b648118f1346f78257ce Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Tue, 12 Dec 2023 14:47:03 +0900 Subject: [PATCH 143/146] Skip failure TC with adding issue number ref. (#2717) --- tests/e2e/cli/detection/test_detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/cli/detection/test_detection.py b/tests/e2e/cli/detection/test_detection.py index dd86fcf46d2..cc2efcd0b89 100644 --- a/tests/e2e/cli/detection/test_detection.py +++ b/tests/e2e/cli/detection/test_detection.py @@ -348,6 +348,8 @@ def test_otx_eval(self, template, tmp_dir_path): 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" From 339700608ceed8e7dffd50ad0f1554ebc5e4031a Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Tue, 12 Dec 2023 17:19:09 +0900 Subject: [PATCH 144/146] Fix visual prompting e2e test (#2719) Skip zero-shot e2e --- tests/e2e/cli/visual_prompting/test_visual_prompting.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/e2e/cli/visual_prompting/test_visual_prompting.py b/tests/e2e/cli/visual_prompting/test_visual_prompting.py index 43fa07a9f1b..2749a09f347 100644 --- a/tests/e2e/cli/visual_prompting/test_visual_prompting.py +++ b/tests/e2e/cli/visual_prompting/test_visual_prompting.py @@ -60,11 +60,13 @@ templates_ids = [template.model_template_id + f"-{i+1}" for i, template in enumerate(templates)] else: - templates = ( - Registry("src/otx/algorithms/visual_prompting", experimental=True) + templates = [ + template + for template in Registry("src/otx/algorithms/visual_prompting", experimental=True) .filter(task_type="VISUAL_PROMPTING") .templates - ) + if "Zero_Shot" not in template.name + ] templates_ids = [template.model_template_id for template in templates] From e88bde14e0d473875ea2e62dd683c9820afcbc8e Mon Sep 17 00:00:00 2001 From: Eunwoo Shin Date: Wed, 13 Dec 2023 10:10:08 +0900 Subject: [PATCH 145/146] Remove duplicated variable combination in experiment.py (#2713) --- tools/experiment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/experiment.py b/tools/experiment.py index abae60a9f40..891c4e75aaa 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -567,9 +567,9 @@ def _product_all_cases( ) -> List[Dict[str, str]]: if isinstance(target_str, str): target_str = [target_str] - found_keys = [] + found_keys = set() for each_str in target_str: - found_keys.extend([x for x in set(self._replace_pat.findall(each_str)) if x in variable]) + found_keys.update([x for x in set(self._replace_pat.findall(each_str)) if x in variable]) if not found_keys: return [] From 41fdefa7926d2eb4ce2453a5a3a10b910af351eb Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Thu, 14 Dec 2023 10:03:39 +0900 Subject: [PATCH 146/146] Enhance detection & instance segmentation experiment (#2710) * Compute precision and recall along with f-measure * Log performance * Accept ellipse annotation from datumaro format * Fix dataset adapter condition for det/iset * Insert garbage collection btw experiments --- .../detection/adapters/openvino/task.py | 1 + src/otx/algorithms/detection/task.py | 2 +- src/otx/api/usecases/evaluation/f_measure.py | 19 ++++++++++++++----- src/otx/cli/tools/eval.py | 4 +++- .../core/data/adapter/base_dataset_adapter.py | 16 ++++++++++++++++ .../data/adapter/detection_dataset_adapter.py | 15 ++++++++------- tools/experiment.py | 3 +++ 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/otx/algorithms/detection/adapters/openvino/task.py b/src/otx/algorithms/detection/adapters/openvino/task.py index eedf97f8b48..c91d14e6430 100644 --- a/src/otx/algorithms/detection/adapters/openvino/task.py +++ b/src/otx/algorithms/detection/adapters/openvino/task.py @@ -619,6 +619,7 @@ def evaluate( 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: diff --git a/src/otx/algorithms/detection/task.py b/src/otx/algorithms/detection/task.py index 84ec50697f9..78af633e2a1 100644 --- a/src/otx/algorithms/detection/task.py +++ b/src/otx/algorithms/detection/task.py @@ -446,8 +446,8 @@ def evaluate( f"Requested to use {evaluation_metric} metric, " "but parameter is ignored. Use F-measure instead." ) metric = MetricsHelper.compute_f_measure(output_resultset) - logger.info(f"F-measure after evaluation: {metric.f_measure.value}") 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( diff --git a/src/otx/api/usecases/evaluation/f_measure.py b/src/otx/api/usecases/evaluation/f_measure.py index cc845ef7609..4837fcf193a 100644 --- a/src/otx/api/usecases/evaluation/f_measure.py +++ b/src/otx/api/usecases/evaluation/f_measure.py @@ -19,7 +19,7 @@ LineChartInfo, LineMetricsGroup, MetricsGroup, - Performance, + MultiScorePerformance, ScoreMetric, TextChartInfo, TextMetricsGroup, @@ -205,6 +205,7 @@ class _AggregatedResults: - all_classes_f_measure_curve - best_f_measure - best_threshold + - best_f_measure_metrics Args: classes (List[str]): List of classes. @@ -217,6 +218,7 @@ def __init__(self, classes: List[str]): 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: @@ -364,6 +366,7 @@ def get_results_per_confidence( 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( @@ -418,6 +421,7 @@ def get_results_per_nms( 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( @@ -693,6 +697,8 @@ def __init__( 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 @@ -752,13 +758,12 @@ 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) -> Performance: + def get_performance(self) -> MultiScorePerformance: """Returns the performance which consists of the F-Measure score and the dashboard metrics. Returns: - Performance: Performance object containing the F-Measure score and the dashboard metrics. + MultiScorePerformance: MultiScorePerformance object containing the F-Measure scores and the dashboard metrics. """ - score = self.f_measure dashboard_metrics: List[MetricsGroup] = [] dashboard_metrics.append( BarMetricsGroup( @@ -813,7 +818,11 @@ def get_performance(self) -> Performance: ), ) ) - return Performance(score=score, dashboard_metrics=dashboard_metrics) + 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( diff --git a/src/otx/cli/tools/eval.py b/src/otx/cli/tools/eval.py index f210ba4a395..f609f5d3ded 100644 --- a/src/otx/cli/tools/eval.py +++ b/src/otx/cli/tools/eval.py @@ -153,10 +153,12 @@ def main(): ) task.evaluate(resultset) assert resultset.performance is not None - print(resultset.performance) 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: diff --git a/src/otx/core/data/adapter/base_dataset_adapter.py b/src/otx/core/data/adapter/base_dataset_adapter.py index 51b62a0cede..52195de4d0f 100644 --- a/src/otx/core/data/adapter/base_dataset_adapter.py +++ b/src/otx/core/data/adapter/base_dataset_adapter.py @@ -39,6 +39,7 @@ 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 @@ -350,6 +351,21 @@ def _get_polygon_entity( 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) diff --git a/src/otx/core/data/adapter/detection_dataset_adapter.py b/src/otx/core/data/adapter/detection_dataset_adapter.py index a6ce1b2bce5..612ab303f23 100644 --- a/src/otx/core/data/adapter/detection_dataset_adapter.py +++ b/src/otx/core/data/adapter/detection_dataset_adapter.py @@ -37,14 +37,15 @@ def get_otx_dataset(self) -> DatasetEntity: 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): + if self.task_type in (TaskType.INSTANCE_SEGMENTATION, TaskType.ROTATED_DETECTION): + if ann.type == DatumAnnotationType.polygon and self._is_normal_polygon(ann): shapes.append(self._get_polygon_entity(ann, image.width, image.height)) - if self.task_type is TaskType.DETECTION and ann.type == DatumAnnotationType.bbox: - if self._is_normal_bbox(ann.points[0], ann.points[1], ann.points[2], ann.points[3]): + 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: diff --git a/tools/experiment.py b/tools/experiment.py index 891c4e75aaa..6d9a271e547 100644 --- a/tools/experiment.py +++ b/tools/experiment.py @@ -6,6 +6,7 @@ import argparse import csv import dataclasses +import gc import json import os import re @@ -701,6 +702,8 @@ def run_command_list(self, dryrun: bool = False): self._previous_cmd_entry.append(command[1]) + gc.collect() + if not dryrun: organize_exp_result(self._workspace, self._command_var)

    sO=vjQAH0-59*mmcmz_Rn4+&*G%Epx4p}&<6=Fbb$@fRZ{B5g6+?NSS%);-UA zkj*-ky4Tr_hxrng@Sgi_geD*F1w!(i#K7Ak>EH-or#qYN6)M!k$jhlB3CvOeR^oBH zFPVKSD|5d|>l zirgJa)5Fjjx35^M7fBZR6rfFjJdD{hq5wNMXA01DmsgC6fYtu^FWRPYEwh z8nBol6r6Mpz7!y^E_)6$||yEb_@+R1}X8ermSiPlVGNN19~!;S?Y- zcI(?o69ri18~@^j-+^{u2zAsescl#)8TrBKB#>&SJo(M@k}rsv+kWq}8NBT$)Z9uA zFNAD6xpFux)%jR=nCi|dD{*j{e2|k|G=4$>sv%Fw@&RcY@X9K&oxy!Iwy0fxIX|S# zxU+^`W~2C2l(WMh7}oAuE4e|3e!3px($6#dYsbc0o5Q5lwwpZZ9?-GXa(#6tk=I8% zxT=F`gsW1Odh9~>M#W2Fpae;ND|&tmHzgsQXh{K5xIF2npO2@qXV44c$9JOSXv~9K z=55*5KVK%4HyX~nnOXH6_XPK)zlfI#1U-5W05owwUgnDxEH+e>G*cG6>mF<$WbFj@ zkd9G=;+m rm=?!uE~kAJ^4_yK2D7kmHfmL?LP}$;!(zmXrT)q5?*GRe%H+QQNF}ON 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 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 J(1(WK{vB_vz?c94 literal 0 HcmV?d00001 diff --git a/tests/assets/datumaro_h-label/images/validation/d.jpg b/tests/assets/datumaro_h-label/images/validation/d.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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/train/a.jpg b/tests/assets/datumaro_h-label_class_decremental/images/train/a.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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_h-label_class_decremental/images/validation/d.jpg b/tests/assets/datumaro_h-label_class_decremental/images/validation/d.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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 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/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/train/a.jpg b/tests/assets/datumaro_multilabel/images/train/a.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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/images/validation/d.jpg b/tests/assets/datumaro_multilabel/images/validation/d.jpg deleted file mode 100644 index 222682d80bf9740d8eb672035ae34a240f949592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 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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

  3. UzYk2Jbz@~TQmqdzCy7X?9K=y~GPtnQVZHX+*S~yq=%7`|)npzWac>Y)rDkNK zC8CpA=1e~sl#pzx`R-2Mm`9XoyE;b=aAywi(G%raaWz8!0Poc-9(A|!pWwfswmIdH zqD9rY#L9kUvj%RsRXa7Ki+Guo)?|E7QJYdprV8XrfQoEOc~ytsVt%I-jyBF*sMzpd z7jT~r(ZffXW^fkH`ArHdY?A-$1Z#mbsB13H6y6Wja!@RH!kM#m4s)5s~r4KF&yeFhDXx}Uk2_KvV%0AK)M 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%0DqkJ91HjtSsM8hMh~;eKv@a zs9WiyGa2Di$;PkIz64G7sCY-_!_0)ZVV+hFpJ3Zs&cBUbzV>l)-Zm)A;u5k0e%A}J zqtEE=UoTU%$eUAVt|alBRu|a3#Vwwv@P&+MU3h*Kx1(3au^_zRtCib%SYIk?Qyw1G#Iv}@7IoU1uRM6SD!|nb&D!}!pgRWcXsJw0orW!R6nV*?Z3L8 zuqI^8mtpMb-J>PaE=ktX?;Jj0`H=M)4mDqV_6eh%3045NvXuNz@Wp)d`7B{ApDBOkI$$F^;nnj&mEN1DYJpf_<)| z<{h8VR5du^V0PQjTeMix;zoxG96RYeQ-0yhs15E11cr0L=62q+2rgUC^;SfcVAXxV zIm&VRN3z;$-#CmopLGSu%9Kjwpt7-#bU)LPGT4Hul0#f0^t|)+0A+mWX7F9I24-T~ z)tV$eg3_z!4h1jQ(ynQ>cY2^0l|p&m!=>d|G>2MB7`Fk(rg;b1rMQFK-)FFD`h~0? z2JjnxFAGf>+NkwSIzlbv?A$>uQRV%T>`yP-jnj|lkk@S3ved6(MfL!b%0DHc@BA|*To%1Wf{hB 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¸AG#Tw(7R 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%0A30k5_MwUh_d)s@G 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%0AZF^ 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 literal 0 HcmV?d00001 From 91ad75195883da75938566177a36eb0dfe6fc41f Mon Sep 17 00:00:00 2001 From: Yunchu Lee Date: Wed, 12 Jul 2023 12:34:14 +0900 Subject: [PATCH 012/146] Bump otx version to 1.4.0rc2 (#2341) --- src/otx/__init__.py | 2 +- src/otx/api/usecases/exportable_code/demo/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otx/__init__.py b/src/otx/__init__.py index 884f6b9a96d..0656f85ae5c 100644 --- a/src/otx/__init__.py +++ b/src/otx/__init__.py @@ -3,5 +3,5 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.4.0rc1" +__version__ = "1.4.0rc2" # NOTE: Sync w/ src/otx/api/usecases/exportable_code/demo/requirements.txt on release diff --git a/src/otx/api/usecases/exportable_code/demo/requirements.txt b/src/otx/api/usecases/exportable_code/demo/requirements.txt index 891041200cb..3496770b4dc 100644 --- a/src/otx/api/usecases/exportable_code/demo/requirements.txt +++ b/src/otx/api/usecases/exportable_code/demo/requirements.txt @@ -1,4 +1,4 @@ openvino==2023.0 openvino-model-api==0.1.2 -otx==1.4.0rc1 +otx==1.4.0rc2 numpy>=1.21.0,<=1.23.5 # np.bool was removed in 1.24.0 which was used in openvino runtime From 85bf95e9f01c67cd7e1033d27b1d27796a88d714 Mon Sep 17 00:00:00 2001 From: "Kim, Sungchul" Date: Wed, 12 Jul 2023 16:09:16 +0900 Subject: [PATCH 013/146] OTX deploy for visual prompting task (#2311) * Enable `otx deploy` * (WIP) integration test * Docstring * Update args for create_model * Manually set image embedding layout * Enable to use model api for preprocessing - `fit_to_window` doesn't work expectedly, so newly implemented `VisualPromptingOpenvinoAdapter` to use new resize function * Remove skipped test * Updated * Update unit tests on model wrappers * Update * Update configuration * Fix not to patch pretrained path * pylint & update model api version in docstring --------- Co-authored-by: Wonju Lee --- CHANGELOG.md | 4 +- .../adapters/openvino/__init__.py | 2 + .../openvino/model_wrappers/__init__.py | 18 ++ .../model_wrappers/openvino_adapters.py | 164 ++++++++++++++++++ .../openvino_models.py} | 123 ++++++++----- .../visual_prompters/segment_anything.py | 13 +- .../configs/base/configuration.py | 35 +++- .../configs/sam_vit_b/config.yaml | 2 +- .../visual_prompting/tasks/inference.py | 9 +- .../visual_prompting/tasks/openvino.py | 115 +++++++++--- .../visual_prompting/test_visual_prompting.py | 17 +- .../openvino/model_wrappers/__init__.py | 3 + .../model_wrappers/test_openvino_models.py | 151 ++++++++++++++++ .../adapters/openvino/test_model_wrappers.py | 122 ------------- .../visual_prompters/test_segment_anything.py | 66 +++---- .../visual_prompting/tasks/test_openvino.py | 80 ++++++--- 16 files changed, 660 insertions(+), 264 deletions(-) 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_adapters.py rename src/otx/algorithms/visual_prompting/adapters/openvino/{model_wrappers.py => model_wrappers/openvino_models.py} (53%) 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 delete mode 100644 tests/unit/algorithms/visual_prompting/adapters/openvino/test_model_wrappers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e0c9cec0c..fcb7d47864d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ All notable changes to this project will be documented in this file. - Add per-class XAI saliency maps for Mask R-CNN model (https://github.com/openvinotoolkit/training_extensions/pull/2227) - Add new object detector Deformable DETR () - Add new object detector DINO() -- Add new visual prompting task (https://github.com/openvinotoolkit/training_extensions/pull/2203), (https://github.com/openvinotoolkit/training_extensions/pull/2274) +- Add new visual prompting task: train/eval (https://github.com/openvinotoolkit/training_extensions/pull/2203) +- Add new visual prompting task: export (https://github.com/openvinotoolkit/training_extensions/pull/2274) +- Add new visual prompting task: deploy (https://github.com/openvinotoolkit/training_extensions/pull/2311) - Add new object detector ResNeXt101-ATSS () ### Enhancements diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py b/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py index 8625727116c..d56d86cdd80 100644 --- a/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/__init__.py @@ -13,3 +13,5 @@ # WITHOUT 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..d251a0ed64a --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/__init__.py @@ -0,0 +1,18 @@ +"""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_adapters import VisualPromptingOpenvinoAdapter # noqa: F401 +from .openvino_models import Decoder, ImageEncoder # noqa: F401 diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py new file mode 100644 index 00000000000..6f0a9e402b4 --- /dev/null +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_adapters.py @@ -0,0 +1,164 @@ +"""Openvino Adapter Wrappers of OTX Visual Prompting. + +There is a bug on fit_to_window resize module in model API. +VisualPromptingOpenvinoAdapter is temporarily implemented to use updated `fit_to_window` resize function. +When model API version in otx is upgraded, it can be removed. + +Issue: https://github.com/openvinotoolkit/model_api/issues/99 +Updated PR: https://github.com/openvinotoolkit/model_api/pull/100 +""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from functools import partial +from typing import Tuple + +import numpy as np +import openvino.runtime as ov +from openvino.model_api.adapters import OpenvinoAdapter +from openvino.preprocess import ColorFormat, PrePostProcessor +from openvino.runtime import Output, Type +from openvino.runtime import opset10 as opset +from openvino.runtime.utils.decorators import custom_preprocess_function + + +def resize_image_with_aspect_pad(input: Output, size, keep_aspect_ratio, interpolation, pad_value): + """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/utils.py#L273-L341.""" + h_axis = 1 + w_axis = 2 + w, h = size + + target_size = list(size) + target_size.reverse() + + image_shape = opset.shape_of(input, name="shape") + iw = opset.convert( + opset.gather(image_shape, opset.constant(w_axis), axis=0), + destination_type="f32", + ) + ih = opset.convert( + opset.gather(image_shape, opset.constant(h_axis), axis=0), + destination_type="f32", + ) + w_ratio = opset.divide(np.float32(w), iw) + h_ratio = opset.divide(np.float32(h), ih) + scale = opset.minimum(w_ratio, h_ratio) + nw = opset.convert(opset.round(opset.multiply(iw, scale), "half_to_even"), destination_type="i32") + nh = opset.convert(opset.round(opset.multiply(ih, scale), "half_to_even"), destination_type="i32") + new_size = opset.concat([opset.unsqueeze(nh, 0), opset.unsqueeze(nw, 0)], axis=0) + image = opset.interpolate( + input, + new_size, + scales=np.array([0.0, 0.0], dtype=np.float32), + axes=[h_axis, w_axis], + mode=interpolation, + shape_calculation_mode="sizes", + ) + + dx_border = opset.subtract(opset.constant(w, dtype=np.int32), nw) + dy_border = opset.subtract(opset.constant(h, dtype=np.int32), nh) + pads_begin = np.array([0, 0, 0, 0], np.int32) + pads_end = opset.concat( + [ + opset.constant([0], dtype=np.int32), + opset.unsqueeze(dy_border, 0), + opset.unsqueeze(dx_border, 0), + opset.constant([0], dtype=np.int32), + ], + axis=0, + ) + return opset.pad( + image, + pads_begin, + pads_end, + "constant", + opset.constant(pad_value, dtype=np.uint8), + ) + + +def resize_image_with_aspect(size, interpolation, pad_value): + """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/utils.py#L356-L365.""" + return custom_preprocess_function( + partial( + resize_image_with_aspect_pad, + size=size, + keep_aspect_ratio=True, + interpolation=interpolation, + pad_value=pad_value, + ) + ) + + +class VisualPromptingOpenvinoAdapter(OpenvinoAdapter): + """Openvino Adapter Wrappers of OTX Visual Prompting. + + This class is to use fixed `fit_to_window` resize module. + When model API version in otx is upgraded, it can be removed. + """ + + def embed_preprocessing( + self, + layout, + resize_mode: str, + interpolation_mode, + target_shape: Tuple[int], + pad_value, + dtype=type(int), + brg2rgb=False, + mean=None, + scale=None, + input_idx=0, + ): + """https://github.com/openvinotoolkit/model_api/blob/0.1.3/model_api/python/openvino/model_api/adapters/openvino_adapter.py#L340-L411.""" + ppp = PrePostProcessor(self.model) # type: ignore[has-type] + + # Change the input type to the 8-bit image + if dtype == type(int): + ppp.input(input_idx).tensor().set_element_type(Type.u8) + + ppp.input(input_idx).tensor().set_layout(ov.Layout("NHWC")).set_color_format(ColorFormat.BGR) + + INTERPOLATION_MODE_MAP = { + "LINEAR": "linear", + "CUBIC": "cubic", + "NEAREST": "nearest", + } + + RESIZE_MODE_MAP = {"fit_to_window": resize_image_with_aspect} + + # Handle resize + # Change to dynamic shape to handle various image size + # TODO: check the number of input channels and rank of input shape + if resize_mode and target_shape: + if resize_mode in RESIZE_MODE_MAP: + input_shape = [1, -1, -1, 3] + ppp.input(input_idx).tensor().set_shape(input_shape) + ppp.input(input_idx).preprocess().custom( + RESIZE_MODE_MAP[resize_mode]( + target_shape, + INTERPOLATION_MODE_MAP[interpolation_mode], + pad_value, + ) + ) + + else: + raise ValueError(f"Upsupported resize type in model preprocessing: {resize_mode}") + + # Handle layout + ppp.input(input_idx).model().set_layout(ov.Layout(layout)) + + # Handle color format + if brg2rgb: + ppp.input(input_idx).preprocess().convert_color(ColorFormat.RGB) + + ppp.input(input_idx).preprocess().convert_element_type(Type.f32) + + if mean: + ppp.input(input_idx).preprocess().mean(mean) + if scale: + ppp.input(input_idx).preprocess().scale(scale) + + self.model = ppp.build() + self.load_model() diff --git a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers.py b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py similarity index 53% rename from src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers.py rename to src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py index 18d55a1c268..83f327a7eca 100644 --- a/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers.py +++ b/src/otx/algorithms/visual_prompting/adapters/openvino/model_wrappers/openvino_models.py @@ -1,4 +1,4 @@ -"""Model Wrapper of OTX Visual Prompting.""" +"""Openvino Model Wrappers of OTX Visual Prompting.""" # Copyright (C) 2023 Intel Corporation # @@ -14,16 +14,15 @@ # See the License for the specific language governing permissions # and limitations under the License. -from typing import Any, Dict, Tuple +from copy import deepcopy +from typing import Any, Dict, List, Optional, Tuple, Union import cv2 import numpy as np -from openvino.model_api.models import ImageModel -from openvino.model_api.models.types import NumericalValue +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.segmentation.adapters.openvino.model_wrappers.blur import ( - BlurSegmentation, -) from otx.api.utils.segmentation_utils import create_hard_prediction_from_soft_prediction @@ -32,36 +31,39 @@ class ImageEncoder(ImageModel): __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["resize_type"].default_value = "fit_to_window" - parameters["mean_values"].default_value = [123.675, 116.28, 103.53] - parameters["scale_values"].default_value = [58.395, 57.12, 57.375] + parameters.update( + { + "resize_type": StringValue(default_value="fit_to_window"), + } + ) 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 Decoder(BlurSegmentation): - """Decoder class for visual prompting of openvino model wrapper. - TODO (sungchul): change parent class - """ +class Decoder(SegmentationModel): + """Decoder class for visual prompting of openvino model wrapper.""" __model__ = "decoder" - def preprocess(self, bbox: np.ndarray, original_size: Tuple[int]) -> Dict[str, Any]: - """Ready decoder inputs.""" - point_coords = bbox.reshape((-1, 2, 2)) - point_labels = np.array([2, 3], dtype=np.float32).reshape((-1, 2)) - inputs_decoder = { - "point_coords": point_coords, - "point_labels": point_labels, - # TODO (sungchul): how to generate mask_input and has_mask_input - "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(original_size, dtype=np.float32).reshape((-1, 2)), - } - return inputs_decoder + def __init__( + self, + model_adapter: InferenceAdapter, + configuration: Optional[dict] = None, + preload: bool = False, + ): + super().__init__(model_adapter, configuration, preload) + self.output_blob_name = "low_res_masks" @classmethod def parameters(cls): # noqa: D102 @@ -69,26 +71,53 @@ def parameters(cls): # noqa: D102 parameters.update({"image_size": NumericalValue(value_type=int, default_value=1024, min=0, max=2048)}) return parameters + def preprocess(self, inputs: Dict[str, Any], meta: Dict[str, Any]): + """Preprocess prompts.""" + processed_prompts = [] + # TODO (sungchul): process points + for bbox, label in zip(inputs["bboxes"], inputs["labels"]): + # TODO (sungchul): add condition to check whether using bbox or point + point_coords = self._apply_coords(bbox.reshape(-1, 2, 2), inputs["original_size"]) + point_labels = np.array([2, 3], dtype=np.float32).reshape((-1, 2)) + processed_prompts.append( + { + "point_coords": point_coords, + "point_labels": point_labels, + # TODO (sungchul): how to generate mask_input and has_mask_input + "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(inputs["original_size"], dtype=np.float32).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 _get_outputs(self): - """Get output layer name and shape.""" - layer_name = "low_res_masks" - layer_shape = self.outputs[layer_name].shape - - if len(layer_shape) == 3: - self.out_channels = 0 - elif len(layer_shape) == 4: - self.out_channels = layer_shape[1] - else: - raise Exception(f"Unexpected output layer shape {layer_shape}. Only 4D and 3D output layers are supported") - - return layer_name - 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. @@ -102,10 +131,10 @@ def postprocess(self, outputs: Dict[str, np.ndarray], meta: Dict[str, Any]) -> T """ def sigmoid(x): - return 1 / (1 + np.exp(-x)) + return np.tanh(x * 0.5) * 0.5 + 0.5 # to avoid overflow soft_prediction = outputs[self.output_blob_name].squeeze() - soft_prediction = self.resize_and_crop(soft_prediction, meta["original_size"]) + soft_prediction = self.resize_and_crop(soft_prediction, meta["original_size"][0]) soft_prediction = sigmoid(soft_prediction) meta["soft_prediction"] = soft_prediction @@ -134,18 +163,18 @@ def resize_and_crop(self, soft_prediction: np.ndarray, original_size: np.ndarray soft_prediction, (self.image_size, self.image_size), 0, 0, interpolation=cv2.INTER_LINEAR ) - prepadded_size = self.resize_longest_image_size(original_size, self.image_size).astype(np.int64) + prepadded_size = self.get_padded_size(original_size, self.image_size).astype(np.int64) resized_cropped_soft_prediction = resized_soft_prediction[..., : prepadded_size[0], : prepadded_size[1]] original_size = original_size.astype(np.int64) - h, w = original_size[0], original_size[1] + h, w = original_size final_soft_prediction = cv2.resize( resized_cropped_soft_prediction, (w, h), 0, 0, interpolation=cv2.INTER_LINEAR ) return final_soft_prediction - def resize_longest_image_size(self, original_size: np.ndarray, longest_side: int) -> np.ndarray: - """Resizes the longest side of the image to the given size. + def get_padded_size(self, original_size: np.ndarray, longest_side: int) -> np.ndarray: + """Get padded size from original size and longest side of the image. Args: original_size (np.ndarray): The original image size with shape Bx2. 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 index 93e34f6b630..3dbe568091f 100644 --- 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 @@ -159,19 +159,16 @@ def replace_state_dict_keys(state_dict, revise_keys): # state_dict from args.load_from state_dict = replace_state_dict_keys(state_dict, revise_keys) self.load_state_dict(state_dict) - elif self.config.model.checkpoint == "pretrained": - # use pretrained weights - state_dict = torch.hub.load_state_dict_from_url(CKPT_PATHS[self.config.model.backbone]) - state_dict = replace_state_dict_keys(state_dict, revise_keys) - self.load_state_dict(state_dict) elif self.config.model.checkpoint: - try: - # try to load lightning checkpoint + if str(self.config.model.checkpoint).endswith(".ckpt"): + # load lightning checkpoint self.load_from_checkpoint(self.config.model.checkpoint) - except Exception: + 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) state_dict = replace_state_dict_keys(state_dict, revise_keys) diff --git a/src/otx/algorithms/visual_prompting/configs/base/configuration.py b/src/otx/algorithms/visual_prompting/configs/base/configuration.py index 7704c1be131..eeb174c4875 100644 --- a/src/otx/algorithms/visual_prompting/configs/base/configuration.py +++ b/src/otx/algorithms/visual_prompting/configs/base/configuration.py @@ -19,7 +19,9 @@ from otx.algorithms.common.configs import BaseConfig from otx.api.configuration.elements import ( + ParameterGroup, add_parameter_group, + configurable_boolean, configurable_float, configurable_integer, string_attribute, @@ -40,10 +42,19 @@ class __LearningParameters(BaseConfig.BaseLearningParameters): description = header @attrs - class __Postprocessing(BaseConfig.BasePostprocessing): + 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.", @@ -52,6 +63,7 @@ class __Postprocessing(BaseConfig.BasePostprocessing): max_value=25, affects_outcome_of=ModelLifecycle.INFERENCE, ) + soft_threshold = configurable_float( default_value=0.5, header="Soft threshold", @@ -62,5 +74,26 @@ class __Postprocessing(BaseConfig.BasePostprocessing): 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_float( + header="Original width", + description="Model input width before embedding processing.", + default_value=64.0, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + + orig_height = configurable_float( + header="Original height", + description="Model input height before embedding processing.", + default_value=64.0, + affects_outcome_of=ModelLifecycle.INFERENCE, + ) + learning_parameters = add_parameter_group(__LearningParameters) postprocessing = add_parameter_group(__Postprocessing) 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 index 04aef5943e7..393cfa468a2 100644 --- a/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml +++ b/src/otx/algorithms/visual_prompting/configs/sam_vit_b/config.yaml @@ -26,7 +26,7 @@ model: freeze_image_encoder: true freeze_prompt_encoder: true freeze_mask_decoder: false - checkpoint: pretrained + checkpoint: https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth # just for inference return_single_mask: true use_stability_score: false diff --git a/src/otx/algorithms/visual_prompting/tasks/inference.py b/src/otx/algorithms/visual_prompting/tasks/inference.py index 18500200bb0..6c93a05caa9 100644 --- a/src/otx/algorithms/visual_prompting/tasks/inference.py +++ b/src/otx/algorithms/visual_prompting/tasks/inference.py @@ -69,7 +69,7 @@ class InferenceTask(IInferenceTask, IEvaluationTask, IExportTask, IUnload): """Base Visual Prompting Task. - Train, Infer, Export, Optimize and Deploy an Visual Prompting Task. + Train, Infer, and Export an Visual Prompting Task. Args: task_environment (TaskEnvironment): OTX Task environment. @@ -367,6 +367,13 @@ def export( # noqa: D102 "--model_name", module, ] + if module == "visual_prompting_image_encoder": + optimize_command += [ + "--mean_values", + str(self.config.dataset.normalize.mean).replace(", ", ","), + "--scale_values", + str(self.config.dataset.normalize.std).replace(", ", ","), + ] if precision == ModelPrecision.FP16: optimize_command.append("--compress_to_fp16") subprocess.run(optimize_command, check=True) diff --git a/src/otx/algorithms/visual_prompting/tasks/openvino.py b/src/otx/algorithms/visual_prompting/tasks/openvino.py index be97910a400..e2d24c9d14a 100644 --- a/src/otx/algorithms/visual_prompting/tasks/openvino.py +++ b/src/otx/algorithms/visual_prompting/tasks/openvino.py @@ -14,19 +14,25 @@ # See the License for the specific language governing permissions # and limitations under the License. +import io +import json import os import time from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union +from zipfile import ZipFile import attr import numpy as np -from openvino.model_api.adapters import OpenvinoAdapter, create_core +from openvino.model_api.adapters import create_core from openvino.model_api.models import Model -import otx.algorithms.visual_prompting.adapters.openvino.model_wrappers # noqa: F401 from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.utils import get_default_async_reqs_num +from otx.algorithms.visual_prompting.adapters.openvino import model_wrappers +from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( + VisualPromptingOpenvinoAdapter, +) from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import ( OTXVisualPromptingDataset, get_transform, @@ -45,7 +51,9 @@ from otx.api.entities.optimization_parameters import OptimizationParameters from otx.api.entities.resultset import ResultSetEntity from otx.api.entities.task_environment import TaskEnvironment +from otx.api.serialization.label_mapper import LabelSchemaMapper 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 BaseInferencer from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( VisualPromptingToAnnotationConverter, @@ -91,33 +99,38 @@ def __init__( assert all(module in model_files for module in ["image_encoder", "decoder"]) self.model = {} - for name in ["image_encoder", "decoder"]: - model_adapter = OpenvinoAdapter( - create_core(), - model_files.get(name), - weight_files.get(name, None), - device=device, - max_num_requests=num_requests, - plugin_config={"PERFORMANCE_HINT": "THROUGHPUT"}, - ) - self.configuration = { + model_parameters = {"decoder": {"input_layouts": "image_embeddings:NCHW"}} + self.configuration = { + "decoder": { **attr.asdict( hparams.postprocessing, filter=lambda attr, value: attr.name not in ["header", "description", "type", "visible_in_ui", "class_name"], ) } - self.model[name] = Model.create_model(name, model_adapter, self.configuration, preload=True) + } + for name in ["image_encoder", "decoder"]: + model_adapter = VisualPromptingOpenvinoAdapter( + 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) -> Dict[str, Any]: # type: ignore """Pre-process function of OpenVINO Visual Prompting Inferencer for image encoder.""" - # TODO (sungchul): change to modelapi. - prompts = OTXVisualPromptingDataset.get_prompts(dataset_item, self.labels) - items = {"index": 0, "images": dataset_item.numpy, **prompts} - return self.transform(items) + images, meta = self.model["image_encoder"].preprocess(dataset_item.numpy) + prompts = OTXVisualPromptingDataset.get_prompts(dataset_item, self.labels) # to be replaced + prompts = self.model["decoder"].preprocess(prompts, meta) + items = {**images, **meta, "prompts": prompts} + return items def post_process( self, prediction: Dict[str, np.ndarray], metadata: Dict[str, Any] @@ -131,19 +144,18 @@ def predict(self, dataset_item: DatasetItemEntity) -> List[Annotation]: # type: """Perform a prediction for a given input image.""" # forward image encoder items = self.pre_process(dataset_item) - image_embeddings = self.forward({"images": items["images"].unsqueeze(0).numpy()}) + image_embeddings = self.forward({"images": items["images"]}) - # TODO (sungchul): generate random points from gt_mask annotations: List[Annotation] = [] hard_predictions: List[np.ndarray] = [] soft_predictions: List[np.ndarray] = [] - for idx, (bbox, label) in enumerate(zip(items["bboxes"], items["labels"])): - inputs_decoder = self.model["decoder"].preprocess(bbox, items["original_size"]) - inputs_decoder.update(image_embeddings) + for prompt in items["prompts"]: + label = prompt.pop("label") + prompt.update(image_embeddings) # forward decoder to get predicted mask - prediction = self.forward_decoder(inputs_decoder) - metadata = {"label": label, "original_size": np.array(items["original_size"])} + prediction = self.forward_decoder(prompt) + metadata = {"label": label, "original_size": prompt["orig_size"]} # set annotation for eval annotation, hard_prediction, soft_prediction = self.post_process(prediction, metadata) @@ -260,7 +272,60 @@ def evaluate(self, output_resultset: ResultSetEntity, evaluation_metric: Optiona def deploy(self, output_model: ModelEntity) -> None: """Deploy function of OpenVINOVisualPromptingTask.""" - raise NotImplementedError + 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 = {} + parameters["converter_type"] = f"{self.task_type}" + parameters["model_parameters"] = self.inferencer.configuration # type: ignore + parameters["model_parameters"]["labels"] = LabelSchemaMapper.forward(self.task_environment.label_schema) # type: ignore # noqa: E501 + + 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, diff --git a/tests/integration/cli/visual_prompting/test_visual_prompting.py b/tests/integration/cli/visual_prompting/test_visual_prompting.py index 8fb35794b62..1ce57770cb0 100644 --- a/tests/integration/cli/visual_prompting/test_visual_prompting.py +++ b/tests/integration/cli/visual_prompting/test_visual_prompting.py @@ -13,6 +13,8 @@ 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, @@ -99,9 +101,22 @@ def test_otx_export_onnx(self, template, tmp_dir_path): otx_export_testing(template, tmp_dir_path, half_precision=False, is_onnx=True) @e2e_pytest_component - @pytest.mark.skip("arguments order of create_model and decoder's input layout will be updated.") @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) + + @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/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..efdf2c0b495 --- /dev/null +++ b/tests/unit/algorithms/visual_prompting/adapters/openvino/model_wrappers/test_openvino_models.py @@ -0,0 +1,151 @@ +"""Tests model wrappers for openvino task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +from typing import Tuple + +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 BooleanValue, NumericalValue + +from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( + Decoder, + ImageEncoder, + VisualPromptingOpenvinoAdapter, +) +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=VisualPromptingOpenvinoAdapter) + 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_preprocess(self): + """Test preprocess""" + prompts = {"bboxes": [np.array([[1, 1], [2, 2]])], "labels": [1], "original_size": (4, 4)} + + results = self.decoder.preprocess(prompts, {}) + + assert isinstance(results, list) + assert "point_coords" in results[0] + assert results[0]["point_coords"].shape == (1, 2, 2) + assert "point_labels" in results[0] + assert results[0]["point_labels"].shape == (1, 2) + 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 = "masks" + self.decoder.soft_threshold = 0.5 + self.decoder.blur_strength = 2 + fake_output = {"masks": np.ones((4, 4)), "iou_predictions": 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) + assert np.all(returned_value[0].shape == fake_metadata["original_size"]) + assert np.all(returned_value[1].shape == fake_metadata["original_size"]) + + @e2e_pytest_unit + def test_resize_and_crop(self, mocker): + """Test resize_and_crop.""" + mocker.patch.object(self.decoder, "get_padded_size", return_value=np.array((6, 6))) + + masks = np.zeros((2, 2)) + orig_size = np.array((8, 8)) + + results = self.decoder.resize_and_crop(masks, orig_size) + + assert results.shape == tuple(orig_size) + + @e2e_pytest_unit + def test_get_padded_size(self): + """Test get_padded_size.""" + original_size = np.array((2, 4)) + longest_side = 6 + + results = self.decoder.get_padded_size(original_size, longest_side) + + assert np.all(results == np.array((3, 6))) diff --git a/tests/unit/algorithms/visual_prompting/adapters/openvino/test_model_wrappers.py b/tests/unit/algorithms/visual_prompting/adapters/openvino/test_model_wrappers.py deleted file mode 100644 index 0d10525d4ab..00000000000 --- a/tests/unit/algorithms/visual_prompting/adapters/openvino/test_model_wrappers.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Tests model wrappers for openvino task.""" - -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import numpy as np -from openvino.model_api.models import SegmentationModel -from otx.api.entities.label import LabelEntity -import pytest -from openvino.model_api.adapters.openvino_adapter import OpenvinoAdapter -from openvino.model_api.models.types import NumericalValue -from otx.algorithms.segmentation.adapters.openvino.model_wrappers.blur import ( - BlurSegmentation, -) -from otx.algorithms.visual_prompting.adapters.openvino.model_wrappers import ( - Decoder, - ImageEncoder, -) - -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" - assert params.get("mean_values").default_value == [123.675, 116.28, 103.53] - assert params.get("scale_values").default_value == [58.395, 57.12, 57.375] - - -class TestDecoder: - @pytest.fixture(autouse=True) - def setup(self, mocker): - class MockAdapter(OpenvinoAdapter): - def __init__(self): - pass - - mocker.patch.object(SegmentationModel, "__init__") - model_adapter = MockAdapter() - self.decoder = Decoder(model_adapter) - self.decoder.image_size = 6 - - @e2e_pytest_unit - def test_preprocess(self): - """Test preprocess""" - bbox = np.array([[1, 1], [2, 2]]) - original_size = (4, 4) - - results = self.decoder.preprocess(bbox, original_size) - - assert isinstance(results, dict) - assert "point_coords" in results - assert results["point_coords"].shape == (1, 2, 2) - assert "point_labels" in results - assert results["point_labels"].shape == (1, 2) - assert "mask_input" in results - assert "has_mask_input" in results - assert "orig_size" in results - - @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_inputs(self): - """Test _get_inputs.""" - self.decoder.inputs = {"images": np.ones((2, 3, 4))} - returned_value = self.decoder._get_inputs() - - assert returned_value[0] == ["images"] - - @e2e_pytest_unit - def test_get_outputs(self): - """Test _get_outputs.""" - self.decoder.outputs = {"low_res_masks": np.ones((2, 3, 4))} - returned_value = self.decoder._get_outputs() - - assert returned_value == "low_res_masks" - - @e2e_pytest_unit - def test_postprocess(self, mocker): - """Test postprocess.""" - self.decoder.output_blob_name = "masks" - self.decoder.soft_threshold = 0.5 - self.decoder.blur_strength = 2 - fake_output = {"masks": np.ones((4, 4)), "iou_predictions": 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) - assert np.all(returned_value[0].shape == fake_metadata["original_size"]) - assert np.all(returned_value[1].shape == fake_metadata["original_size"]) - - @e2e_pytest_unit - def test_resize_and_crop(self, mocker): - """Test resize_and_crop.""" - mocker.patch.object(self.decoder, "resize_longest_image_size", return_value=np.array((6, 6))) - - masks = np.zeros((2, 2)) - orig_size = np.array((8, 8)) - - results = self.decoder.resize_and_crop(masks, orig_size) - - assert results.shape == tuple(orig_size) - - @e2e_pytest_unit - def test_resize_longest_image_size(self): - """Test resize_longest_image_size.""" - original_size = np.array((2, 4)) - longest_side = 6 - - results = self.decoder.resize_longest_image_size(original_size, longest_side) - - assert np.all(results == np.array((3, 6))) 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 index e38badb899b..27258658808 100644 --- 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 @@ -237,67 +237,71 @@ def test_load_checkpoint_with_state_dict(self, mocker, is_backbone_arg: bool, st assert v == sam_state_dict[k] @e2e_pytest_unit - def test_load_checkpoint_pretrained_weights(self, mocker, monkeypatch): - """Test load_checkpoint with pretrained weights.""" + 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_CKPT_PATHS = mocker.patch( - "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.CKPT_PATHS", + config = self.base_config.copy() + config.model.update(dict(checkpoint=None)) + + sam = SegmentAnything(config, state_dict=None) + + assert True + + @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" ) - monkeypatch.setattr("torch.hub.load_state_dict_from_url", lambda *args, **kwargs: OrderedDict()) 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" ) config = self.base_config.copy() - config.model.checkpoint = "pretrained" + config.model.update(dict(checkpoint="http://checkpoint")) sam = SegmentAnything(config, state_dict=None) - mocker_CKPT_PATHS.__getitem__.assert_called_once() + mocker_load_state_dict_from_url.assert_called_once() + mocker_load_state_dict.assert_called_once() @e2e_pytest_unit - @pytest.mark.parametrize("checkpoint", [None, "checkpoint", "http://checkpoint"]) - def test_load_checkpoint(self, mocker, monkeypatch, checkpoint: str): - """Test load_checkpoint.""" + @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" ) - if checkpoint is not None: - monkeypatch.setattr("torch.hub.load_state_dict_from_url", lambda *args, **kwargs: OrderedDict()) - monkeypatch.setattr("torch.load", lambda *args, **kwargs: None) - - mocker_load_state_dict = mocker.patch( - "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_state_dict" - ) - if checkpoint.startswith("http"): - mocker_load_from_checkpoint = mocker.patch( - "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_from_checkpoint", - side_effect=ValueError(), - ) - else: - mocker_load_from_checkpoint = mocker.patch( - "otx.algorithms.visual_prompting.adapters.pytorch_lightning.models.visual_prompters.segment_anything.SegmentAnything.load_from_checkpoint" - ) + 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 is None: - assert True - elif checkpoint.startswith("http"): - mocker_load_state_dict.assert_called_once() - else: + if checkpoint.endswith(".ckpt"): mocker_load_from_checkpoint.assert_called_once() + else: + mocker_load_state_dict.assert_called_once() @e2e_pytest_unit @pytest.mark.parametrize( diff --git a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py index d122b2f9da7..d7e97499649 100644 --- a/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py +++ b/tests/unit/algorithms/visual_prompting/tasks/test_openvino.py @@ -4,40 +4,43 @@ # SPDX-License-Identifier: Apache-2.0 # +from copy import deepcopy + import numpy as np -import torch import pytest -from otx.api.entities.dataset_item import DatasetItemEntity -from otx.algorithms.visual_prompting.adapters.pytorch_lightning.datasets.dataset import OTXVisualPromptingDataset +import torch from openvino.model_api.models import Model + +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, OpenVINOVisualPromptingTask, ) -from otx.api.entities.annotation import ( - Annotation, - AnnotationSceneEntity, - AnnotationSceneKind, -) +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 otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( - VisualPromptingToAnnotationConverter, -) class TestOpenVINOVisualPromptingInferencer: @@ -50,7 +53,8 @@ def setup(self, mocker): labels=[ScoredLabel(LabelEntity(name="fake", domain="VISUALPROMPTING"), probability=1.0)], ) ] - mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + # FIXME: change VisualPromptingOpenvinoAdapter to OpenvinoAdapter after model api version update + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.VisualPromptingOpenvinoAdapter") mocker.patch.object(Model, "create_model") mocker.patch.object( VisualPromptingToAnnotationConverter, "convert_to_annotation", return_value=self.fake_annotation @@ -74,13 +78,15 @@ 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 "index" in returned_value - assert returned_value.get("index") == 0 - assert "images" in returned_value + assert isinstance(returned_value, dict) mocker_get_prompts.assert_called_once() @e2e_pytest_unit @@ -109,14 +115,16 @@ def test_predict(self, mocker): return_value={ "index": 0, "images": torch.rand((1, 3, 2, 2)), - "bboxes": [np.array([[[1, 1], [2, 2]]])], - "labels": [1, 2], - "original_size": (4, 4), + "prompts": [ + { + "point_coords": [np.array([[[1, 1], [2, 2]]])], + "point_labels": [1, 2], + "label": LabelEntity(name="fake", domain="VISUALPROMPTING"), + "orig_size": (4, 4), + } + ], }, ) - mocker_pre_process_decoder = mocker.patch.object( - self.visual_prompting_ov_inferencer.model["decoder"], "preprocess", return_value={} - ) mocker_forward = mocker.patch.object( OpenVINOVisualPromptingInferencer, "forward", return_value={"image_embeddings": np.empty((4, 2, 2))} ) @@ -131,7 +139,6 @@ def test_predict(self, mocker): returned_value = self.visual_prompting_ov_inferencer.predict(fake_input) mocker_pre_process.assert_called_once() - mocker_pre_process_decoder.assert_called_once() mocker_forward.assert_called_once() mocker_forward_decoder.assert_called_once() mocker_post_process.assert_called_once() @@ -159,10 +166,19 @@ def test_forward_decoder(self): 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): + def setup(self, mocker, otx_model): """Load the OpenVINOVisualPromptingTask.""" - mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.OpenvinoAdapter") + # FIXME: change VisualPromptingOpenvinoAdapter to OpenvinoAdapter after model api version update + mocker.patch("otx.algorithms.visual_prompting.tasks.openvino.VisualPromptingOpenvinoAdapter") mocker.patch.object(Model, "create_model") self.task_environment = init_environment() visual_prompting_hparams = self.task_environment.get_hyper_parameters(VisualPromptingBaseConfig) @@ -174,7 +190,8 @@ def setup(self, mocker): {"image_encoder": "", "decoder": ""}, ) - self.task_environment.model = mocker.patch("otx.api.entities.model.ModelEntity") + # 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) @@ -220,3 +237,14 @@ def test_evaluate(self, mocker): self.visual_prompting_ov_task.evaluate(result_set) assert result_set.performance.score.value == 0.1 + + @e2e_pytest_unit + def test_deploy(self): + output_model = deepcopy(self.task_environment.model) + 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_image_encoder.xml", b"image_encoder_xml") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.bin", b"decoder_bin") + self.visual_prompting_ov_task.model.set_data("visual_prompting_decoder.xml", b"deocder_xml") + self.visual_prompting_ov_task.deploy(output_model) + + assert output_model.exportable_code is not None From bb3ab4cb16ff33b9e56d8e7c873424bedc9e51ea Mon Sep 17 00:00:00 2001 From: Dick Ameln Date: Thu, 13 Jul 2023 06:31:27 +0200 Subject: [PATCH 014/146] Bump albumentations version in anomaly requirements (#2350) increment albumentations version --- requirements/anomaly.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/anomaly.txt b/requirements/anomaly.txt index 97bbf4db2ba..4716c2b33dc 100644 --- a/requirements/anomaly.txt +++ b/requirements/anomaly.txt @@ -1,5 +1,6 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Anomaly Requirements. +albumentations>=1.3.0 torchvision<0.15.1 torchtext<0.15.1 anomalib==0.5.1 From a8c665805ea8cf435ca966fbebf9330dbab68794 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Thu, 13 Jul 2023 14:09:42 +0900 Subject: [PATCH 015/146] Update action detection (#2346) * Remove skip mark for PTQ test of action detection * Update action detection documentation --- .../base/how_to_train/action_detection.rst | 25 ++++++++----------- tests/e2e/cli/action/test_action_detection.py | 1 - 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/source/guide/tutorials/base/how_to_train/action_detection.rst b/docs/source/guide/tutorials/base/how_to_train/action_detection.rst index 836fa77b53b..c1252ad011a 100644 --- a/docs/source/guide/tutorials/base/how_to_train/action_detection.rst +++ b/docs/source/guide/tutorials/base/how_to_train/action_detection.rst @@ -89,7 +89,7 @@ Let's prepare an OpenVINOâ„¢ Training Extensions action detection workspace runn .. code-block:: - (otx) ...$ otx build --train-data-roots ./data/JHMDB_5%/train --val-data-roots ./data/JHMDB_5%/test --model X3D_FAST_RCNN + (otx) ...$ otx build x3d_fast_rcnn --train-data-roots ./data/JHMDB_5%/train --val-data-roots ./data/JHMDB_5%/test [*] Workspace Path: otx-workspace-ACTION_DETECTION [*] Load Model Template ID: Custom_Action_Detection_X3D_FAST_RCNN @@ -146,9 +146,9 @@ We will get a similar to this validation output after some validation time (abou 2023-02-21 22:42:14,749 - mmaction - INFO - Done. 2023-02-21 22:44:24,345 - mmaction - INFO - Inference completed 2023-02-21 22:44:24,347 - mmaction - INFO - called evaluate() - 2023-02-21 22:44:26,349 - mmaction - INFO - Final model performance: Performance(score: 0.537625754527163, dashboard: (1 metric groups)) + 2023-02-21 22:44:26,349 - mmaction - INFO - Final model performance: Performance(score: 0.5086285195277019, dashboard: (1 metric groups)) 2023-02-21 22:44:26,349 - mmaction - INFO - Evaluation completed - Performance(score: 0.537625754527163, dashboard: (1 metric groups)) + Performance(score: 0.5086285195277019, dashboard: (1 metric groups)) .. note:: @@ -160,7 +160,7 @@ 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 efficiently, especially on the CPU. Also, the resulting IR model is required to run POT optimization. IR model consists of two files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. +It allows running the model on the Intel hardware much more efficiently, especially on the CPU. Also, the resulting IR model is required to run PTQ optimization. IR model consists of two files: ``openvino.xml`` for weights and ``openvino.bin`` for architecture. 2. Run the command line below to export the trained model and save the exported model to the ``openvino`` folder. @@ -192,12 +192,7 @@ using ``otx eval`` and passing the IR model path to the ``--load-weights`` param ... - Performance(score: 0.0, dashboard: (3 metric groups)) - -.. note:: - - Unfortunately, openvino has trouble in export from ONNX file, which comes from torch 1.13. - You can get proper openvino IR when you downgrade torch version to 1.12.1 when exporting. + Performance(score: 0.47351524879614754, dashboard: (3 metric groups)) ************* @@ -205,11 +200,11 @@ Optimization ************* 1. You can further optimize the model with ``otx optimize``. -Currently, only POT is supported for action detection. NNCF will be supported in near future. +Currently, only PTQ is supported for action detection. NNCF will be supported in near future. Refer to :doc:`optimization explanation <../../../explanation/additional_features/models_optimization>` section for more details on model optimization. 2. Example command for optimizing -OpenVINOâ„¢ model (.xml) with OpenVINOâ„¢ POT. +OpenVINOâ„¢ model (.xml) with OpenVINOâ„¢ PTQ. .. code-block:: @@ -218,9 +213,11 @@ OpenVINOâ„¢ model (.xml) with OpenVINOâ„¢ POT. ... - Performance(score: 0.0, dashboard: (3 metric groups)) + [*] Update data configuration file to: data.yaml + Statistics collection: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 300/300 [04:16<00:00, 1.17it/s]Biases correction: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 168/168 [00:15<00:00, 10.63it/s][>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] 1572/1572, 7.3 task/s, elapsed: 216s, ETA: 0s + Performance(score: 0.4621155288822204, dashboard: (1 metric groups)) -Keep in mind that POT will take some time (generally less than NNCF optimization) without logging to optimize the model. +Keep in mind that PTQ will take some time (generally less than NNCF optimization) without logging to optimize the model. 3. Now, you have fully trained, optimized and exported an efficient model representation ready-to-use action detection model. diff --git a/tests/e2e/cli/action/test_action_detection.py b/tests/e2e/cli/action/test_action_detection.py index 16bb9bc5cf0..6c1e9902026 100644 --- a/tests/e2e/cli/action/test_action_detection.py +++ b/tests/e2e/cli/action/test_action_detection.py @@ -102,7 +102,6 @@ def test_otx_eval_openvino(self, 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_pot_optimize(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "action_det" pot_optimize_testing(template, tmp_dir_path, otx_dir, args) From 225ddf1701b9ccd9f06d71857f93cc4356699b03 Mon Sep 17 00:00:00 2001 From: Jaeguk Hyun Date: Thu, 13 Jul 2023 20:49:44 +0900 Subject: [PATCH 016/146] Fix e2e (#2348) * Change classification dataset from dummy to toy * Revert test changes * Change label name for multilabel dataset * Revert e2e test changes * Change ov test cases' threshold * Add parent's label --- .../datumaro_h-label/annotations/train.json | 469 +++++++++++++---- .../datumaro_h-label/annotations/valid.json | 484 ++++++++++++++++++ .../annotations/validation.json | 157 ------ .../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/train/a.jpg | Bin 631 -> 0 bytes .../datumaro_h-label/images/train/b.jpg | Bin 631 -> 0 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 .../datumaro_h-label/images/validation/d.jpg | Bin 631 -> 0 bytes .../annotations/train.json | 414 ++++++++++++--- .../annotations/valid.json | 425 +++++++++++++++ .../annotations/validation.json | 141 ----- .../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/train/a.jpg | Bin 631 -> 0 bytes .../images/train/b.jpg | Bin 631 -> 0 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 .../images/validation/d.jpg | Bin 631 -> 0 bytes .../annotations/train.json | 252 ++++++++- .../annotations/valid.json | 302 +++++++++++ .../annotations/validation.json | 70 --- .../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/train/a.jpg | Bin 631 -> 0 bytes .../datumaro_multilabel/images/train/b.jpg | Bin 631 -> 0 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 .../images/validation/d.jpg | Bin 631 -> 0 bytes .../annotations/train.json | 190 ++++++- .../annotations/valid.json | 238 +++++++++ .../annotations/validation.json | 54 -- .../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/train/a.jpg | Bin 631 -> 0 bytes .../images/train/b.jpg | Bin 631 -> 0 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 .../images/validation/d.jpg | Bin 631 -> 0 bytes .../cli/action/test_action_classification.py | 2 +- tests/e2e/cli/action/test_action_detection.py | 2 +- .../anomaly/test_anomaly_classification.py | 2 +- .../e2e/cli/anomaly/test_anomaly_detection.py | 2 +- .../cli/anomaly/test_anomaly_segmentation.py | 2 +- .../cli/classification/test_classification.py | 6 +- tests/e2e/cli/detection/test_detection.py | 2 +- .../cli/detection/test_tiling_detection.py | 2 +- .../test_instance_segmentation.py | 2 +- .../test_tiling_instseg.py | 2 +- .../test_segmentation.py | 2 +- 131 files changed, 2580 insertions(+), 642 deletions(-) mode change 100755 => 100644 tests/assets/datumaro_h-label/annotations/train.json create mode 100644 tests/assets/datumaro_h-label/annotations/valid.json delete mode 100755 tests/assets/datumaro_h-label/annotations/validation.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 delete mode 100644 tests/assets/datumaro_h-label/images/train/a.jpg delete mode 100644 tests/assets/datumaro_h-label/images/train/b.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 delete mode 100644 tests/assets/datumaro_h-label/images/validation/d.jpg mode change 100755 => 100644 tests/assets/datumaro_h-label_class_decremental/annotations/train.json create mode 100644 tests/assets/datumaro_h-label_class_decremental/annotations/valid.json delete mode 100755 tests/assets/datumaro_h-label_class_decremental/annotations/validation.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 delete mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/a.jpg delete mode 100644 tests/assets/datumaro_h-label_class_decremental/images/train/b.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 delete mode 100644 tests/assets/datumaro_h-label_class_decremental/images/validation/d.jpg mode change 100755 => 100644 tests/assets/datumaro_multilabel/annotations/train.json create mode 100644 tests/assets/datumaro_multilabel/annotations/valid.json delete mode 100755 tests/assets/datumaro_multilabel/annotations/validation.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 delete mode 100644 tests/assets/datumaro_multilabel/images/train/a.jpg delete mode 100644 tests/assets/datumaro_multilabel/images/train/b.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 delete mode 100644 tests/assets/datumaro_multilabel/images/validation/d.jpg mode change 100755 => 100644 tests/assets/datumaro_multilabel_class_decremental/annotations/train.json create mode 100644 tests/assets/datumaro_multilabel_class_decremental/annotations/valid.json delete mode 100755 tests/assets/datumaro_multilabel_class_decremental/annotations/validation.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 delete mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/a.jpg delete mode 100644 tests/assets/datumaro_multilabel_class_decremental/images/train/b.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/datumaro_multilabel_class_decremental/images/validation/d.jpg diff --git a/tests/assets/datumaro_h-label/annotations/train.json b/tests/assets/datumaro_h-label/annotations/train.json old mode 100755 new mode 100644 index ecd34c01f0b..dc7994026dc --- a/tests/assets/datumaro_h-label/annotations/train.json +++ b/tests/assets/datumaro_h-label/annotations/train.json @@ -2,209 +2,482 @@ "info": {}, "categories": { "label": { + "label_groups": [ + { + "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": "right", - "parent": "triangle", - "attributes": [] + "name": "blue", + "parent": "", + "attribute": [] }, { - "name": "multi a", - "parent": "triangle", - "attributes": [] + "name": "green", + "parent": "", + "attribute": [] }, { - "name": "multi b", - "parent": "triangle", + "name": "blue_rectangle", + "parent": "blue", "attributes": [] }, { - "name": "equilateral", - "parent": "triangle", + "name": "blue_circle", + "parent": "blue", "attributes": [] }, { - "name": "square", - "parent": "rectangle", + "name": "blue_triangle", + "parent": "blue", "attributes": [] }, { - "name": "triangle", - "parent": "", + "name": "green_rectangle", + "parent": "green", "attributes": [] }, { - "name": "non_square", - "parent": "rectangle", + "name": "green_circle", + "parent": "green", "attributes": [] }, { - "name": "rectangle", - "parent": "", + "name": "green_triangle", + "parent": "green", "attributes": [] } ], - "label_groups": [ + "attributes": [] + } + }, + "items": [ + { + "id": "00", + "annotations": [ { - "name": "shape", - "group_type": "exclusive", - "labels": ["rectangle", "triangle"] + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 }, { - "name": "rectangle default", - "group_type": "exclusive", - "labels": ["non_square", "square"] + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 }, { - "name": "triangle default", - "group_type": "exclusive", - "labels": ["equilateral", "right"] + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 }, { - "name": "shape___multiple example___multi a", - "group_type": "exclusive", - "labels": ["multi a"] + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 }, { - "name": "shape___multiple example___multi b", - "group_type": "exclusive", - "labels": ["multi b"] + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 + }, + { + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 } ], - "attributes": [] + "image": { + "path": "00.jpg" + } }, - "mask": { - "colormap": [ + { + "id": "01", + "annotations": [ { - "label_id": 0, - "r": 129, - "g": 64, - "b": 123 + "id": 0, + "type": "label", + "group": 0, + "label_id": 0 }, { - "label_id": 1, - "r": 91, - "g": 105, - "b": 255 + "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 }, { - "label_id": 2, - "r": 91, - "g": 105, - "b": 255 + "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 }, { - "label_id": 3, - "r": 255, - "g": 86, - "b": 98 + "id": 1, + "type": "label", + "group": 0, + "label_id": 1 }, { - "label_id": 4, - "r": 204, - "g": 148, - "b": 218 + "id": 2, + "type": "label", + "group": 0, + "label_id": 2 }, { - "label_id": 5, - "r": 0, - "g": 251, - "b": 87 + "id": 3, + "type": "label", + "group": 1, + "label_id": 3 }, { - "label_id": 6, - "r": 84, - "g": 143, - "b": 173 + "id": 4, + "type": "label", + "group": 1, + "label_id": 4 }, { - "label_id": 7, - "r": 0, - "g": 38, - "b": 2 + "id": 5, + "type": "label", + "group": 1, + "label_id": 5 } - ] - } - }, - "items": [ + ], + "image": { + "path": "02.jpg" + } + }, { - "id": "a", + "id": "03", "annotations": [ { "id": 0, "type": "label", - "attributes": {}, "group": 0, - "label_id": 4 + "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", - "attributes": {}, "group": 0, - "label_id": 7 + "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", - "attributes": {}, "group": 0, - "label_id": 5 + "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", - "attributes": {}, "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": "a.jpg", - "size": [10, 5] - }, - "media": { - "path": "" + "path": "06.jpg" } }, { - "id": "b", + "id": "07", "annotations": [ { "id": 0, "type": "label", - "attributes": {}, "group": 0, - "label_id": 6 + "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", - "attributes": {}, "group": 0, - "label_id": 7 + "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", - "attributes": {}, + "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", - "attributes": {}, + "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": "b.jpg", - "size": [10, 5] - }, - "media": { - "path": "" + "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..dc7994026dc --- /dev/null +++ b/tests/assets/datumaro_h-label/annotations/valid.json @@ -0,0 +1,484 @@ +{ + "info": {}, + "categories": { + "label": { + "label_groups": [ + { + "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/validation.json b/tests/assets/datumaro_h-label/annotations/validation.json deleted file mode 100755 index e1ab5b0d1c0..00000000000 --- a/tests/assets/datumaro_h-label/annotations/validation.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "info": {}, - "categories": { - "label": { - "labels": [ - { - "name": "right", - "parent": "triangle", - "attributes": [] - }, - { - "name": "multi a", - "parent": "triangle", - "attributes": [] - }, - { - "name": "multi b", - "parent": "triangle", - "attributes": [] - }, - { - "name": "equilateral", - "parent": "triangle", - "attributes": [] - }, - { - "name": "square", - "parent": "rectangle", - "attributes": [] - }, - { - "name": "triangle", - "parent": "", - "attributes": [] - }, - { - "name": "non_square", - "parent": "rectangle", - "attributes": [] - }, - { - "name": "rectangle", - "parent": "", - "attributes": [] - } - ], - "label_groups": [ - { - "name": "shape", - "group_type": "exclusive", - "labels": ["rectangle", "triangle"] - }, - { - "name": "rectangle default", - "group_type": "exclusive", - "labels": ["non_square", "square"] - }, - { - "name": "triangle default", - "group_type": "exclusive", - "labels": ["equilateral", "right"] - }, - { - "name": "shape___multiple example___multi a", - "group_type": "exclusive", - "labels": ["multi a"] - }, - { - "name": "shape___multiple example___multi b", - "group_type": "exclusive", - "labels": ["multi b"] - } - ], - "attributes": [] - }, - "mask": { - "colormap": [ - { - "label_id": 0, - "r": 129, - "g": 64, - "b": 123 - }, - { - "label_id": 1, - "r": 91, - "g": 105, - "b": 255 - }, - { - "label_id": 2, - "r": 91, - "g": 105, - "b": 255 - }, - { - "label_id": 3, - "r": 255, - "g": 86, - "b": 98 - }, - { - "label_id": 4, - "r": 204, - "g": 148, - "b": 218 - }, - { - "label_id": 5, - "r": 0, - "g": 251, - "b": 87 - }, - { - "label_id": 6, - "r": 84, - "g": 143, - "b": 173 - }, - { - "label_id": 7, - "r": 0, - "g": 38, - "b": 2 - } - ] - } - }, - "items": [ - { - "id": "d", - "annotations": [ - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 5 - }, - { - "id": 0, - "type": "label", - "attributes": {}, - "group": 0, - "label_id": 2 - } - ], - "image": { - "path": "d.jpg", - "size": [10, 5] - }, - "media": { - "path": "" - } - } - ] -} 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 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/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

wBsIa8 zRXXGL1S7TPhu_iaQ|S;#MeD^cr3=1}gm1czp=xI(O8i{b$zuxML3V<W2TZ!xNRGO?3vWo!_j|E6{pru=(q#OB+dH|!%?Hh{udy_WAOp}D<<%dGB!F$ZT; z6Ar?}uA7ves!}94mzek!mFvFM82GTO&8?bNEws&ZnZsC3DypxmT(oo7EVL$T9iZ&@ zp9WM`shF2=KlY*4ErH!Dx(W}bx6doJKwnd*HLRHm5Ai5h@eRdj`vCKw$Pq3%<-&Tz zhqfJ_1N2yIM3jQ!;5tw0DC%VdFaP$sHvA;!_^ULJUea}2HCO+Lef0rd2_X)y0Y}fB zXX-U7EKq!!LGypDDXM@ zu*`-+mz zCV3`_^_+QG26g03?KQj=(*lqeM6#tjAW|a%FbvQ0F@XY1(OU(cDgssM(lFaiHR8Jd z>mvnxR)ZT1HQkJ1$6>>uc$ls7mz8Y4Q#u|-7+byIWK8hk0ETmhL(v=&dc6+>-+$Hf zO0goGbMQY6RODrRE(!hFem^P^;4M#+$WUn3zIlel!^V_{nd%%a{fbg-nnNnF!ugWx zOe4NBm1k)s=}*7v&Yca$8#Cs-gW}=MI;Pui?%g*E`Xyfy!Wqyku3?Tc{-F-ka;sgac#l8l^g`^JS{3j>E7G)eUa zxziGB%gn{XSU#P%Hs!nRo+7_1E<{OSn|{y%|L1kxFLzSfNmf@r;m>&J2!j>3o=NOu zT5$U#pgP}B4aMUwZX`iEXX-|bcQPMufbiHC*}Il9pn*A#_268;85SdhdO7}jO8fo0 z6&7iw@JH4c|8Vi#59{bI`Mp|bSnz5pg4306=sWl}s7DDmO1FD4g&nTp0D9?fA5_w4 z*46t-0AoIv{2)j-hz6^0LWUkXWe_63P9Q^MhgJD}n?EGqm<3p0Vv?sGypmXMqx!d= zDvr~3nVa|I-Tn0AVQqBHS5^$ys4brs4QL>3b)FR;@#@^O8XC(E zxq7=Z4|Dw}dH-DjSe6Mr@{^+ZHkNX(kXC!RE{*@QA`GH_aHEArPp=0GScla2IA*9( zsX-P1%jk-M@VXYVOlpG!@XfQ7XYGA77Nde4$NQ2Uc9xtN^-T+Klj>7uB?yLJ`&G9d zmL35T2G)+x!u-YnfWXn1v~F;?*7lj~gy)Ctdoe{;>t~i@17DnA1%Rxk#Tf0KV)%am zzY}>7Z$hIGt2H;p$mCyh*k7ZB-!Yn$nYYG56u6-LBCTAfFCXAn<_tj491Btt3oeZj z=*(w<={Ts3vh3JYgh)&mDCJyT3)8b*hXk$r~mi=5lPG5hF;2$ ztIA>f3$hr?Q+Uesg+hq8T)J@(r_i6z;y`{DzS}?1Sp4!mEH^uDtLb%k+D`P zAceJT)91-`0+6?DzZ`(~tLWEV{&k96!hZ()`%2rb?|RUZW&WJ&Jly7U*ikhQZP8#CYxaxv!r z0q=hU1}iP>nTKOMZiepMH1f14V`EjZF+-+H{6j9HJl6Wc4?uIlFeu+-)Ywru1fmdUAfi+eb5e_xAATBzZ~RJJJ$PRd;l z((q222%=KWE-2|VL+jr!SU*o!joC`bnX~)AkG#SwqN<~BKgX&WY0@mGu4EqD4QQX& zC`}@XOK1n#%?1j+wJs4{t|_Cl%ulYDUaAv!)p&SMEY+&8rTopk7Hr{<`{W~gJwiPD zg;q;CkxJJLw%bGR_l|Kq&x?lEA8E-8(8e^ZcATVzoOi@bKha3EPkqpu!ZpV8MYAbK zT0&agor(>nuHjj>&KW+{HtC*v&pd@NwPd8bQAZI5(2}g7p;oW%JxDCAa6|o=lz8Hr zjHXU+5c>P*@YzqSB&8cB*6Z$6m;}^$i)E?M{s$m-4o<${m{J!XbjL@jD8996V9?Vs zt=B@b72A*X35t7G)lJb3tQyl!oOBckbYUMZtR)JHNi=mEXR4+bIPA;{>MPav59qZU4{f*T7l9TY)$7PloD;SQNxm3*YPHVxkg2m7DzxB+l3KHk7l$)2INYm>{xT z&o?kaX|2Zr(qh9feruNoaYIqc=P49-t{P@ZCrjhhtJpL;tpCzf0E-FhIzLb0Qgp<( z6YtHXg`=c4B3x1t1AU1o9sdX~zGY`fnKE5Efn@<$YHr;hona<;iON2fh^%_%?;oj9 zCO%$96Va&pw~oZy0J*TO(p0BK2<*SLH(ITY4dmrYc-yF798ByYEP;KIJYb+oVbKPi z*|RsQxGsZn9JB_7t7{QcUZC*!h+v`%Dvt1xcE3}A)==OfXA|# z!pj*B+fj)_U=f2K<|o|nyf|W!0^C-boBT01v7*#5uG_ya1+XO|QMslv5An{RsfLt? z!560y{o+7>>=dKW<1<^-dUy2qeKOS42$x{$qj7G*`BV<{+g%R&|L}t*gKeVUFE4vb z_J$Ag@<{57{<@F%+Q1k9oR=Q!S?bC_hnXq4AjLTgFR=a1RgE7sQvgI+GE7JxZ+fnD z)rD*d+o?ZBwbWWIQ~t7p@^wUVR1l1|A(n5-cOz1!EaCZ1oJhguryel%kuT4!Joqr; zU)jz|AENCjY{kCLMS zC`}g&HW;)BPw+j_gO*<2PoRB)c=r9;_6fGY65jQvPCC8*Rs=!R-c+fxd@zi6n5j{r zeu}=ZZi3MYbY!=vs_aEy&#>Nxbr_sB(8 zST!bycBz0Gf_d{nF=acEVi~J67m99ZNF->oJGB1mZxNtHynA9y%iR;A=?j(}33KF* zUn`uauW1BWa!sDhv?poehxdF|c<77%Fv_Ih-HDCWlHSys2B`TOi zHf|~N%62eG+X7GBOX|ovX-3e4OnV3(y5WK}Rc_FVQSgtz|7n$)+vh=E5a0)gg9M26 z1=K}RWL1GnK=`%a&@eR`hDmgy?rImI|4fg)<2PGER^6mHXB9JR%fr85J_^CGSTF() z_NxLH!sC)nP-bSQp3W8;O9gw)$3UX*4b#s|q8mgn8loDF@j`$ip#OBx7{fPq^932w z`Ff)|PM$>f(SSyx&a{yZQn11nlL*#(S1vF&j_-&=LKi}@z6WO%mhuq;pDu}#&i5(i zGnPMz9=mCddwf(J&gscj#`%el6#v7a>p&>rAkELGu0GrZ;1Y)d=Q8I9FtQ7eLn5Nh z%xJ8+G#~FI@v)Fk!J)^|+`3LrY!74x{IrZuTphY;Ef4a@1eAk^aeN~N&^oj1i7Hr1 z2=(ZZ_7>h4Ho@ln+TYb!guuXQ=*fw`NVl&ZcCo5=2AQ#v;;O#}@^p2POEW zX3pVhGR`F^&TTf`%yY&r;OJ1Sq&z1riXy;VyadTakqHB2dC3K139bl?r)H86x;uyK ztGiT-61;vdKQlM$|8A^a7`=Kj^<~k&_uvLD*lIB=&z-T#sin3Yklv%h07y{X2v5bM z`pa+YbZMovx*)nG-T0*yCi-kZqJ5P0eJGqNsFTVxk=flOfq*g``$qCW6m{004n!eD zltn3>^QTG`mVRZ%?xKE+_EhN;3={VyK&Satm#gomL2$@N<`Inr2JHS)I|K33v}r~> zp$_5ndt@F)d%*9`ieKdL77n5XH~DFzaajXtFn?A!NtgI!(FOpVwgRHT6thkq)W)Fp zTmRDR->*(WpSSMDCLrhcOnv!~?9KD1E?+pohh2W;RTp?t zdcR;gwb0#V4X*|XH4F*&CpkNsLjEV=69Rb4n2 zT%o{x+wTbQ7DMK}i8rcWG z{9+sD-<+pd@(YdE@r@+IjOklz*Kk;f)icHROEAK< zCPT_sfWa0sH7>k3?>DvsdK&+k1l4GAJSh1OxYGsLU$3WIX@qoLlP3Wj?4=lsFPrQ$ z;I7tQJHr&pcj^FAcQ6JyvK$k{nnuTnE$}xznNXrlKrDOlt&t#O0;1`=oPNG97XO0& z1S8I>_mfVt8p|s{R?mzLA(F>w6c_3!P*%mj1k-#w8&3c#85i2un%V}Qyw z_7e!#x$WVjq=xu6&YqX?=wy%V4c9&|gWXvRqX~HM2zRA?JTyK#HOB}ZM&iYH@E7GM z_gO{h?JtM?`-7LXqwwYMz2AxJ=mZ;C+JhJM4wD4s?MwjyfykEto7q8!wVr7S8VzawC$a z8vAtGJ+(|W6kYmb?T{?TYL}MqzPj08hHx3iF=&oV6DL6D-|zb4Ax#7YxFv0N)Fvy5 z3?ZVT^b19O95+?LHx|p+KYseJoLYmfPM)CwuRO`i6gX!vIH}eAhbbIf`_NJg7A5HH z5{A%TsM)}UQ}4@KYA-T;A38hzeey)er6s>XmabJf5hGCB$lKd{F;nIKJDkl^C6!8qMkDqi(_-k3*#;eb(!>u`H(8~2JX%%sw6%XVFz%m|1zkwF@8$gFN-%p%MY6ezT`INd5OwSpbvWBMm1yI8D#qGm;}CEW zyM>c*V!?XV%Xxbo!8i%`j^!?Fmvv+eTwjbvh7N|Pxa*By4ij=5&6TaMH-2t%zL52; ziQ3_wn#V}E+1BtTDYL<=vCQ6{JJ)UQ7UPw7%a`h+CLnzWq)%PO5@W9@MW&;X^tx9X ziXPbd{$s>*=aGDAh8Abj3$jr}#jZ zN|OA#xGFX~$GN2^^>zksA{E~o*6bREraAatFFlfQ;Z~b+RWiTO>B7CnMR&uOsDjVU zHFhXO$?MG9yfHriMMZj5mf3za{1df+jhxRncU-aX&Spq5!t!N~GUrt{*$TWadkZs^ z*Ef9dYTzUibR`rR@~9aw_^Fyc#Giyx1wHA`l)c(ICS=OJ!7H~~bxQ*H`249Ec zB#XiUS|g;2{pX}e;KEAKfCRgi#!?Hw8GT_R;?CcH;D(~)wIC>Nv>o>11NlF=8v)$k zxX{dAOMZV{vRIL+RyJJf64@CLmdO{km)&UG7oK((f;Yjf&7G&zlNCsN;ItBR4x{tzUuPCo#u?bl#h9O(EZ$74c2Cq%ZS@_bAtnKsU0#+! zS=2Vp9bb&p7Svz-gt|t=b7i7Ex6Vxo6rb;do@srzQMc69PlTcE*Q@%5>z ze&I_?Ov9{Ag9egeQJteekW=Z#x2wd6aQ^;7GKx`qHJJ`NcoI=EcDw?y{qV_LzhEf3 z`uB>2a^q;szl2aU+)w{EVCCR6m`KZ3CDOXk0+`^-U{NmiUv2aGZkW-j>wt;}9|q`l zFKJH-axJlb9g)LZo54uz9asO@a-)XEZ>(i;)rs%p?WpEH8d@G7W<^lbu(J6 z5DvqduQU+S{f?{&=0>pB92HQ;>|`D%Dl2r@yBWZYkam6B90RnRj&GkV0kM91cTwbY z3WTx(+~>R11RV)yodfv=v>^;--Oby+oj(m!0qaYJ-(j!U54sPxG9Y>-cW&E?=+ewB zW0c}83#1H9(R(lUmm;BQtd-7BhC6>}fcbquEoOhTVY_P$JhdDsO&wrN71j5^&wG{l zaj|)f+6gYY?B8#|NBkU$V&~|`cNzfb7&5$8fy?gyJs>O7(~)Ak$)y)1@fxUyg6GKaoypf86fk}hRa}dRpy!o_`mU(k+j?pNnF?;dF-NB znHiw0fX*a%dQW}^k3L~>#YkBve}QCVjCW-I z2Lve<0CP43hVsPWa}}ot5}0s6U1-i2!>jWF>R(C;48);njAnkMKV=Xd*UljaGw zMkJ+8IVW2XV(A}*3wRZPxDfT~1$}{9xEeg{ACc&Y&(IGMPYApC7VwdtQ(Q+Fvpo^f zvT+rl!~}NnEpg`=m<+F3)}@YQ;Y`^m`tmHvStHB%+VAYg(3e_unKEl%OiY3$ldSutsnfx` zA=?{C>1!!#`5iADo0%mPUrpWHr-yT6=ZE<1IQ5~=7f`bkGj~u&x$zLH^h^Y!Sd6=xcutkruI+YB%*Mgc6w~ik92{CqcIF1(|co+Mq!I4dP*(r}z>L@n<-T7n=yJ6KOKrl&EpKI&|x z3L)m=RRSMdF53SHqHgs6z{8e(W{|)w*Ut3_&^X6nZRRc}>hL;p8r+*0IG1i-+}ZZn z==3GPwM0`@67(lUq)^jM*YIS_LDF}`#VFfO-y{Mhlnh0ld8x9ponGwY#S5wN3X{hS zij}SrfFgDNuw{|kTOjEp3r7>bLR^NL+JlE^ws%HtBC^`KVBZ8#6P+%B`{zRH)F>CT zor8B>=*_H<%O7v2$5Uj;zOhTROSimPrr17$^CkkbI8m$N>-TF4(K`aLT!Qj=a>Ig# zL5^>L{wUPTvW5ZTB$}@+D?j`FJQY}D(i*WSK;0PXEGPDVOPNMbGi5w}`g$|L4=Hf2 zP@J4@&#G0IRp4=9Wsk~otQcx%epAjm*2dSj`>8R0eFH@scjWqaj5rE(jW$at$Ldpv zYmo@L0(7VhD>yOx#X5I>8&Ch0rbHN%h!py}I5_-y$}gTbkdsBFQ3Sty)B! zzzXhM_~MwvjH+b45#i1eU-CSc>>3zvDuRTdX1w075nVJ)U2VDeKVUV0f%VCzuJW4N zox{mMqV`J|uXGVI*wkNMHBA)cI%cFD4~6q=eFrHn{Vnb{fp9v8B$)Hct}X2l|zq2m{ch!Zzqc zJS+W72pJlWEznuhcx-Ig_xFGVaCPe@GVvrr$(_4kX@#4wn$}?osYMV0Qw;VtYdHer z$e7|+5t#+x)28}eyTHKw_~^z$KzDc8MxvI=cpAJw4$ zIKx9Do2p>*0{6A=lqdqc?~AoGx0kuW73$5Z>Q;=w*_~Q|jTUoe)M>3h6s42`N!+`o0`AI{&+4e zIud|6De<|}OqmaSng=Tfv~}r?PJ$|=GF_O+2}U_#V&D<-63|oMw-r$w1;d|rgy;tT z2dFDg2oPwU0~F^ytKY?Fqp-y5&R;G|103izErSB+mxaUr1qKRJTb;Wl#Gr?R42m$- z831t;(7uEXJ|&=;XIrWhn}yIK7e==>NJ3=44XoUFlb@Ds=l=eQFakC&8PJ;syJolm zs$e2dC5~UJkG?gSSf=_uIof2j7`rSNHr0);V zi)4l85LYs|9{SVY_q7UbM>ca8Sxq@FhnZWp+-hxJ zGrBFVKC!s;$ar8X!!@WGH zXJdBo{dKpT8pa!RQa*|FXOkce_s`5IaZR)`e*WDgBFC1t_L9!%op}X%hX)ZZ_7#3M zqem7=*OQX#*LNj=Aop4;nl%j*j*%oC#?jH&o5%!S`vnhXA<0{wg)Q~sYHX?;-I)hV zeUyh5DM3y~FMM5tH`==N8s_J`_C6#X`F_9r&L?&>qyDqqu1#g8j}9Tp^U2Zy4Uw$G zu2_3iZ{<-xLx?G#Oau8^hSj8jULYqX5MQ7kQ#AaZg%6*0J_hCwv zm2dCpfAXwgAt9I@<9wTEa1oy`Sf_3$w0!aYAojUiV=B8mLhYmP}qdm+`gc46_E%WHZMs-;?H& z&RBHbB>%khWdO$qRbvSOK?*;?2ka7T*04yZX$MIf`G$-92au$Z zg%eo~DmaKHc*|}k2ohr7j_%ksyF@Ar261KRv&02eZPFdhYNZIbw0T{>b3kQ)-cO4b z{b12!{{xI1(RO}@4h;AJ_>hj~7yHy2NR!T(D;~W63;Tlmja4p_R{c9iNGliAE76Dk zw`TpiFf=~j3`0B+zZUof0I{a>O(XtIiu4j3pb1*t)F0?2jlRu5Gq)<)d}ty2h4cNz z3Q2H6ckC}pQv(yqgf)_+Y58UtsJ+AlOI`Pyc^%2Ha4Gj2V1(5@V6zIHFOf+O1t&*G z0dymk2DA>?2pZ7A)sRr27shm8sn(76@8cV$(CAlL&66Gd9;^^t3u{uC8$b_^7Pb@A z^X(0Bdne8tSEbw7qHJwUF;{YpUJpb_&lv+@{A3kW?7F+_aC?TDJT(~ak#Hf_(wq3s zVh9IKsMli~j$OoqV!@NynWGn48jAVgw$*_tmO01p3z94p-)iOZ~m;vh_{EwEFEK7d0aU{GH?+9!993{ldapf6+H-KQ7A`>pH3LN7LyNzprA64I0a#P$>IAX#S5j-2z#H|Hqegv?h-?o|fkLO7Fvk>>fmuy+ z=xB;ABYWZn8tP=#UjRoAMaxRFlX9~M*2d;x-(0B&Y>az_H zgU%d`n+!!QjxNBRK_@vSYhB9?DZjI{ss?6B0fyz9^3Q6>xeI5#zXe?@AKwzMXhbXa zJ~H!GQ7RwJFf{J%Z6zJe@NnP$Q=AWbIp(qeqO|I>#vciaT2fto=fEnU+#V+H9 z1wZEE5{($Fy^`|WHJr6ws~m!t`0QL-EAa zE{&b~s#F6``K_t$lJRA)O(L|&Bl4BmUZ7U%C84zSw&qUtv-2+fI0@d2Fw@uf)Cvl`!|sWig#OdZ z#tOHoeZa3#nYJ#4noa*$mdrEwV=JL$dh4Z04{{1Q`e|P5E3{D6@ai*Uy0PdKlg^Zz zO(17`+4{BSjjc5O50jU1?w61$Z8kZrlBM>24s=i08fBk->)c;U!j1Pby!E*6?&Y6k zhs_}~tX?=2J6Lw`H^X7w!--Eyaclzb)N-^w=np;M&>pc?H5nf>tCLIt5ILdD{%Zt* zx9)I`pN-P)^byOPGfQ>X3*AP1thl|sZMldSrpjo7cZ#uQJ>V1PN;lOCWASwsr>u2B z?>WZVJad7*PleWtOEBIGE0nmL*?$x;$m$hDVp7#SUvmW9$$dTdN5+T1RI?zIvau%) zLJZuvF1x}}F=v`=O)qWiNie)v!S0 z7YB*8xlp-tEcCa$w--b$E{?od@YtwDk>DUd#yChI*?fe;8PW2;>4#|1By9J3#bf$4 z{99T%aT#@|2S#fy2?F5wb9?OMA?W9fwQ(& zDkl+U$cTRaQ&TOw7mPG85m5BHYT#o37(XiLIs;sBoie6v+hib!Tx-T7!>1%AXm9zE zhU}aSR^M3<8Z9qB*i=MIXEr@Z@n~ly6ZQtMqT~B23Ibk4T1sf=$z`N5>r1`qIz@S3 z-Kge=g-C4^ww&zEhR-SLOKVFoe2I`?GKPT~9C3!8giN@(h@EpK-fb)1+rMluA#QZ+ zZO+mrKfTES9cHnxR;h0+^Q`{NM0y1=;WE~SoNd^;}KRaOuE>Q`%Y zgh)S9;U{(tE^?Pc5%_;!kX^qzUHW#B>pk1JAQ_R03%9CbcDZ1gW4#aDQXiFcWH z9}`;GEm^x4Dag4II59Ygq}DO8He182HcZ7poYg9uADMYpg0dS5JR_+XfXwk&i{SqQ z*Z|i2#IjKPD+&TzfjB@+UjbLu2zhq|NL^S&XU49A4?svSFxzO1fZ-lGfGhhCwCUZ2 zKAOey#B|!$f0m&gDdI;xQEWUkN@W=m*$_?nSL#HO@F7uyRXVlIF~7lur3E1sy#zqD zX~+(<7yq z2?r7~#DG}4H<$!S9rcEvRRU~wAHE{ZDSQFf6oCXevGaLKYTE(5V0=u5Ghvt4{}TGs z7y+!*n#VsVRp!ZmL0_kK8DQabQ-=dMu=OVZ80HlrtCxBEeJ3bqg>FD7%uS&##K1SY zk6NPwo1jsIq)}a?e?8hELlkEmyFQrXQR-`8cvXz~dAcpgCQ%(f2BTXJ7$8}e|5?o& zUxZ^g$XT(6$bsw?S7UMPT)K}9wDMiuc70P5Pv*i_e+$JPBFn3j7{BiHLFG7kErRvz zU{r+$&nYfXcIzD=l0H$Xk=_s~Yi)#0?*!h@5M)%K;@ur(pa(Ck(Wl!97;c!tiT(!vD+^ot!4q5{&@1`TY@^q?e>4` z`X7Si$z_G|E=c0XX8%ZNvO{nc8zUV#%ikqKOWG5a7A)YT1$T}jkR>$4Hn8>2T!MWb z)hGhp*p!jvjspaE$upiSw5m1}x4>h#U?mrC2SxeQ>T!7Q>2DynIvj-EE|;``B)0An z)A~s|jnu`Dztq8R1*TC*w9;%C_~`kU2yl?}l$CV89Ne1Q3I-3*aAv)Rn^SCG{ywrO zb^w_9?DDR>TVaN(5%~YN^oCSp%qPQrX(7F5WzQA>wrj~On-X1J-6j2(wv&M8(994o zlzfm{7h&u83yvn4hPJsZR!gYauNl1RmOSX7u$Srfo@=`x2#n!REuEzznof+F8cdg>*nq ztsla#vHYBv_F_Beql`j)P_!lrb~;LThJxe$@N_!k-wkq)4A(M%IN8D$$VasEO>mws znzNbun5DZ#f9tG&hkpZ4-*D*xswOq{)+O1A*Vzp~vc*@!sM~O7Ds!ev0P$aDMNz$M zbd9&+C#?kov==(txE75EfcR^=ZDC-c9#B`Q>jg5T!|y?*p;is^~8 zcvDMhg4s!-+-|9uo6q)-&B}L2RqncDIbo(b7Ddl9ER#O}T)oT16|!MT{XF`s&Wp?2 zGuUykfnZ)ZcV2O|QyPNx=zm{UxdCl*~tpu|=*$1;KK76z`KwSpWOq zKR1UhEELCm&fy+Qt{k^_riOZ_*5ow>j_M~{YU~e{q+|rrIo)PS_<0GC5jTAvQ2t#i z?}Oii?2RpaSZr#yO4&q3$+voS%U_G>@A_TIA>u%M_v>?Sb^-T^HP3JCK{u6OtCajp z94W5e0%>}qsXy5ql&(bP^K!f%yYWER>3pu77y2_|mgg%Mn$((MC zoc>k+$EHdVDYUoUP@RE?qg4zEx3#lZxt>m>@;eTC7FnT}B@;+@hIp-1-pG2U#Q#Br z2Kk@c2wwQscY#4~;7j3HFP?=d8>5tv%dr^h5GCPfAWY79q%d^zapx^Ot-^O@-v~H- z1yV1p`jvK_ZgmNO3{ow1kc>22soMC2s5!;i#`;bl*GJhBD&5xe5&1z6!n(jCO=~tA z_GO%r+u|AN*Hy6`H)(CoAUi&kjfXn;XCH{tOWqtx|bVi-unk1f3;*nrf-lc=n7EhA3OI6 z(7pOqk9DL86<`f51?_&Igi&HyP1* z4kzozB;Pw`vEss%j2^CQShrvB$qhn&Z1$r7>dANXZDGDUORb$7CoW_M|F*H(Cxi3X z7)~F!*rl7t-Oo4AAg!feI7iT{klt5b@=H8C{JR23zU==>8*+Bh>@tItr9!t&yELZe zkgQB=LEDDk+NtF92S+CcUtOnoXw~|L!LjE8ki+ffIhke9rgj+y0oJSpd%U}jZ%*Tz z^rp^Fo+~qdit`6&ci(S}@yS=kYK9E~ZIe5WaPHb2GFgT}EnX5&L~860;yV=t3uSyR z1}0#nrNay{Y&Ac9*S3@JIYM4Ex7JN%xVXw3>Q1;5pNS!g;$SF0w9GeuU6Q0h+G7(% z45{am@=5XMcV%$vM|DKLMUb+^28;}`tIlk)A2|SwVY1ona$d4wcRp1D%;wekf}_Qui%H^{hvH=P`7^FK{*@e|C4BCRN;?7ELk z9UsHQ{ry{5W8)B`rB2@ZgB!LV3>{o>LLf`g8X*6l7yYzVr(&HuN_(#Okmj$etqm+D zixYmC$vou*9ThIMl|f)Y$6MA;{mlYXa|GISAm`dIx&=*m7&+tDU{*t0)40x(!D$QI zSAq9=fh!&rcN{$jQEGvm;C6;5nKCVDb>t=KTGj|Zcj|vIbQ0p5BNkv?@--_z@Vh9x z!_vP$GTo}8zgd^F-K7;sRO;UF@=xz`(qlm(iHJuR>rcMov>WPqb+2aQDy^iVIxqy; zN!{C;Bi0wid-v+P05mj$<6`P4;z-<-CAGZhgDy~OF|XdK-5D7oA)d`|(qR0Jh4wLV zoi6+AUZ_aC_HffCEyHw^3?yk1<4>guoyJ-2@!TvI59Pdcy|{iCeLz|2Y!OoC?WTP& zx2`w$ZJ%<9J8GLW;^?ic1$f!qhZ}H>nuk~XpKkB6h)m~}>Ch@h6cDEg1 zJDc%$R&w_iDPLa6r;IGA5m%ox8mibV7wx*+dV1xkDJ!pkB*DG=m--}m4gD@#%|&v2 zT?a3n{+O!j3$we$tSYm5kXjEW(p|s0|Md+@QFn3o^pgY^R)r0%U7fkbyz-PaCDB4RuuEAg<7AbigG3t=9Vv zmSEg>4HNwshl>+cyBcN}TXe>PxCx<;oc}I&N5VY%HY-tRYAuAdhd+@olg*8WjkU0H(iCH0$Zqz}QbLq&F`vZKawAG$%&(&iDQ@fD{u_l`IEg^g({|)U!fb*P3_j7f8#jc#y`G1c}Wg%D;UZ zr`T6Uy=aPKuK|SepH^&SUZeLZ7yCZ?sSq%2dR(XoNiB7G$9kv+cCcyr{M@7pIbq}c zh-aoNz{H(W)BIi4vHk_7%b?Ict~!mG*n_)Sd=>|&sn@5pr7@qErU}MtDI2}>goMPr zkP1`p`j<$fe<_~d+UDS0XMf)jop7>R?elc=u{*s=>Gb(-vQ-^ zBfYTSELpx>)!9!rKbms;71Hx)(;LBa1N`Tp8fWOy5u6HVTOy4~B9TaCx-c{@@m<9Q zxd=Ij_1}mO1isKo&NF_}QuBXuxORMg$|V#sr_d(O#mM{(rP(sp`Ypa3CarHE`_Li( zWM5pU&du2JeheAwo6NU&{zp1e^`0Hv6do6Gu4b$T5YEuL_F&sbq%}Iy2Y7EX5@qIa zCvN^D?Dg5vkU;It>pAfv>Rs0YxEXC2_7r{K_6r^0*Hc=F#9c>p23~;WIEjZ@jk z(P0AWn0ET-$p`=JwpZ&{^4^aAZg&dl$#uORH_IJZiNjv1tl?6r53+00UW=3s;k(db05!6|*TP=Q9N!9b3{vQ&Q zZ(_)%4Bv^xp4|mE7Ie$il(=vG5&J`)_!Fc5A11P};OAFM@rLz{hexXontJ(?Xlddx zWCIk+sJ_zUcNY%?tuJ^ey>X62r!Tqh0Nf*S@JZVl-NV0I9u-FbM&Egsx$zQ$4h0g$yggo;%$o+MYI4WIwDD~A z4ULKG3#9K-8b#tlsV6;5T8tGDzRqu;{YEgwe zOK*8%?_a^gF!rNE0PP9CR$Bo=sb|}d3JShM?_i6?q3W;}hl!8uJ{aX#&-bBOVm@KC zl(f@;KiWDvpr0Vj_rie$YhTwWrmSg4MJQqGeUk6#^{yeBh&B=a-Eii>cN7fQ=ZA8Z z?;z^F;&tGs6wrzHHJL(QjM!R;l*gN{GC*S+$T7W{XPP?_S=%F?M&2Kr*GJP z>@;)~u)m!T_moW0^xmfK4*_*emdIiXJna+6KIFlv@VGNwoD)e=2a$DnU@Fw$$I%EF zh>vWW3=Qb)$mor_-uoLUaMxH|XF4y9*rZP5z4@jE@+}HxYJ;QAAUaTv1Lo>UyakAQK~#x(MJOCthZ@O?m`Jk7IA%QF3Q&`0d_K@Drc_h!`_M@|Lq*HUraP^r*U-l>&xgP)bDn zN60|4rf8=?X6v%Md@|#9M)5sfDP|Ay2VPJ(442a{Ghx5Ymm4$pRg0g%!LFZaRdPJm zP8^lDRJ*_gghSQBw8CF39)fL3|13x5tyFE4HOhTfT`Wh|n8i#I%_#cTh_fBaFduogw6~r9 zbkn3Q3Lc5N<8(`2(oAkQ=(rjS$4!-}?~|0LpL+IH907WNWKm|n`81M)1Y%XTDI!D} zi}>_pm1D~(+PPbEKT{<-k-iP5{{aV03jf$j3ZBn}(B-&Q*45h8lT(I_LK_x8XT&U? z72eat*4Y^w4iN)+RegRaA(eFeY_l>fx4s#(t(Kk=-({u;mLDp)dC@u32E3x#&@a_A z*a}BDqT~DnGZaiBd+WTOWD0+(k53kEHW{9%qV}c5^t*zDk#BC^_RHVBzh=ATXOEA^ zn=N&>I;)=20lyf%ec z@4`sXg+HU(=Nay9-!y$%*aT(8v5ld^Gw+aW!Vd^9>yGx^9Yuk!-W3+T9WYFqCxFjDjD~WV~pn?+WrDgKC8;; zTE8~)M+P1@yY_Cy-O^KH(0(6eQ_>wiP&bFj5;WK}*%u*rV{(QW{!&Uy0=zLryhZ41 z)5+q2r-TUvccQEPforY56R|-B>hx4Bww56EX^2H4F(dz`Y1|rqSfceS!Hti&L|i;P z*$m*}s99skawLdmZ2E6x5IBP;p$FG%`)5INsYUfGCX@(iXH(G<=&MkXw)|=Q;-~Rt zY1sJre70sj{ulJ~)NkqhF7EvdodsK%u-;xpx&Z?j!fEVBIg={GwOElKqMw;_7L=ByBEZP>UMkBtBCg{`dr~m|e(WK{x^{d?@n>`ts z6qh<2kJ5b~fFj0ZTXDbmLZhAbi-(Z_{4&c%6`dwDD%1sUgJkix<7ipo=%tTjp+h&e ztsgrQ{`%mgnO*#qSR9L4rw`VA|7UTrwA-hnZRcY6iBl(?gBo!;Hr}_&gZMnFKRbH( z1)+GmL@)Ly53}@AdFMuK23Pg0K}Cy=)BgF-9hpiE5O1;)x^kw#5^!Z-E+OI(ikbT1 zT3e#xL_|~oz-i^ml3b@frv-+1oVQ#90GgaT#yFIz-}T;r9-cBeIhdE9VWDxA{ld=x zoSas2rZx3-Zc;4;UXI1mJl*f}RG35;3p~8-;EgCBThv0?VI=BWS z?CyglVBAhlXynMdERA~8j4lO9LMSppC~(5hi~eBXM!8k-FMy?-+Bnbrx`4MudM z6RZP1Pci3B@72HlzZT{9cdpnQ?s~ZpW@jV5=s#h-?sI+f!KQv@i8>jRbK9gqD7Ed= z=#oEp+5mVOaGxRX{1{&JobyIc?NrfT4IXvIi&& z3?j5EXx1bNJ_v0ZQfCFM8#&(Y5lA%N-ZCM`!5c=YoQWdsK$icx#}B*`VeD5{Dxn;8k9q&Fnrm`8tZcfkSaovU zPv(ibQDa}ze}Hw9o1p;B5p1ve^CpwipBvs%k~Ch9g%Tf{s&BHm=WiyA4;8i)I0a43 zk(AEZOEVB7u}T<>IG?o>jjX?oR+UE$Tak0#?{{r&ZLF$bU>BXI^YXvN_hBAAmV8zs6Iy!KJR~*U+AB}R5vxMU7Y1YjKhspsB14CK z+l}nMynZRGTY3!UuBX(iai}2E#7-_6Zv^^Tq60-&JA&b1_`VcyKBgw?WWYze|V0ee24@eVNYWm^q&M{b)@@#2)!#mB{s z9nT?{_#o?!`549gt2T=QNj84mw4a7CJ6nBJmz;0pi)O7V2)TFBg83nUd1F(=&Ynd! z%pZn^sF@|oJ*Cs}3=R5CwS%R;eq&QgmrW_h4Jxk=#6`Z90@AsLUXe- z7&sdIot=J;S}=Nf7n*2djLeExpPfg4WG0qA^~ioLV>YuO-*yqqD;6Y^xe`w3q6*m% zJ6{)Y`MMZ^xo3*K>?3$W3F!b!>PmrKeys(lR_~j@_X>?ug48Wr!cfP}QOYNG&kVB$ zES>0xA%(>c&sQR5>oI2bHAV&2Nfgc^g+)@;^_x%8EvAw2#5{ilDWU!7ctEqlt8jsD zNHEGY^>!(EGn%&Q4yUn>%W#q@O({o5-xLebxAiLZLCx-p!fHC(#rM9#gpG{QN#LP~9q zQ|nd7f@0Q7k4Otcq@%$C{SuZ&l;ZZ{cuX6Agjg_UWTv}cnV7&H7pJ7 z^*e`13RYnF@o&23iE625AF+no$}lnn+?Yb`R9f|4)TPr>wrT*VvYaTA%qU$FKmqMB zEA}kVG~l0fxnMttjlQg`du%^z9|4$naoM-L1D1Yc+VG9 zn~uHDbNuH!J3WULpG?5A)1$bW`Q2dMQT`7OTrL9uYd`?=P^wI7i|lAu z0c}b>PA$$3;C2b8Y=^(Ot6vtqqj8@EbyRs3CF{Nl-Z~vNo2;Z8@MIoJ_1ZE(ZSA3^ zNT5BB;JJ`bmm@(96pgOSH8{g4{r&Cm^TLtZ6mI!x|055X=>Gt+y~Ko6k$f7TJX~r& zg^vJS`&6m{Ubp>yFpBwkE-u!#8AAl)3OYy$dR1y>jV4c=<~8erSEVrM0A}GM7UWw+ z$~?0ZMxml(zw-`xYsyAHST=nVh8=Lkzi!~APL{*?yMnkqEi9ldRkxsv3FSX$d{zh` z@HBenitSA!ryxYkUf*shgAS8|=sFq9LztsCi-wDTI4cXykgbA}Upv4KkVq3k*}S8@wBVxs^Bwgt2=%H9vaS0+ zfF+iS`)zkKUDq{37^rv}DV0l7+=G$Z3P7Z-KEI|gH}MSi&>9PY)y-G_NyH_M5CH6K zYcY-lz?>`vg8}Y+ruq)OZG(r$p4i}V3x_;o&;VHQE!GTRD{W}$?SM0~tmiB#rhp-R*A2tN5~=9eA6Wi-;bIn7FEHIlE58g#bRU+i{aXX0o>T+O z5V`PGf{SYFLrIYT7@!1HNM#t>X~?%?`sewnG*?|vMDMM@;g8q|nY$TEO2^cgJ&=Em z<}_1QFK#aLkm-Fw7$nde4S!5)^@D+`ZN%~q-cmHn=7 z<{ou;l5LpafrrYUz7M|uyI%QN9sOdi%ft%>95$$qZv%i~c4CPs)KcA}yEesG!0CZL zBgEv>M@+}*(EyrboH#zc+=4aBj)twHBj~+qlJljW7HzF(mS)X(@o0=0ukBdhxx~IBuA#qcWIOAh(Z|Mf+Itqc@}_p*L%otJ=LMRA z1L?>mgjpFIi?5umrqo?-I0l5bcMDbFU-AAvk*s3B< z`v5kl+gi5P=QY<}O#8V_`}uteqh>mJZ&u8G#TrZF)uj^Y#@aCHlPn>Ic0Nph)fbrf zjO?HW{rgO!_F86r_di=ZVm_j}VNF;V)k!-CnlZy4XFa?5&f95zao*$wY+moz4raem zO@J2R9kDQ082241RS{L~t{i&p85))COoX1oL4naq&$6<5)5`m$srlxsYMoz+XB^R} zNHG{;3;%_&@^*;~=ar}DkCtgi+g0UCJf^kO%Q`0MyLQqz`LcJ7BSZq7x;g>?rAav( zZZ`?4K+yW|6&tLeh;QUxe~6&sh0nvJ-lNL=-N?@mNfPhqY`_JL0&tf85f6DhZCglV zRq9r!@uO6kQ>s?g!u0jI8$?o3iIN|%x9M%@Lv2 zXIVdd`giOn>7Fr~$32vC20n>%3Heqlc?jWWrB9pzq4G2BWN%e)0hP_Exzo7Sy$g;) zpG&Xj(^9ap?G)e#EaOF1azmw;U;A=b{|QOqi98%Sq~DzQ;BoAK7fLkQh^!)uEYV`u z{{SPs7YGu7GdjGaYKb10Nj^beGOy;N7l|6hI!$w~;(KNLAbL=2raNLkyw10nQ*20pbDt z`3?wLB<5wGBmSb1V3v(h6Mhdu_sWks^W=^*26EJvCb0fLiBMCM@f*?IvyKvyI%~No z{o0}|odwH&e?U`Ahq=<8DE;Sl#}x_Gg2482kQ$ZwqiTQs6BTeM&DPB`4N)N{LY#S1z?5K(@?{SV8k}&6D!AO}h_Y9FJ6} zi%4+IaakS0)jSv73118* z9bH>%7X31TESjjvR&}>(9KK!6B&VN46zmwJa9w^2xA39!-OMOlWq+z7B6{kyEwuKG z>dMvs0P#P?+(09qOnBG7R)#Y5oVB@<(mvG1O-};Cywbc0deTRXydJvRNCrW6tB zzx0r<^Cx|(M1NhWmFchP*B*qi$W>1Iilq8+XXh6ei4oo{(7fx3SYzr^$`N_{{r-8x z-veF#cD*c*uB2It;T%KN04yt*MNxQ>;k6QfW76hS{b7f`gYr888tP;BkTdrt_FR*N z)$4AY*T;3az;^A#kssPPhziD83hWXcu6%%}ZQ$30Ijxp-`HuUtiG zOn8YGkJJbR?QJ~5`u{A9Ri8lDg~)&kTgS}`NTA*$i7n9K&EYuuQE!qJse$o#f|Let z$Nac4r%WL+l^tUTRcs(-RCQEm5XNzYBZ*7PDV1Ts*~=Cc99C=N0fE^*S+$yP0< zsKtNNP#9zplb7Jkt-UY07GxqZ?sRjBilAy7h#0P7o4VlWq&B4&i&0icj=ruGAAM6Dow6Ni zZPp@_3AakD^%kmNQS%D@2xT9OPUbh9(+H^@-MyJ0+CbZ*NSV}&KfqCa`zlm4_`hY@ zy3u77tUgqwy&+xj?CkSvgL|V1^mgj(gOuORL2Fw$2s1nHXbr?uwVf0@(lNriBneF* zu|413D~B@_gCb-V}y`IV0#cfDvbb6Qr;-GG+}Y@@Iq40kPP`Oj;* zA~-)yBo6iazTL`S&&TYEZHr9qjYe4JMzvT4kzCfm`f&B^n_7XBrEZkaScRc)9KNmf z5%^U_9;&`yObgU?!W@-=3bdC+X~?pamqS>p0|v01yJzceUSghf4&+NqJP$XFwdWri-=H?DrcQmV{89a#VeusfbWB>@b7plmSvDrDZy(e!wKy2ghqA} zRIj)ez9mpeG75wc{e3-GKsi?b4MzJ54^0wo;c^*lo%=E4)byr^H!MB#l$xc31hp9y zwfBZ|{e*&ZwpWr=c!}`_Ka%Ie#doZTQT|;$?nt=H=n_KXB6z_%m`_Y}>V9uhI@L1R zwnn=yWBxH&40WmOJg9++(;+lttIq5tgQ3R#K`ec`Rrv#GPUPa-K=Tt4q~cZMGkq%v zq3>_Uf6*i>>Rhz?a+U9q!Uy1y3+kv@ilgNiktVGUpsC1E5TuQkVsRcFTDegr^x3n2QX6z7<=f{R@$ zoVuUX?7K(X-jH^wn2vJ+fPYE~`&z}NAdI+u$r!9usP6ELnbI6j(UwhVUF-N$!Jp_c zwPVG=N=ID>cRL&{(SaEM@`V{KU_NJ4yK0#rDF#eeemo$pu0!Vh;?wo|M z7^oIju}st3wSx!ZxzRBxZV^jO+PqtA6cY=K-jm_Ni3mv04Z1;Fq|&ywF$MPWkI@ zn+(H@QJ$KR#;3+kW}id#0Vb2~Q#|VgVb%F!lvz_mY0V~4l3}IU@Ec<9kp+9k^WkA2 zg1x2|{Dm9Uz~WP!{a&37^QP~Y3h=we5P#2BY#I_+eD=JWS#$pTh+UX9r1TTMjzdB@ zYpRl#L3PfM=f>t_rbdqP^(tHYp_p5;Civ!32=}aK+v8|R%BZ*}?Qdmyx7;}$L4=#x zPSNYMHkNPUDGR?1g;lK|RuSvP7xcXxubacORcu|`WR8e=v4{{)zPT#%KP!ZVz}mW^ zpNazCEo}8ne7r=elFWx*{6efgk1LeMFVaok6Q8K_Nn@I8y`FHN{T7#eIBw*OYVLHz z`5#2;G1n}TFe2z>^QY8-%ZA3KL;YywbC5Fwr?2arsinYkF8rWB*zZxxY-od2y=ES= zgM*ek*K=GSznXo`{MT-pHm=Vpt51m(m2Bjd^RdkTrI-`)?N;hrjQ@%7c@QY_e|ZN# zuJ!*?H#gm`(9j~{bSxeFH-T(F&SI*0lv5-~Z$PmrvlWFq4)t%EUAGXpsVA->b2#Jn z;|{Be=;(&--Om1{UvJABZS}sd?QMqInb>~t1%f_vbCR0-JkTv$>Uyn3Nn+66-> z(ZH~QbO$leNfAROF;YgN&1DT5^AIY%o8fc`WbV%+juw>IVwr zIv^&w#Qn7){Irixfm+}G?(f`|ZR(YiorD<^B|{k_c8tGUSSOf*x|i)y)Li>-%iuC_ z!fPkYH~Nhx;h1`bOuB4&*!*arPAuA~W+!NTT}aF_-@Qz~oKr$pFy-LGhfZ4N=dWMe z`5VcHUdqK9G^+pzeOk9+;c_r-qff()m5;BlWU=NfGu+!vjz+XkeIbp5=`5-ZC*V5 zjXht7XjQW~tn7$!BRY&h1)r_C_A@+5O+PD|ZH*}r{|*1A9w}oOy{q;R^wjIi)Ei>p zlAWH>rzAU3L&$=!M5%-6Nyj`DWafVMwr*7S8sYeCFA0+9)shdw>`jY3=|@4u~CYHO{oV@Sd)(%6h48n?jf~bV{~6cl$^F_0bT;4oNbSa*j3x;b< zSRE-spv~RsbIvX(`7IZ{r})bVf03&nLMpR@%0?AnhD+0DTANp_|Dr6oTRJ^oK1`PF ziLfTGL*PW5Ssun{rZubPEgIPWILU1bFNyCIeIM6J1B4oO0c*%>tIvOuAl#~vwCb{X z%}Wfi{nvl5E67DPNbAmGmS3xagI&P5vsB?KG>|R+1|Z3|&-PLp3)x<#26c!KW9|jz zmD49v1i?GCHvoppvC6H!7sK593V`632g+EjV7rgF1W@pxlF>0{Z)s?Gpk)PD6$2~0 zy4NMy0>pBrB(-LpPgIY<)PasfkG~z9y;1({^Ja+V)oa0`B8WN40d{CBhz0pSP(ejB zK`kGldPPwhbJ6{gA>H>Fr?>dnWpo_}euK!_%D4wyE_pg^?jC%PPe@c;ZSz`)#QbZo zqB8TCFtIOi#-0MTb&XB=kzdDAm1roN421G?ha$-4nDaol$eaDp2Q{B-lug>zKp;kE z3adO^>oxgo{DfI=CI1&LqmD#!; zm2Ao^#m0Y6f_6QJ*e0s{=wFDx|fvC&o9%N`Aid{g*N;0tVSSH z+0n#Nci500tDlf$<9s|?qYIym>J(wWrHre|LyvXdZXa16u@a?%2m9)Y=Wgq)cGQAC z8b!aWcWwM~nqAbx{nL-nvvBLKpMq3}+pc#XZcszVekHx=UuMeY?3$Y6jL$2~!#V0J zT`d_skNOmv#21+!lTiBa$@u;XZDSYmMwe~wKQz) zCHtAH^5GU4F+FSzO}&>X0uR{8Ujpj4zQJ)m*bjE^pBF`Z3z=ykI;U;=`A&(ck6tR` zJ^%DH%WtIB)ac)?YLDhh=@L?Z42}C9ZwWkti+yo^jMKlCA^9!C8P(ZG;=uG)Zf#xJ zo6vxz`;R1N%ChT=+8lXAM$fl8dH($H^$1b)Qod==5n^XNToe(966eF+s&@3N3KoSW z*jXSKkbTQS9Tc{I9R=H8&lZNLGW>d*HD>VC`(0cJS9$Hkj^wc4-17_eQ|kT74`0%c zhqsiK-{SL$+U z-LAG8UNZ4lYpGeiUF+Kq(~lYBeyv=)Q5Y~LGZXrVeOZfF32AG0Jwl3R|1##sz0fd2 zOQ@ri$>FT6GO;bQAmd$cV9~u@vV!h{A>+y(7skg@4xxumTds9)kXjDL- zm$;;dcTwIDvwf%<`NQWkPfpzGN;uhmq_uW&t#$#xleCLJ`$-&GYIFTl4%c%y5SYeL)mv~9`GS{KR%d!()6f)Zyy(aww z$yXu8<&WIALP8R-vWoQZ3{5C@^xPR;)@Zan-}I+agy~W>CHDS>f(7cNLJXzPkZ@n! zkvtrq{KnhI+fLodJjhwiBaM+it6aRpD(WEVo*!o;x0aY_>k&}@d^aRsXGO9SA5VS% z$}n5Z*jj+6e!5*X0|a^C9ySGBDCSa?He8b}FkEhRTKhw2_kR2`FU zP`i+JD=8`E$=^;b8Ry8Qc!Rht|JVcU!Wi2zjs1M}^5WmVP6z%Wt9sLs{m71L_^76p zTuP}VgR#@nFII@d@0b||5+eQh1}{+VH(zCsIk|L@0ihU;Yj3+D#@w;RUibt9FGcaR zpwHs_WZlRGW%10_#@q*)uGT@b2IOG0!z4s*n+XO_httWlu>2ws{t`18X6KeYI9Hunej&f7g7 zv7!t}O|@n@Sf?21<#Z26D`z7;<0{r5WgQBNY=llR!%P0?Yly4*w}w11vcw(AMchBM z>1+}nNmS0ze!F%dF~UuELjT*6H+_gCh+M|NEz(v{arwV!I$GP{?&8hPjR;Tx2;F-&)c zcP2PY1~_nDm&&Rw&g?|+s{FBu4K1P}QEf;cxu5Ml@`unkWMO)F6-~H95F2dbU(-9Tu8bez&UtNivP?Pc=E~R6)boAPu@!FJa3? zjkL4b2kzskc)460$X&mK+~4j72K`2JsdJ9*&c;hT2@BQ~0@225bZ)tgZ$!GNam|;j z-*=-`>ba)uv+{NC+X|<{qMjdM^+i?ao(>{bk{@-clA!r2cD5gkC559Y?pP?!2)XP{ zMqG4&crd^$`)%PP4N?oM0r+ZuT@&8c8-)s5`B1?-LnzK2u0;dXc&)u8Wt28bDG!n^ zH&mb1f6NO=ywfFO<9uMxJ{wZNp>K>mac&k%lDfedJJTIIK!SAsp>{$6B)sg`m1Poi zG5^P;-G<}IZEQ0D6L*{8>zu2+n79ErSnP0D9g^836m7Tu)DcTfE1gl z@N#dTmE<`g*QSFyDb2i>Ao4PSBQFd0r#r5KXAy{$VbcCa1f~mOK;hHm^S14r3tYC+ z6hqS_GeF?K3l&oF4hH^|{{SKh*sCmAMxxd&9kGp^#p|U)1?K4 zvg4;$Lt^&T*a)LQZGq*_TKQ;6W)Bb)$~rG5TG8}uf70BHvWF{$2*-D|N^W5~&Z5^# zAy8%Rf|iixVa7?1K zdF4tg_6@3UY^dKL82E%UnAH6Th!{e-O5792S6(;fvlLH0FS+PdgjFnCRN9XeA@OGK zny5W;D~R<8;i1}skCeE9l3g|2KCP-hZKq;ejPb}^|KP^Wh+D$R|K8vq9AzOLErKk# z-}$)-eGT${Y4!W2e_lhK{Y0l}@!)BEjUOvzX=eDWMJ;30J6gz&ghh0pd0@@8k2&C+ z&EF?y{giw=QRi~a!_iIvlV0wKskNoOb-`FY^pKRh?XuzT@-nlpfJ8%|UEiwoYb%tY z(>*V<$e2biF5V?*jZaQ9=gL8m2(piTb%EKMsyX5DEd93n2Qr?~BumPl(N6nNbgcfW z!+TWL!X2%eZ;GX+(}(|7x98v3uJjcu^ixp(DYn&KPAQHJhM2o>wz@ieb@0qcPEnv| zzZNaA69DLD>xguha*HIjQ@euTFRxZb$V;44%)(MmA+TL`v^8q8 zz}ylGwU6uI>H2RkBwQcM=-2kxtp@aBWJOw4Jt>myTRxRYm< zda=x6qMMYbEyC;{_@1WMAP&S;F&7?I`j?n`dUgqR9JdecVQIKujT^EfpH4o(NYVcp zAl=e}NgX!sp(ktXK}6dY)}6l^Wh@ao-Jw}pqM5!ATtuAzD4Z~`w zg>#{Us6NWmo^SBZznS)XzR!pjzQ(b6xg4 zIz&ORUi0S~|KOk#b=sa_3gFU+Y+pWiV&JCn0#ykI1Y_U(eiK>-O@3V~<@!HOe^mLS zBld$?;4NG-_MV>0=;hIO`4Ehm&{zAXACq$bq9kNQ|GWiLs*dYz`&#|x?F^hIiL}|F zDCWk?IuD&W0e4ZNVZe0Mw{Le`+D8F~Bx5N>p5CE|v%miUA@;6q8B~qi78=Q?eX79$ z0ZQZAdyzWsm6%6iGUBQmB_z8R9D?k4ezf{odtiEVXZZ#kv80~0LWT~B6cu>?WXb;t zLo+f?&nAiQkGN7$#f%_E0^*@tY5tX`w~;H(rD+FiX~jsZddcVr(*bhSWm^5i*cDZQ zJf{aK;OO=$9`50FMRgkEe`Ohf3@6g6(nJVmlA_lkvXhGvt87znqUcZ)NUI7_D50ub z=uw>xFOHx0|63A-rJCch6y^x#p8>^4y5VdS$Q)Gz*jNI_)iiLeYHR{rDvfnk&b zj4xRPLLjE+r>g?8Ty*S{NSHys$v#jB_*iDg%h#4mR2^zo@H+!RBnB%RzFxc1@KTY8t~mY+5-R!vyj zpQ($GdTp0X>(<8tsTqOh1ruQ*b02=WB%7tLcBuSHX0+nxyk=}e=q}rd6zu-|T!vA# zMdiD<(t>s4OD@KuN=$^J+Gx$YCl;}a6Vql7rmS+*D;%X}40X2;n+EW5wf_N%Bi<#w z=#!|N7u!~1vU&OPyPl&sb0E7W$^mvg(p01r>Q0 z-J|zUjz>P`JhFEmds-zPtM*h6qhs#Ab2_{+MR%&|eEIoCIHwBNgYgG9bK0x2gB>+) zdx;qIgrAJbAt^BUyZZ6#f660c8syU)Y(0E76=?*X&BQ(4N|~fyGG2dc!_*fQh2|pP zQa|w50FOg(2d+hvZ0QPPIEE`h!owce;<{PGJR72?wDInluQ=TL_VW~`TQzORu!wfa zw5W8+fByzjy}swS@mOk_X^}HbNlWMvjqc_sA&NY@$#vL$n>8cCM z@I57V%-*gB;j8*1VVik7EtxzU2HdiZNkF1dw?WVw$g@J@>&Tn#+zxb91)@&o{xG59 zW0Eo-+o$BBVGr?p3rUv+-yuP?y&FvI)-7`3V1$IKP*_J;u$j;;oMKT{8cs1>^dnOE zQ*Uaefiwae9>AQUVfNqRhz@QauTEjN8WRjTk}tT*#R88iIa*Dx5<(HAc`SpdHLZyKPRitoPN2^mTXNsIUGdMmola+HD8#{yf^_L{Sa>{ zERAru)81ZOZ&ZdfTH4yr%EP@*3lr@&U#$RzAFk@#+u`ZD)*jY?ac@-WG0h=S`t)`u zpiHG@b=GM5=7sqo$(yC}QBkZ+K!#?A6BtlkKr3I3Rosz5sCJlv1tyZ(zwt`N%(yqp zRlPqlyl$x6lWOdaq<|3NzKcuOOF&g z$;q~7jl;dR^;)h2%wor#vV1(s@JWrYnbE-KU{lB)>&T)o;4pa|dI~*{`^SMF-$s^w z-Gq*BYm^5Uyuu&-y)JL`e}6K*4hb~un)hMaIc`M*`t;gHIC~4);)5{I16VuqvMg9j z$UmJ);_{$;RSFY55m>!!;-{E7+AQv4(x6q+$=XCNfOQ}#b8n}=W|?0lE&J&@`FI;bMk|djiLzOyFow7v)}Av#$#hDF zB4ARBAsx~V5M}w3h-f;ppTB@wUJ~xr-MeeCIKqGjoez5`uDN2yKeJaBD+3j`vpSet zzP3t%X}8np$^~?%@;B4;F9*mJ&T@;&#N(Rzp13}WeyH{Pqs*PZqEaGp-F;B{LzyIf&Mx zNv_+|W4NW5>?;g9yE$zgefse%l?ikJ?!SO2j^v0QXw|>@3FW0f8R#Uc2rnrWeR_3g z(36_vH%f0?i8F#JYp*bD?SSWLoBLKbO*|Qx_us?X`fmZ^tSRrP#-U_J#Cm3b69K5R zQOZ_<5r5+bxVifP9vJN$1jjpy6ssCD0oWTSV7zaW;H-L09Zu(gamyqC&-?q{&@E@T>L|_GsL`nd=`5+K)N#UjbD2= z36-tYujT*H9ZQ?S@2vI!6P-yB>LUZUU9-GJ^v2bCPAL=?z-l1RHfhl&00W`{L*%7) zzpSk1)L)nZtRGX1x7*A0&p{VI4YiAV@{$Gp9No2_G`%P!*403YWJ1bM~V$2HkR9AHhV$VMFB~bptv$^@xe3_RhPYy?c5@Wi>X$wQu9;XgVoThX* zUY1Er4d74uZQkA+d2ioZ>|`s9MZ-~+lP zxWv^B?PSE2$q+u>y^K8;pS*WWKj$rHlPhW)l2r(?5AAwkMO@V5kbCgj2Ia44>zj<8BwKuHWWV=qv zMKhB<0P8D^jXmj~rzHt_7Kg|=F?ip^Fw1=J4!bMKe(6cbede9xQHWfD^mOQ}YL)3s zUxA2L8l&oz-bcQcgw^A{7#XbD-5l$Uxd|=!&xLtk#+eK*`G$_#wvMf3Mjr=b>l+KR z7W@v>0iqUW6%}Pnpy}#k!sO&|8XxXvqO^pJ<#P*QSCfabe1M%(RASUc?QLG_5-J2R zsiDB(?-!dZzZgpOiLEdWkIAS=F@Tx-zHau36Dx4GceHz(><3Y*E^ETnVTcEP)U~t& z(kjcL%e`lifE#q7>nJ}py%P}bq$0+XL1d2eAYZ8lsA{wAf9I0ZT|YeENY)g=E4WbC z$sws?(>X4Fl&VBXRz2D|9_mx*FF?|U&!(m!9PDFJsz_q1U)e3# zcXD$Epz<(hCmn7uzsr`eyGp2JEIDKACFqXQTEJ`zx2S3RM$bP^B-%i)p!M;py$7CH zU|efyqN{zm!x#hJod(nNY`COJ=E7attPRpF9ot)7(8=%wxf&vYjOI18d%GKj>i>PY z58}o12}ztpGFVSnQobqB2`8flBt?KD4F$ksE}0O^BBSZe4VoLMmnJh*V6gXk;K0)i z^`mZXmjSD!xp_d{NsG2lM4P6d5JBiG3&BDoXr_=!L+OuVQ`N)wu#np7&Y8WYqya&C z_(Mx9^|*z~lye^DZW%RWI{1-O`en=AyB0=I`}+V65WOBMs>fsqFg|+g`d;IXj2|R$ z318i^#8N%H2J%5cS2?e~82&oq92$KVOK60Mib_j$!Uc;D0sy#@y6`5uHnGd}{9!)8 zpv}OkJ-Zc54}$E>P7lCF$@`Ylg+tjf<@-@od&!VGahWv1qwF!GZCP+Ch8A1O)v72G z0OsD;zW}q48{NcAwsZy5S(?P5r*V8RAr4K`zKm&*STF61dw7b90 zY@?V|oh?JR^IG+JW~YoLcTRRg{c#m1 z*lSX#?(%maK+6kPslJngP1O5H=omny4ITY7%$ODJ)CH0c?&eQa=7jkZw*2=z!Q&9X znV2I}W!O6%)VCzGwt)p~Q!jh$jzg$FP{McBGXNGxx_8bu)zaRZEw}A~Ul^IlZp2k& z1J&zBVX%-omWEL*aVy9^W|8+zY zXRDQ2?k|>K7~WtCP!Z;7Em2)Ct3IK;=psqbmTD@Nq}5WRIBG9{3kG2=+WNEa4+x9B z7y2rYqpoE5|Eyc{m0ksye<_|X>Yc#q)uZ$E@^UflsoL_fqfqlNdW08=)J1fxx2HkT zS|f7!({E_9Pxk#KP2nX?3oBh2rPq;2O59`q-`7@kIKA-d+p>JLpWb)e z3Gn2T5XY=pemEcxctaSZCX7Xng zyqGDRACxkMHCk_pv>kW$jWrR9Mep4;(M*0i#n86OGPcb7MT0v~tQz&Dyr{~~CVMBg z;CF!P@K)c3*nsEIgG`K`{r8syzOBtCI@%~n!TYNpK%~2op=en0$Onw^Gg8WRKCjxe z78~V`m!UNC9yjlXEcd&1KARk#i_f23UYKIO8f|!sbw~MY3BAiZ1^V8R4_qCBGO>hJ4c(>ylJ-sMfN#G!`kAacms*g-*&Lg<0}7FbhK9 zrPUB}(~);`dRaUL;d#c=m8LKW^)0cDptejku|Yw_S7$s43H$>NOl4!@(40IM$Z<>t-7F0ju%EE-3ocfO+$*@C%o?B1ENK2Y?JAJ z+X|JA!XY%WG?qy#H>KFoR0;VOJ8v;v8e{WZ8OLn(*BzE0zk9+lJ?JP|w{xVA_u;?^ ze3e4MOZOv%X?HRUy~*Pf?+mhjWX5JsT%-9BZVQHGapG32Io1;Hn|61U??#_HQedAl zxg+!qG$(JsO(OF6AP@D9&kdOMB=~QDlXhpPcF3FuwG-zmD`--#FjvpXwwf zrq{(!-!+*E`C3obmRZdA%J?9>)g$48y~zl<wfhjM{{$*)DB)6ve1 zOln_-lNXgGn0A+X)4&I-zp5nwytthwhE_R+J#pri)_z&PMHi69z_CqB`Z&+^7%gU{ z_3riOhlQAUqHb4&%umU*SE; zl9{M3)RdZADQ)E^O_bxGR#%V=aL2Lp*yio40FeGaoiwF>)+|+?o&1_xNm(!Xd?0~H z#0pu+L0m;gE@ zE)!|0Y(1=qU77o9%>3+TDSA@V8T{L`s07Rd=H$PH_qFRBj@+M^{9j*sSlaZQTzfdd zOLgC{^Y5>pQt7*Lwr^XMlel;cub!EWwVjY)OpSH$knyBgw0?xQb{i9Ytd7A_=s$Z$(5NU8Fa5kjW&(jZ@+#TG zc=_HW-1l~>z*lp;b!vux1(Yp-hI|(+p$m9&kcJ&M9}M1KW=883fY3=QqZiLKmFt8o z!TP09jF!&Jw?%E8ZKU}S!XMlZNyT!wn3RG@=9kqoXU4e4KSBQ;u4O_pguQ@3q?&*D zbEJYqp9g(8QN`H?jw>wv&-livZ{vsDgI{;3Zkr}klZ>e%;o&PQ-5;?`Z1dWqD{a6= zaeuoaV1z;vM==|hmivQ)RW;!SHd!g28~()A51+w46f8G&X)bGNZExQ%|5ASM0z*6` zGE&xu33B!XBz4=W+2Uz?0xrb>-1xbdk<@I_fy@^Q)>lX8r|XqDK$gC0FEMwT`$VQR z-$xJK@w?#RGCy$H!`2g?*{=35B~l{_4J0NeB>sNHPSa?PApn}WYC7E7qkEzE7ltEA z8JevRiQ{2_gbw(!Ux01!qiunT)CeL+FyrzK%TTW;OQV!5|m{n<$%W zXP{px{JpoBW2fGB6z)5)mQ}%@l+~;S#*M*v-kFuI^&zGCqW=vbf^;^SGj0?SVIEj} z7~{<(Vx(3}145JU!lB`rg>O0QhcqQV21Z73t8N=?VX1VlImPr+!lF6+aid&VmF^1e zmbP8(T?BW(o2EkJB{{}#@b)k7yBR$-eV1*NI_G#PTgx%Ptx1=p*96|Sg23t%k_tWD zrp-c&SaU;maR4t1$h@_1-=$NX^Rh$yZWn&muYNG^5kbONyh*Ffoorn@pRok#5vVm+L~jE3W^bORdCFtK(ty?11|p-I^Y ze(q@-zdn$)W5NymRA~d`m+SuYpb7LD7-H~b3`r?H-5CwCpXbIl86u?=P_&KQK!X3i zCW3O1nEiz&yGf*H<+D$xI-7e?jK4FVhsAY=@W4`)6k-5S5-%3$sBq0%t?B4Dwo3!K zwLwy{li>2Dq7_y^xF*7mkJjeM{0gRRzd0Ybl;}oW{WT28abL1sy^}v2NZ-A#Df7<7 zSZ*EV(0g(F*IN#BcVfaBZF)L{aJJSCFzj+F%<-Yp-#1<|HvxpoTC$-{%~khRg*pKn zqe?E#fupVYZ}M2^`=h2*8J(b{QidV(Jj(QN%L~`dzwZ?+SROm!Ol~_Bzt03<<05_? zY~VS_Dw)L{U!Sw+IL*Hval2!%GdM&nghC?WE`V8zUr&!m245jmngGn1<_(RB`fVOtzgn2c zlIQq|KQpf_2IWt4DbHwK%V_xnB3G2&D-{rozf(5aO$%R(4HgQX07m6(99ObdWO3J? zvEb!0`H-|r`~PzB5MrukNR_{%ySYZzzZothvl!4?!xRMjC*Q{aFyGip%-I_-FPvh~ zm}XYO84C_Jz7O%kO%;bkR|7P-Oc{JQ>dEEh6(rLLp6o=8hgO0eV0IUR`vp&~vcUnd z1!V7&A(U`bk!xu9`BP=-!DUNxjF<0d5nR(Rk-#Op62A2ap8O0jkbh#G_6=qdrh}u} zBleiDX#EojtRMd1m?#sM|2hXW;w*Wh618^}sD?nL@;IrAB%`?xeN5QNXzwTEX@rDi z-CV3mgjFgwuwoBdOP|#RfC5@Y7BJF2X<%~d5_uzRfl;@m+Ele~0q;^_UI!E=WXsWB zy|(^C)O`b%!Q$aHm}lwY_Q$h#`coij_$jn>jTij_-G@&-+(%FrE}JmbS3EFx5m*1f zdPFP(w#JIac)H$`@t36r z{Tc+bgsq^X{Vo9_S>?7%eJN&M1iaiz$SSBfu?X<5%n+&#YxV< zdO!=)r1#W|UN;N0!Z>+Tif%$&Cf?0YRfa`a)PTW&HVLqdtgT2}jS!}613L8KEw7Kd z(ScvO8bz!M?r27P18t(4^%w?|Uvx>ZHmRWa&K3w&qa;8lE!{6ckjDI(*o7~|1|cs* zL#l1OcXrRk>10mHeI)+*w~Z z%l~5byisFtkkwY79`Y@kg)79U^@D{u_jRn%&26ea=I&Dw^~QM1!)VUxpkGK zNV%b{qI@Ua$fNrodk2dQH*`J_A-g`;SCWZm%xX`SGNz}fgd!MYazDXRFX3Tzs5=XI z%X3<1ffO+o&a14<$zD-qF-oS$Rdofg0#6c@uL*mdP4sORyk2uO&CHGwGrdxK13Dxa zmRW?FGp*jjP%$!aeXPsI{uHP9VT=Q8#TGpgALy3Iu}U5`P7>0`4wwrIiOh}WXag_0Oja@^Q!YpC(m+o%U7t4bs{L!i5=MlZqm{4y{rFeZ6 zsG@4E+JJxl2*sl2_S)CU!&l!kYLRj6yqXEt(yhtj)jLxN))#3Ggf{RqnFUSAqARvh zDuakr*h@GJ4LXQvj3@S%&7Q;0awr-H$Nl~?P1;RKu$4tLGa3?THmbm-6piRcNe%^` z8?Xtog~+EsOfG+BG^@9x>-@MNbS*XRso#K+H)AZ2PHwPF91tm%rJ=Bvcr7|ld7_|k z-V8PPXVoS(#^hA#@i;1V(?sPP5`SFixW3n?#5YOP`W>$iKhDR+`#Q)T!($wiql|}C zPxHHv&>D`y-R|LQS*yR^(VI5j@w;Z4mslXTM@?_0lYt1G4k9E z(?-R6H%JEKVkuI(qa87cTz@i|i@(tElbM*{kQ!GtkFFWKv@K`o40_aAXYXcvrMRD{ z+6kNkX#&^u=4_lRpFSOv>fn$sTTS0Ymc<3*FuojZEYh+&YYAu*}l?wVxDs8%@|Q|EO4sd zRR2nKqF=!%DmAAQA~W|^QuiZoq1EmDsO(*{D3yr%G9KMo10?!&{D|K);TRX$1 ze0gnzUm4-#iH;yA+Al`(;}7Jp2Wx?4Ibz1*uX%jD7*kJpl z%5&c})G|nBi=63Y+g*KM;{oM};H^ab{a0{#7W;{y1<-O+~F zica12NgTaU8Ojt4+B?joRw1)p5_i%q6_S{(0AGT7tNp+D#Jt#mw8;fD^T=_W$$*0 za?HxtZ(QD`tge&lSAco=z-UVOm#kId1zh%Qui`4Y(Q(=8Kv&*Mys^||2Jr>LEZR1D zB`eT~@f~#_G*=?}d>`$9Rm20|gYW@_W+ab~dmwl^`6u_3VbmR8BiuJCO0ujw+;^$~^S6^+~fXhpErf)oc--zR*ZK zy2LYD@G7ll>nf`j%Xj5Ga|Gpr<>J<^Q_PI2@t&A!`&j>g*_&RMXoJYfK}Zn{Vx_o* zHU%05*isxtl9kxV=R-CymuFbfs?%8}+JId0t@(H*7wZmT%a$$>aJ|wIZfpNk$h+i( z71C)*{z^vAj5up(%(RzGocrh>@V1*_`iiSA6+{t)mHI5ZERp4r$tM#^IKX{T#gWV< zg5a~)+p?83VV+nax8TXLEKh}D^0tW-XlV8C;77#1(&Q%P7}bnsOd~gdcLN(&0x)sP zmAH#@o>dNsTERFJ&Vu`9SWt=3TeW&%WnMQTwMU`**9Q_9L=TfD*_*Cqhw?Oj8P3Lm z^m{}vWpBOa`gVJt1#y}Jgq8pY#oF$mg&%$voo~F>TeoMa{^4PhVIe-X5@JDc$sxVr zbUFvJJ&N%01DV8YR5)*%Z`*s}l+y=ek_BT82M{WTwS%VE6sF-c<5d8eD<;gj1^niLhG^ti6zpW$$u?u87x|vgdU%!(U zet`RK#NQr%dD-D*84zdUBZaG7A4~uZe-jigbkOmtLG!g2^9`UznXe+ybQIRs3%C5* z!e*8~kHI(g@VW)8+CcW4GnQ&RRub&d`_GkGP?=zBR4-NjYNK^`2|)Lj(ad4Q@TyR?f9B;BVLb{hgmU0HaN9QWQDbft!lyJPGl!Fn_f@v(LfHa7jO(X z64s?#QcRT*tjQs&3o%i!Kw70%w~BsflP!zMp(0t#frz47$HUt5e~zDy5hUZ+7$ycF z^BOZtFubcy13TCRKJ1O1d&ig_(q{gr6raabC_-7I;dq3C2Fbw{^V1m$Ys~5ed+iNP zFpki{0Tk(Wx(c%f-a#A%jBSenlyw3;w(so@W*0ZWB}$2u6&??UObUr#>H!onkLle- z!5~xrB4D-ww!nGn@T5!G3AWvbM}aF#st2x5zZ72F{C&XX!{6C(3vG=i@|2+AWk6}V z^mDSU2-omV;#;tV664-VL<7&&K%o`Db_U4Djf_orwG300mW_5`Eqvh!?_^&g$FC?P z?_CYY;}l~OD1CKN(KSI|Blmt*hf9N$@cR)C7#f#OwH`tB0A&a1bV%1z=m=2AU=Sf1 z+S-~GFt`a(i3@w740Ad4j{^`WfZx8p2@3Wd_rE<_`o+gTE=4*qr}AXjZ-%MPrvE{ovI^26kH6_LZE!J?Z^-oZ($(Og z3vJZh8>!&z;Q4o3C_hM(t=;sjp;iJm=_LJU9u@%mg^rgE2GRm8)Pt9w;XsaPnyLQk5n(q@sk zHJr6+hug^7;DfBqLsj<7Q=P1EJ&Q0$iE6MCt-$PM0$;)|v&yf9<~3^&`nDTpA^3@_ zP*YZvh_cgyg|@_gL0?wB7+2QRJH#g1cqD#_yE3+b(S$AE1^egDcJ}YJfD%NPTyY*nH%xl>KF zkJY>JxGklsSc*dY&CkWd^$%JvlLB|AfkDv6Ubx~sk>=R2U}u0<ci#bUqm0%Syi9+rA1rVLH8v5t23VP4WrXqgixNm5 ziMQN{?#xp>sBzs#hV67^3f~Hi=!|@o*NN%YNn)v&-xsw$dn@8QHzLM7^6Dy(D$rxZ z$f$3jpY!7PhR@5pi>1NDXsUN-*src7D7Sv8XMHK<07bJY@X?dIKc4!+sR}4Bq_>g3GzuByR-HS`*%n3crv1 zF7fBB;9q0D8eCZt2?qwTxl(CKEhPX?N}@INzV(Nh6QdA@PS+Pyw#FzgLQV(%)0EC5F7CeX#?;&UEEB9wW!zN3|0bK#m5hcm<9rmMZg`m4gdRt zmNE^USgcH@GXcY`PDMOXkF+~mc##F{Wtsww|YcCvnWr8e;p2$$IYcZ(E&O`WcPDs$a!*+=gf<8`TE*{1yFB>g_ zH`mgiFmxJX_Y`5E36D#_KkONF9W$T{%QjfraRj;427t9-1`M{bdvwPJUeszo06=E- z3C7^ui@#L*wY7z>S0%QFAq{Nx=y}U>Jq#tAG@IB1XAf?$X&+dufUc-(pMPr0tg+~F zPgxIqVymti%%#c=MiqTg1mG?l7y$&V@c0lKlgDG;7Hg!6`w4PBc$dgmnukL4tpWC5 zcxebd$2U)WmJ3=S0}PQi$x_@Uxk3u?0RRwY5@f42Q4>UNw%#KMft%%D4wYtribyWa zC;Arz0Iw$k(QC>=TL3UEBhz@iS?Nu~?gUAk2R#`S^EOE}|A4cZyjUFP|7^AMde^o0Ek@Lf%xU_4HVKSAw+ZBS_k*TYR43NkR| z9LP45T=b4nhncOQRakC3aKzPLW!(1>2@pmqFgY-zwLzvZJ(BP^FUzkmt`Pv4?0X^& zHsyfv#y>#qsf!1Y`r`5-4sTIVjo$4VG<0jdCT*`mql~qe;Q%`s9xUMhKat#ONWKiU zdQp|auRh{_LV^wzBEUfd?)0zQrxa)VNcJL`r$)7ODIml@{yaIBPu|tjJOd3Am(9%+ zJ7<0Qcw{-1jSbae@2HBAn9*;|XJCd2GSLB5LO|lsGSrIBVP4Ic-j0g?wExLuq9|~1-jkIXRY@HE~H#M<@>z~`YzTb3St@p3recUcOWZ=k0 zFo|g52^fcDU#63h#mmSwxsEMApU+T1I_Q~9IhxU*_QeaA( z*`3hL(4A)2TJu!v*&OF?o0kR+H;bn+T6M?(We)IWd#auMog;d3=C=0k3<=%Af6GJub!AKgE3=P`~p zNv+Yq=n|CY(e@mN$NEXYH4pIMW1=ONLR#VBTwOlMGOzIRs)a8?=S$d)#J;|TG_{K2RYW<|9K zJ^?wWp513N5sSxy4J;=rKD|ybnV!*pou8VKQ~HT=1@%y)q^ zE^#&WKz%|}PHO>OdD?m-1#v2(7d&FM2l#H$bGzaMtzvpgLASnUFW(#atS4KBAO8SM zgMu>(*vC@hi&oP_vn7(l-?j}(M*#x)U!!#RD9R#UAJRvOt8oh7`Pw7b?IaldsTPV}drzt`Dy2n$*a0hjKEKn?{l2wrM!fyHLp@cnZM&A!;_-h+ zZ0lf8bddcSHovzSIAZm(EXx|^$g4+CC2Wx%TkeAk-9IvJx#MBqsNmV9=CXggBu+kY z>Ll)T7EMjedG5K7y&t*e{Re!ylN`a$lg$4IObmg(dZyw&Z1lst^&lcW2QX1(D|Ike zd=hVtBQ^}C$iNwcPd$t+BKhR@pCR+k`0UjU0S z+M60&lC`d5L$BETZ!ZOB|A6XCp@d!2VDsF6K)7CIKkC#)n79>yb(1a#Z|4jD)W`5F+hyr+#hhD^Fy8HUOxe>4`Wb|y zN;brdVya(w_waQx-wr%I1>oOB?kWBQqF>4+&!}^PguefIx&$Zc8OlH4r}vrbU~mJB z93MQ}pw<2Zo?Wqrsk0?N{{ytC?_|%r{z(2QxHu0!$Y;nWA{Mml>Qy+Id)R((f7yt} zvRAeurFeCW4l;sDY_uYlfvF0^uU4Xo&U!=kPAWAO#~o?T(y&VNhplKO$LaK0<$I{6 zGapKP8y3LoQr8LAZ2-F084ar&(okuxUL`~ClrC(iO^CAlt`#I*N#^aP*$8NnN>%^% zPQ*0^b|op?dH|glZ20>oM(-K*mFIV z6*RuO_$a}{pvV>gm4T7J$;r-zd2WpwEY|K2_;pRB+fw&fvG!=e8%tgjlCWKjm4!$_ zY{w!CNH_l&^xsmX!2F^lFI{nM+WT}KH!o6KQad_FZh|8u>K33z_bJ>vUDZ?}btbcQ zuA4{&s`Hiwt;F}VfgB_zzS6j8q|_W?b698|iofl-2GyVB3a1%z9Z6)|Z6(6dLT_g``)mA?XQex3?c65q)wEAK$<&%5m7XXhg!F;5W-ia0LB%XeE^eC#K+GfQHE@6;cfOiTvGM*_rc{?{ZrVeSW)_Ry5fIFoWTE< z)o@yrH#dV`{H$TL?1iE?(_^gHX{3!U&lE${8=n%*Yk1Z&QQS z?=HuAsI>8hzHhREW#wSN^Qzavh%n&G(hn~j2sw!1T|urKA2uO;QeS-;D*fJ*)Y}rHmzTc40?)`ksalflD-9=mp3ttvnkF{_b^1Zic=;t5d>0>Clh=uq_Ni5%MW6?l)tAjDJ7KXy=VMPEzr7bZ53h2VVl0# zKh8yxBt^7ANLAyOp^2;U)S)*iE-m!Tn+xPCzuTjvTh#V&nY!((M?sT;vjYH}+RjtZ zkitK!Yg$L#TGkD$lKfvNQ zc6(j)#B1ZXNcp zV#a@;q|A5l=j}VE;FA;t>q8s1{vtd_2{*kCqa6((KO3*>uHlK{z9GvKK$IDoyy}xr zS6;gr`4Djr>-}zCm8dxL2gXpB(WI~3IuT=WDd9Q^oK)C{A0|Xg%jCU2 z7x0CerUo=6zATwHjvP0#U5Jchqg@14OQZcj9xdc2MW9xk3%!^DALJNMjgTL{d5u4+VFY$!R=UFYrz)#LdGW+{zlLKrog}Ba#HDO@Bb-nZ6|n z7WIQ-jJBNy-x?^^Fi(|nfli+f^n<&>EZR@J}u#B~70kGm?8N(Gq z4OPig0y``qoOngAxH;ApPTI-h!5?Q21&Kn?L&!>yO~7^xX*+t_eg03^wo*(yT;H|L4>E4SjL}s0ehZev${n0StpYui zrR9k|kTjF`aP4@tOXXr*n=0b~0Y{;QgR2p-G8@LR?|y9djy5sM&C=>+T52)Ts3)H~ zMI~e$){EHND{)`w(vt`%7KGZ?Ua}w{FxELRfrrio36 zJr+#|&tj+@P0`MjI*9Dk;LPb7HAIym5@6a{%k}OB*j7Q{O)%Gn+l4SdUyC!E3{&nD z`~vGnfzB@~*#XOmS=hffzOLW7BYdhkC4twJ*WO1ecH8HT6Akw?fDRaP`(thSv^JlF zztZ$X(i&1P5L^_OTC?E4H9!7=cE>urEIpJ4`UQ4A^e#+J&9uXY8FMFbxTR3D-;hJ$xw4SaSnL4SV;X z!!S~6#+1Ybt8FC);dRVvhx~X|UJY;~xz}lYXAA*yZn#+iC9mjsThdnMG1|UM`off=23!SX3h}s#@O0QLtg4Fso0AQ9O zWMB)&Yz%4Zt3Z;{c1recsPD#<;**ZlxR+fcx>&QAw+U54jJ=dkTMiADZhb=*7L@@p z$hT;7OBr0I2FpiT@JQx`RZlBBZde8$lA*mZjiY*#!@nm^%R!h*2+6?wOoqIFM7@_K53DB{?rq(o zxtFqPLDqW~>Sf8gX3eVKBl1LpIr#ybdHbeIZK!ZX>FY@djMG_D2-xuW4 z0Im4ISG~CT$Bo3Y&VHlUD?(F=tl!%BeB?&`i`@t{9FdyY)q|Dl-nfiV%E;hp3`4oJ zY;ysVMEqXkvl56wy|wjGy>O8ND(wofMuH;J;u>amp|w$wCsjE&>il7&A}z{hVH+_u z?e#6)^5Tib}W-ZxE4pnML>iB>mluL z)R2jtIhyLU0j`4I#6+w zdk4O?u6v$2IcvS(6vazZl6!@|76z|AR#XN_wYF=%8#1hPCAW-zG|(D*nhiJmB5q4w6dlKmf}<2M_W|GHcpiIW*S@_ z;`x~)$Y*|=>>F%gSnxv1qseDjG)UfEJN|I!65PUGWN$FqUcOz^-^ljSb~2^#A|%Ft zrI^jf7^~}Hn?4~SouwOf!8ef86eAvy!HH0(AeIgwmH#{B@3^-1p?@OOkhhWx;~lOI zXR4n5=*%!HyYpJMGi_}-IXfUY&)M9H8Q^6#p-512-i<+JnW?kWsG7aAkR3;WRichm zl7mC^RH1(oGV%Vpf9K%G zXW#u>>`xI)XSXaoI;h)og4uF8*9q@WtDUy*-F_2zT{3G^UrRPcKWUx^Zsdt9z5IOS zu+=g zqM&FVNcg-#tbQOL=vh9Yli~ua01K7_cbwNh9PP$WydkOFNvW;;rNDkSTRvel#Pq8p zu8UUfw;Q(YI*kn#)>$0x6d|9onjx50C{~^rl<`05b+riC|FW*c$!D|+{H$T2VzYiWdefbdkoECh!BT0I9`ncWq(l5Dc zBitg%E=hm{NWLavRE|;MnXS-w|II^?!s)JXb8w4Hy~?Tik}5qzWiZXo;mKEZP5AZ^a$mQ1fzZJw#?b*89pBm;A`%!QEjEsJ{iP4sp5Q&eBtk#l2|coN zpDe#uxGVC(BgeuGBiR4b{OCQLf?5}z)qtivD-ExTYu$j{AdF$jBIlo?<7m#)HO5?C z9{dLGQ+Px?d&Q?&h-fSxH@Nao!>PqZw)Rv92vm)=ahyNJpICaVoj`4v#>kX?7Rqi9 zMGLHh>)>VfBubeWS=)fWSigqqN%}AOoeSfITHj{-933rP^|HLvTgn{OH(Yev1ML7Q zN;W>r`5C=%(}Wq6Y$WRgLHWxp?t>|W(=Ryy++zx8BF_j^X{F>ZFuv~Dak=R)v9`)k z(|U#I!afhbfhxRGnW81nRa+&`)w|;VVxS9Yp;Y!}od@bMP)+-0v9Fo&)6`5-ivHk` zONlyf{2tutkM?Nk<-t2bG*r}VHL=?HJVa#tYm?WZc{wj0dL^k2QtXW}(dm+|i8wki zhd^bvo2-Ap%qk`M`@3QS`K~D94VZO3jD*;_BlpJ?2V>rZBfPtt3rM&xES{@j8|3%a zuV>_5wjlgsMD|x_RZhq$#2p>tZ|(fCumq1krE@SR4SrJ7=~;nDX>dQ4AcaE;-LO@= zHTP1SCmPI>sEWbM_2*s3`s%EiJfvF2u%y@{eI#=rf_%w`1+z*#pLdYTsyDV7mlYP) zUd1MwzmK2C!>`q;DONA12_6201aABL`YH})uGA8?{~w8}(N-6GTze03aZ9F>W@Cwh zK)6KFA*Kl3@Ov_p)a4>Yeb5*YNK+;?_?CxsRLFyLv$l;uuxK%PUKDG;$FdgaPWHtL z!6>G!oZ+kch9KWaVU0$EYJ#V-qFF|q@G8pE_@ql{J*(avnPQ76c;+#iu6Gn;V| z^MMVQv*cI!-%rM~BP5fMt^wgTQDamK8mUE2Pv;K}rm$5KxA&Kjot^~;4Ac1g z`g4z{`Cfbmm>`*{qd3W>Q8c$CB&e;Qv$h4NIeV7K;4LVJGa3`x0GD)V6JO}2TTqr0 z!(c0qk`lu&zptv0cu2eMJrO=YZ2IhP9Dvj?CHE$dVvT3DH>~*`J8!ZW23+j6ipskDBn_J z1-bsmzvlPqMsJh)>H9f0U)hTzN0U+T>4Co1Gd%_wXjl>fjh9 za<6Xu-B-)?*1K$Y*MuJL=T<-RN8)BQY6#DBlrS^gPu*pFZN^_Q~q}qI`%tLt%W72;SS6w>kW4;$Qe@ zYWzt&c0_g$yAC{`HPE*)oV461`aSnA(9m_$Yi|P)CBAeG=rvd$G`ImTsIz(B)WzrFdR{Wf~BEcu^4*wB$=)1%jdea4`V2Ag5jLgm|vIbK$lRi?vBW3@}y1^3n1>z0stwu%70gn0G{d@YOm7` zGLB1@PY=MZd0lI|-24u=D`dAX-2eq!#nBF$LJC~&@&^(C{XITathFtAH|RjBdy(>C zJUQl2zcU1Io5lRz=m(mb1Q`G>URk@m8O(v?u|zEc7=VAZL_MKFE|FUJ^3?|Dx2HOW z2rAHG-B>9diVL++AVy^TE!#m)Z(k;V@Pl>pg4wV>B+w6qA}(SnPy9tsO_dh$dm+T~ zGVcb~0eb5CBPtV;xecUWbFF&)QnU_gD|S_?O70kM z3k{2As49yRd)2=wnrKygX89Yq(NB&JvrIWjH6Cxhy@6`Ta!6ZO?er$jkMCgR zP~~JwErdwqs-efa{UAcNP7!{|_ISDk>exJ=rzdIlXiVmUsmkgYk*GQ66pOXqa&p~k zuO*b3A;5d-8`5)*0U$XUD&G=;ghZkC_^hiX3sI1bisy|}hp!bI+I{yC8bRby*>Q|T zf*?LAXEYHAItky@ya}oE7I`#Utsh1BmifUSZ_EkK&Ep@CJPe(IVAy|&lHzg$A)$O- zP!xzE)xhE%z?fX;>dnLUOZ0;<;G7-BbgWWg8V!HZ3mL7ciqiach*?2-`W|HHIo=>t z#jg>;cCV-daDfeNZIu!0MzHc7a%@uX55LRYF8&r4zz#vN#gnU%BL9oDcO&YD*$`32 z`K?fo!4Rg*aJo?jkL?pQxg>#dw#^sLu~2JyYREfsaXM(e!L9xB=f+(!@`WepS!Clb zc*R;ILW1FGo&>lKrVhTSnFWt##LWMIse|)p(mm>z{DJ2T?4`Ju#!LNwfC~6Ht3T+u zv=aN0_~+F<|9@EMPu;i&XvZ)dolw8$hUb>xQt7ndL3xjPfgk<9fQ_$p>sbI}}TDKYn4%MsTvlP>Uu9crfrqxwF!Ql3TASdraVzBdYE83_|H=M>d`sGFZZqrCGjw z2UoQ)&}U<-iV!zirSDCq)O@-W3?exelV=6dE9@YlIY(BOO$-6UX6j#H3v zv!ekWOhyb2GV~3mIh$UlGIs!tK)OYUtMsofcOZ$8kB3PREg0`u)!%5s8F=l~!H!iS z)xH7zHV7eeog{>c-Xeh`1+CZTsa}H>VWjMEQK(%!_|yWTw5vgrtCum0RUHcmK7fiG zub#}NgM^hn36y!3*_s^~#6zxN;~5?1>A$HAj z5wr}Th4NaIIt!XADigcZLJ=9E(@1IP{t3kZ(htgLWrZ@bOl{7w?q6qs5v?hiv(&zF zODa_3H+!9T1G7eRn%BJ4#<3)az2Q}J1kV4m^0TJcN)k%yO7;<3d)_+pTL=y^}ljeI@^(W5oNK5s~tIipBaO6lVMy> zr0|VGTlWzyW}69_wBHqc#&(Cx zl?4%(Pg{eHF>hd`%|+UFMO>(Po_>0{+cb3-^GbA!{9zUl>QwWE+N;g6)Erz{0KfJfet$-$SwR$MBf!+M6 zRKDBlJ?o_Qv_7YL5-I(xO4WwN+qJ*`7@QYu$*P=K4m!*~99bz>baV2EY~AB)pw*VU z<}9MuLxsa^jep4qlgG04bw*crtc&EPQ6<%$gE9mMezMsQ-wBEdC6%%XcG4n<|aE(iKwOHQpxv|*@#|I4vwU`T3NKVme$YE3%w(rI@H*^el^~jzABfktMbBv zK-HOx-Ewz4fG(H1X^ySyP-kYlslZ`{+c1xnr?+N2zx^&DYG{0FU4F=OXFsl=^`J_X z;kRtHyMO*K5i2f1uL1h|^pS+N{KTkByD9VEe5;lGlYrKEc&x%0s9uCrPg`zBBX@B^$TIgn>*PCIs?1L}-yIL1gCso5E zWlMA~S>N*_?fiRgY^ySwdS6KM9mo{fbxO0PnALP9+Q@xIF1`+Wo7o)(|M!^tVW>RF z<4AJrA7BgL>Mn$|_uY4JKHzn_JngGC0em_~f@u@v><}75>3ya4#`sZ^Gcn{okwhNv zy9cT~$@jPXUkx_+0_uo&^$xgOb?&tet-NfRPuIhaaQFjyteFKrKrG3?~pws zdH*MFTPNdlL7QM~Xd707z!ZD(FUI8Lb=rEz%1Hg>gb9!0(co}6>NT;6ZA$KeujnIN zX4nLcQ-Zl=OEwp30XvThi{B+T2Na*F_x=IY``!3=g4_saB#Z@gE<$P}jlOoBJx|)p-Ap>OI*_Wt$%?;C1ggHm>DM?gDe!&TDyb zd0M&q(_gb9QXuNp{vX}j*BE@~_bANe0v6EnnE4O*|LG6MPi9~n>c^fH&^03=yDuU7 za(5P7pLmmv#hEz%a|kps{6ICzk0}`;G$~`{*w^q4q2DFCSbnsAR0$|Jpp4E;lgj*w z>ypOD_2sbj_9&bq3#I^>N9B!=5_gM#6SS?xnN&g4#3Q;AsjpInWWA1J%0o z65k+&$e_Y;IB)RM-Eek&?Sej?kqo|q5R``BOC^c8D*VC*F+#4=IYVKrq9$JS08-6} zRt8${i4~~5@7VPzt-T`B@W(L%EziKFcM`qlwuA=BeT@+EQv}4Bd8x95(HpKKtED&Z zj6|3sB8h3IzJY#X?yFfAS(C<&E)reM&M)<{-8JLu{{5*ch5=eFCE7e6#%OWq>DAya zk`}|av~N+)NcGCIJ}}%gdn=t+jW{}<6M50(Uj0R$Q5Q;+;}2oN5LcwZ55v(FH)n^* zbz_y$^%#md%kY{@j4@**-;#6jj>@)`$(K*3L9)l3BN9Q&15PaS`%^fz1I zUhXXbScE4p_yPkMtsMzI@eWN*OTi7pyj!+m6~Z=?R)!3`J;E74W9sT)^z0J}}klufj=YeI!03P_C7qzI&5eaWni&dyp zZ|R7J`fJSkQLLiPL-bPbjfs8Cj%Oak$d<4&9^MB%3Z1%xW;1G7dy*&Q5_ zp#W}ob6eN5I~rnxE`Zb0{0Kt3vy{k7hm(+(5MOCG;H1Z;vdj|e-+h=DcOM@Ii8m`n z2ERRDOGl*hv9fjb_FV{6G5oj>IIy?IQ2u3D8{ExMKqFwBHhdmUXKTS(a@Df8{q}aa zT{A;(N+v0(+r~|v2-%t6lhg*479|YCX9${Qwy^A4jX@ndQo`I_J<`D}g3MR+dc@i` z5kG3xO=_vDOAei{S%);a4}O6>f_7mlaSC4rx?3rU3EuS45L;I5sy&dC$MK+A2#9Z^ zsjMw~iMqr=xZoU3a|4e%jR6$Uaq`P6y6-g_I^E6b`-4r2tO50RrMTfRy+y-I( zXJL1t*bg5L!ksvy<(H^S^`L^WARRNp@N(9nZFAS9L-o(KT*h0?q$+F`FSVEDnzI37 z7jgmk<&N58$T31soT;O@ap-f(8g7xRdSo`>NjE+TEYKH8sDcYi7Et`|0zXbKm#n=*=>i8o!HXQlK_^ z9{!Mu4TKmo!RIqkms7Xv!wzmgg?i8)pvsTj;eQ)LAvs!EUABCVF=d&n#S4xSR<&0f z5df|j$Q-A-RpskE90K7_$eH@64yJkGP0VltMNq5}VuZvegqiDBlsw^XhYhCU(pI3j zh}iTdx@r9WT<_fY&2<$ioZ77D`&{Qd6-s6Ef1n$`KMKjaSwK0_`ov7h6)TYZ)xwR1 zgf98|i=L5E^QDupOqJt?dv#c9YGzgLc(#$SiekvyGpS^*){kIfz1Wj2&vw)jU2g=5 z5_AodUP}gNNE`6WSHKG^J!uFjW1*=j*tMW8=SK2%e#KBZyAHNslrb~pN~RVWX);&A zgo9-H_*5K|?=H_I-b#Vu3T;C+wywSyRo_04E6v)sZLH$h#7T4^2F4o1Wh5Ji(dhKV zVSzmomm<4&Gv%Z<)p~h&VyoE5KBTkK%u_zm%K`bli(m-SDXdM4!Tc_QDX%?dFpR-K zD{E1f4Y06Yqz+jgA;9jE$MN8N-m&~sftvnfjZF7@fYuN6MPS;Ha_we>p{ZX|<|m>k zt_aiE$Jg{wf{}4p78m`5aGJ2$eIYmOZM6@NcTog@sZ5GG;LV^L`uiNURyM26)75DQd8Z|!=alk#CBH8 zo#w}Uy%x7>6Xa5AWAn%De69WLHEnDMdrehiK4~^1wtc)(S5N8^BcU@*<^h6mF-bx3 zJ0X2-@w8W$U6Nqsp1)15h!HE}d!L34Ess{_?P?yr$Sn47jv6EKQdsJg(Abnpn-xUe_In<5^s%vQstr4=7jTUO+`RP&C)NlTH8+}#&YR)H0HG+sB4C6%cp)?H= zd_LKQRvO|>FVCOSg*^B)V8(f$ggfMeT_?x~P&4l@Rey=^iE*=Jx1~^iIoKi7c_~KS z1m{q1=hb1bV>EhU1~3om*(TN@r!0cIZr6f|S-iIph5?xbIeMO|W^O1GTI0NRhKwv3 zTJrL31PDBJi+6C$(pifnVa&5|Um~+K6fIxqYb(sX2GvWP{77_0sYsn`0X{H6)K0FF z-^jmvY{*c3#UPpI0we=guJ!RQsHsyHD!-T|;_|RwUx6AFIF5@low1pDROKA-EMCAU z%nxH`=mHhY#{*v8*`#RZl+ovYS!#x`oE?EQ5U(|&vn(x~NA;u~J{ZzDYJK%WCS2%v z4*V;~krnCu-D~U7IL&Ku;MAr)5u~4p(u-=Qd=_(YJ#_D|P;*9E{|B@}2i^z0k^>7l zGWUUhfY9_&((c#jf&SN(k6(XphQ4^Pe7y4=03~!SBR;=_ptn`LNQY07plr{${2#E) z4XXL_=Cq=_VFQ!}IwDf2WVBCPQ3(!Mwb&`^x3%?KUym9dv^SC6V%L(G@~YRpJvY06iyJLvEOJmL$X;KHg$AL+ zUafsgZF3;4Zbq4KUj^ckRC}q$i6>tV_gl4tIWCqNi>FcF%$%Qi*u+39CUh}X5lKwG z*sbh`_NesI<(Mdk5tek2%kQ_FyEYTfp%^wfasD}buVQfXF>9&KET zdtEXckZ$b(HV;VXd|v^&=zH|<5N=uUSA?0`&`>^a-}a8nFHKaKJE(jO6}Laiz+9>9 z#A=4wNNJ4RtFye{Z^0nv>D`Eh>`Ys08A$(jKY%G$BFsLjLO9^r;eyWlWt~FSsAi18 zb)#gsW0VgdHP0yl*=1Arc@G37CiEWm52vpHu$>h7Uae7-m?nOrAd6!Wj$~ z*CwMZcI4UB*$T4c(Lb6cM4L?H?c3-QSg3Q^YEeB`j47*end-2XNfW7@B z`18FfjcwU5s}uiKQ7(z1Pl{2LEz<{!&WrnP8|JV@@H)pbsOQk%)AJ#Q^Er&w#PcOp zBaf_foHU(8?~JWNV*Sq$QIiF&oKrK})Vi;gS3DWm)0#axW+mTm7sCWnz1Z1bDpUuU z#7_ta;8Q~JY=~f{wa9%Zu>0*20{J=iqmo?mYz2eXC@r6|_|4R}+8$g34PsX6B+7(P zalRU^JQF9+Ns@u0S443NO$T;*m5F(qA5-ysU$6{Kf5XmwNA zQP#@9B?+~X+(#XzS6L$N6#IO8;s>c_6Cy9z*cfaG4gp-4WFBsIdht2a@!Ug+_jgcPM%pffK>f*S9E*~`|@!Gncke4iZ(qbm}E8DP} za1!oRFrRog$&#i3}cTNKD<7n2Kh>}>{`g2+)x+hGqVTuBdMm5-xP(}kF zg+b^y5@T=^VerHp+56O1f%y3*jc9w0`mmOW<1zxxRg^#0==yp4Lh}il+Dzj6x~w~F zTxB$OGOG<-KCz6ZI@-Dlnx}%T@=cRE+}~686vcqYexDyx&Gn}B1hV+F$7|qL*dZwquvG za88lid@rl&i}Qi;4uV4I7JI|IQ;UylOqsT~j#Z6QjCJ_Ba0uhV6ck|xn5DW9p?Co- zv{U^7zq*r%&O<`7%03i8I~lX?Uj>i9b^V(Gn^ihp8VbcZe6aW$tR(mC^HRS3PtM|7b`8JlA6h7dg z^d%jE<|OyA5R0tMn}XLBs`>P6uEGJ8W(T-S$9j2C4ZVMW-`_^GQ0(4Dp;=4+yK(Md zaqog#>cd>KJCEbH#L%|%{n~Q_-!C+rrzFn0`TIe%Lzfu~NmIFw{9S)Wif3iavllGDbK%IDAuSIR4`I1kt{Mqzc z*{NpX;Vjf6wG34Er78}>3!ZI@mkygH@lAIsiI$YbZTY9oYpGH>1~^Lh@P~0L)9UbD zJW2oMA^!C+Jhy)s;FA1)W38AmV>{JgrQsn&_jd^Ct@B8))O0Y5?!tw?=XO%HSEk4a z`{^`?D`S0I*2=Ge$BIs+*z+gxqPnq88_1NUS_x~ei3%IDf~Vk6wVe)9Gy29h&FC-> z#wjh{X}o`cMuGp!W+u;%WsQ!fj*|Xn!IcB2m2h#>;h<1lw-4pxnONAm9^j`zi4APE z75TwcJE6B+y$b(;4GhrccS9`ol$SI;;7lX@@D;Qc9~EBHCO^&fj)EUO`~yH!7R!y= zp18pNi>clo5vN@J;jPQHbT8)prP8 zB=d-nu&(o+jkUwj_w~97Q*DgKkmVAaGRUv7VLTTir2cE_{$;9=U8GAV|!Q!%76Q_FyW&>)!_w)I&024$_Few)yN>k$T25gZ&< zN5)!8YEB^(LD$Iz`F2La?(RJMFN`Vlj=_wgu3cWPwq zziUXV5C`!NKBHFo&m*#pTJr1X2;co)kHO-@holaTeEF=Y-Ygu?KDt$?tJp`>+~rmu zvp}&x$hP+*n$Emm>+Q>bz!G^={r8s;tV!z@KHGVm&(t&b_nSR@%?^`Q142vFzOwK` z-T9SuzOvQg9nNWc`gW2YGxPej`%U68h}Yj{>z?XorW1OR%m_~xlo36m+?<7WaEhgwSZfuyf?S>mC%$$5yua+pN-mRgC-gl(eXp62s6 zNM`8HII!i08v}hl&WcH@x=Jn1FqDdI*QZf3OIKSaVz~8iyb}tF)yfT^+QB9$n49G< z&Gl`yylE3+{l?fv!DvH}eK`6Mj;HYWAHealBW~oUhQ5r&O(UcMkcw7kCl{ihOcy`u zp^{iMgkT34Co@;?29)PIMZpk%Og|5WjcaFQaN#;wM*gtd-x5DDa%FWKHWY~p z!P(U_TyPG3QHZP0_oy(QZ1fTFc4HYNI`T4=wsPP`L=uOyG3DE^Q=_jYR+25`u*e|@ zl^B>?_wZXGt(RoW@PZ+7LLR@}igF%jV}%~;+#EmYT^#%YR&Q#QJU>oY$Hl{Gxh4O^ zShgfY*Z^@O0CF!TLzE3h}@34AZqOHnl zkmY=$M}aOljFT=*=z4Lvb8NjyXe2r9vKrdRCdprS$y+a*tqfV(xX@UsIhEw~kChbJ zWAnK&y}0u|xc2RKovv!DMl>%Rm-sCha*pm-?pnezc58Yg#%}d!w}j`GXxAX2Wvomw zZPa{{8Fbb2+`d?$3KmOb*ep;0fTT2GPXsGjOajp!$xcM9{jIJE9|%{;<(|i@X&3N+ z?w}w-KrcWtE2l48AfHy5n@fnn%B}}c$v9b>ElbQfQG|ta9}f_Xznc{Fn*kqGuo z0V@0C>>b%$JYIJmSt6sSHvr&Zxoyl8XB^(NW?H=p;NrMcOpt#{eNN{nH94>ZJdanh z2l5g#L!R<^ftPRQDYa4a8-!Wv8!x_KQ?g2x~28I~F# zn-sXKIe_?fQTR#mxPu7yPqpnI*w|s9(}sh&dW|zc5ZxT3npOzx?bv##j0vtKeR2t; z?V<(KqhGn6(lcNRNhBW~_;o#X0+^~JROKTF7*J@38i+(u&JaQmNu5y4J#18J^_RiLq%DpReg2qO}JEX zeO{Gm zVe9Q*o}L|It7C-`6j1W*)EvO>0#^1lwU>xhrZsX(Hw~w%tL+lgUcD?M%{4K;Jm5n^ zo{9)Dut!ZfIEk@MpjH)n3950@;w^aIL_XWG_J^kx$0l$#H-U^Ll#|4OW}7r`-5if& zd189!Y3$hOIU%(sHhZ621AduiG3uVQ2&Y&f1)o}V3Goc#3|S7gBssj5OV_e{E5erS zd#su?u7U*CZ6c3R0EY@6FZK~_{CJKUpM`tRho8F|*%*E2!ZdC9dZ*)mI}B5bn#g1(B751)2sf%h5EYU)L_V4W>oI%k96{F4I#s4i@S}M9#pt;Qi6W`) zlzI1iGN1nCGHIw+gK@$j`+KpqJPQC-C^}g{bnO6BjCx=aKusJGvDN2zJGZ#`C*<+b z75vK_6~fB|bt4JInYOqae&xLjmIYt4Dxb`!|9@-A1R0McPrrN*Con8pz2Clf0hP#d zTy#p^$+IGiDV`zI{{Rh8ePUiLJKS5YxDF1zPh0@SgU9BSAsoWVi~9#YV1nAjb+8?Iow5wd}Z6YiQ;qL-HWO}9_3CWnd4*})6Dzo z<66Qis|AbjkEuj4j+E+G<}!gBBUe(HWYWv%Z@To_+*Ah?BpR>j9-H=f5qjh7paMZP zNv;g1uigbY@^kLnmV6fV*z2=+4nb62Epj*`32Na{ggqL4GQ)<}WH1E9ZD}MCodlcG zz6;ybE2I;U)(SV9#7yqjf}-`s!0DV;MaeUj4t7bTJP$xc6ROA0nF_5^^(^g&CLBxp z92o=Ac1Pqz2AkD>pxGBlFf{LOSY$W`;a+xROwuh)^j82IinCRf6%$^m{NEyV#Z4@9 zFt`m34<(bMI!7+t1J>CL8TlTMt&G~@o1VHZc>F#B`PS{Uh1XmrU$bb{{eBb;u)B&T}46ZvXf+am-KV=_m9t|gJ8PS zO8EHmyZ`Bbzb zlKi8G@`GVEsg>JxR`98O%nwZWm87do?k%1bo}pQh$e*1(ys}j=#uJDTlRfNJVcW?u zcS2dnw*nRsiZF|-L0(~+qL_r~q}3O!HAFdsi1vYF%1qU%sfWN*igHE(Ntnn`<-qNV zAl#;XoNWJ{Tw3Kj!h7|1bHjyg7@)!YNyPI}AKhxCi;=j^H%Qg!`}}7nAC*(VpxGpH z)LJ$Yx@3h!=AY6Y*rifi;*oKi4lNW+1f_@e(YS66QJ{~OYBsjnCF;}Of`6|z)L3tp zY@AS6x>ChatofRyqk*-gO0lkY+=8B{upy?)u3tR+vb;hFI$&R!e7ocNXy_7^_ z>EgYvOv@CagzgO0#0^OqRErT9rtDrfkm2xoD=Q`zM zJn`B;pJgC)An5PF#s5dNazHCfVkKv_pDyQkLxBhue_Fk;E#Rx&Bw0yykYbsbOG8B$~-Hd$8&Tr_U1JC{)X&shZms+QO*8X~O)(@FH_R|#a$2T$fV37k_E zNc80hI!zqkE0veNpze?~K=eQUtv{nk=o4%cB9@KKWq)mCs&TxV)0WJ00J=Eea^tkP zx?<~ywBi6)j$dDSoUS5Z-O zh?~^EWv-yM(}~#L3w;Jd^>Kujs*BO<$J;b>)o!vK3wE4n!1o?O^T}UY`1CooeCli? zpPfxrnzN0bx}1*e+2jQO6C`;I)_0Al*uS{@+TWpNnko`k?eyQVJ_~Hu^ zWpbP*ExR$JI$Dl-gUpk~eCSU&hZHEYu*Q{`?iKpG8qpcWseBlo2rH-|=R}joF=eyG zM*8MfBZnKI@R?Ni;e3yl_malwyit4c;y^W;4Xm9px$#^r#{cFzx2nt;6rOeD1}!bz zifz&AZ%MH0Y*X2hKCp_vTQ!`FYx|!=3`vhPAMQ_{vI-v{|A5<9SD{phC(?(nV0$O{ z#Xn#Qe9sN+hu#;3?ju4kz=7@0j(@=T=NnL;@^@e_^d7XR+=hb=)3*;N{{SC{2l2;J zPQ~q9zS>^L zB|SBNf?3i-c%N2+-n{tugm!S37ucs>Youb{_iFvfd?}DK$mkU8k}-wi>)O)(Zak5S zE>W3rANBG%OqHLj%v=KD&Vh&l_2zZ(C#9Yi0)F{~ez?n%QMq2~Nv&D57(NLVm_6Sg zrd`pD9t77s)Jwnj`FFVs6WTSi7Q;OQg*K%BfWlxfL-ia?hzEa&g_h&LXW6ZL^EraV+gwf}RGpt{oJEM4Gb45RW@hi}){HQ=?9mx7r@mqb80LpTTAUwtH=UB8o z@@6cDtZG9!LUk*mEw0nojb?dIZt44-%3cdGqlaVg7;>}zinnRf1eO_( zYGsfM^+^!DjqVOcdbRJlQ)bNjjNQ_{yvC7h*Cu#=Id7XiPEpqG8Fjm8UG8L_8cr_HQnc@6-<|tqz9gD8p@(vD^=>(?y2+Nzno{-<40u>t7a(C%XDF?*Wr9nbGkK9SLP)3 zuC1cx1xk#bNn1vFt}UOOgduB1*op!=8jH)+>R^2|Bn4JTS(4T=q`#yAaN5YK9B$TZ zpg{3~?HtT-rb}1oFX}GS>d@%WUR(w&cuTtsP|a$w0$d+Gup=0I*gW^N++>d35YC&* z--d#<^Ara)ny-^nv&Cwm6Bk?n{K)jNe6{Cr*}X zWNd6+0@!X9RjFSXlw568%r7sQAN5|SS++!?|z^8jnGU*S%tGuoIz5LT|82{TD5QgUvV9Fhi~Ez3g%G>kq&eYs2KCid@K922$$C zV<=dsqklTjOnqS9^$3580sUolPRNRH*dV45W-8lrZCA)2b10mM5it3!O0!e= z2wBE<@CERT7ChVxC%aDikxWU33%z}LnmVPHkTS`uQ_S0LVYBEht{~?YBm0-;r(1=m zPCQ|28VP=7d*&~E=UCV7YHssPXcLKtm0B9b;T!OTuzMRna(dhZnhb05-IJjgB)*v`pwBiH)ul~y*jY;1*h|AY_Bd!AzK z!QmKLW>*YFE((%32ED@_b{%&`ctXnbMS`=xiLXa#Xogb|`JTSS2 zt8C4WW*HlYTz^d3%}sq|@Z?~LQkvjb?4BIid1mz5#>U1n9ECHnxhQ^sGu6d!<1qxZ z6NxO;XCH`CEMlyMdWNC)I9KwMbC$I?{_y}h63mCAN$k(5XW%$V?w5ydz?LH;Df@5L zA2C-K_f!D$b-Ym|9V_<>SzO$m>mZA~#U{%s5ab5zBjL2?*hy!pW-L9DW#g1N9 znHwrRpbaK|mRK{Gs?0`O*9=vgcBJF(1GsUS)^cu~5+F$qiHSI#u6mXdDhz*Bq$-js z=K(*wLI8kLgFqQaGiZV6JnlRQlg`UlhP4pkA-VQjh1qh_8|oC#Ck;TsR zl16=nlKmXE-Z+P)@D0Xwj)s~_M&b-8l4(m_IogwcBZi%Xp|7_C#@FU`7#5)oe85^l zv9Vlx!sqUjZ~uTy*5v1YqB-4bOmS9`%^h}U836A*l=>PBd;yv{d)rOKj%*vpW`YjHQh!4sKy}BTD<5mt3YGv7@91k0XtKAyP7unC8!glZ z0w@+=4^r?Q&Hf8K{I4PQnRyco+Eo)g9!qc(ONlJgb>p$ss)MI@fNli+=Xn6qp|L!K z6EmxvDvY5GX@`+8UL4QcIOx-12>_Bd{e#Pe2VtuOiM#Lf@Tj3z{ZyR1!&VE`=_ua8 zurE!m>xfpNf54gYUr=N7&qs*Mk2V0r z_HPITRc-&c4aFsH|3`YXKo?`C=-%j6^d9M;Yx%JWAP=*lfRq?E-hrHNT;)rC4D&X8 zS99^vAOQVb!B?R}SRVw!p)|_Yf|y$+Z}5&QqRTK!)1N@pEL(-Cg~xl#FVXXO4amgX z4`Zg|HH^E`@oAc_xyG{!ZvLVVv)lspifL}8Hgm@biqPfrnzF=EyI}M2&c1NU-w-gv zKRc|PL4UTs3c#6p^bHs|WzoW9p~2k)!q=%K*r% zGuGD2Jo2OIhk@33tF^wRuVu;l(v-r*lG)C{o`KNIm*rTBK<6*j@TbrU-VtH0bd%j{ z#zJ)vXe3W?!ok%W_Cx7E?QN9QxC-e$av9+Fv!jDK1lczX%sBJt*`#3DOK|H_-c&?U z+ltpa4Er)fNr2RGln~pbe5AdzK&7x58cD^29k9xO`XW|;{)I5B)(-X#<*}ty1w?1w zfMa;2Zp+P=Np(_YDnpXw)7KJ4b$V&6GlUDbBy{ZYX)yP{fQpI67y5dWPlw;34R!|S zzbxHPmX6{G7$!A!HR4EYWCUMZqDXw{zdv!NPI7SqitFq+IPtS5kyS3Pm$A~8EhvzE z1-TeX#!#bW>}jk*R&5l`69?}Z?Na7LW;aNm)y}f%#`Cks5M{{F5jQm}Fq&Q5D!tC$ z8)KQ8*+13#2Bjt#W!GuKX3F{T!H+KQ_OUU@s~?TJay+xZ(SrH>oWanzH1;P^Fhuy3 zAusa+v+smaYXAkso{B%<+h#A@@u03`<=PnZ(o)AxTxvv*P}0%?-1@ksLFpF9%FGC4 ze!=4c4&dwIv_hFBGD_;ks!Zkz9K#gK3|9OZhq(d7Ym}FRpTFhdF;vvu*jEZ_Qq_&D zgd;Hu{|Z2a+`tYKmREO$-f)f8SQc_5uy4udqo$E#2v{DmsN#g*$9s33iQCJ3wVyag zt4U%{VQ@$mPx(w;RuxEMZ}TAIx-`3d@Ufm~Wt>W4)PoTVDj~>r!wS5i5Fhvg98L$R za!X}5kT?na{?d{MDJgh2sVhSC7f!`cbsKjec5pDz|Em0w@ZI3FBdfPsi833FzY`YQ zn^cGvl@E%3dRzcud=37j&KL0xJ{(QVUX@w7sr)VG&srA!qrW}blo$R`edJ>FjP3qa z;kfWJe20X0)hjG;b2nY{{h}k&Yt?ZrWmY-n6l=;|+Out6>pH*R#xVs$(kX@*K^OX}*{ z0zN8I-T^25p{?L?T>rIX;6ZzRDze&)6QDPGOb`bpf>6vt3GV z6D;?7VUIPxVqer5^m}r*x~0Om>Pu0Rr$&mHr#3XD0Ux+k>oalpstKWOZ(}EP4&SYc zqk<}vTdh2b`#7={k)Q^JF|C=Q_tGp=5X78|`17N2i<~hY3W6*doASu(_5qI7lUSFb zbnVK&tqf(gb$Oyw3`$;&>Y7B=@0M4mMweDG&ktQZjcKEAAZ8VP=hzh=@}x_3<&j>z zIHx|T3NR(VkxiZu&7laI5MXQqR^$^ATXd$*Rp5<51>%vrCracu&-* zmkDhdiK;y5TyTlg%<0l9TQ$=wmu)Yi_wTkpCd6D8eLZbs7fEwSo~R?0F*WTUvbeJ> z{+6$}`5z`{(%h=Am5*SJ0lJb=&?XdB>c@_jz$Yw>Vv_$y<`M(w_z_Zm#@4X~ z0G2Y#fCms5HiZl8%|o)3b{z*tvOoC`@YgYZzUO{no`^92OO{;%)h!I9b&PF=C2F$vbl#QxgqFrqV0}r>?uNd`4u5a}AxVcYiHZr$8yM&QWgTg+3^K zR}`H>2mbdZ>HAt8KZnMoa$Z`)t<|ml{M9JAB?lzccd>G#kua-h0>(dsFxJ!lqR)dqa-0r55MTBBb&-EYI4=-`ZOIz}r$70|wxPi`Fxk zk1gke7tFk{fwyzAX7NSG9Dp)qCQ8_46?h4bFZaVx)qG`D2ew{cTgU~5hoOpj+y01M z_IE=9-ojDr$Z9zn)@4W#2wYS@S+hFXkxg3j5yZDmq*}W9rFkw-tAV?wS1o*H=n22; zYV_j>Tmx}TK~B$?Z%$@oMg#z6$^IO0k7ln=uf>M#R)hd1WECbNFbyZ({?WiI!BuA( zG>df3>}#}f+gj&~hoT{`g;0G&(}8jZ;=dnyR>b=8Xc5lJxzgA8!3inGhSQ8PRXJ4V ztnnyTQL$aAd%)flRD~q{ti5bE2iH9}mr$M{#kYHI|0^BG4|@H0ds+BD(s5txAer0d zxq&w!FS26Al}Z~kvvOoo{s9B?V$S*mFKz(-=C*gEG&0#`nx= zD&6aye(Xvj`u^9IOaFjBn1OsrUc(+lt58@{FsEF|f5GS`gW{@d$y zWepwo>O0#nOsmq3Q%xuX|KMzp_c`|60P*nVa^-yiSv52;>rdK#-c@&G0~fb&YQKq@ z7;P$T#0LNcpv^+dG0jXY)yPpLkz|zifHN&9 z0TC~r3gn&@H76(f?1_ze>t;u_G6w4Xy zf8LUU9ArP;JxM!K#13Rt15p5?HW;|;G3(qY_HEihsRz?!P_<{(TD7~x*G4rPzBnCu zPh~R_j6GdzE^qfaOiPV!e^)vReJuiTm<=Ge#r`S!C`^qz@~E*6_9{fQ!jpxyA`I@xqDFgB`h`Y%9@cgU{SWQUrR6?%dIuAKPev&700b72KC zSYGKd{;#zfmguwnVC$#jnLaZ(o!rjO3=TD3Ya^M=seZekS$IGCF}X-nBBMo4uMz}D zACEoax?E%sJqTm>9(HG69_cz9^j1!MLBtGLXa-S|?b@OK)w4&CXMD~2szgjk>~D5E zUq6n#Xu;Rvz6Iwp77gR{_nuu`m^q%ZPuOKlHVvfv9g@Aotxo@RFzLX??$BzR@OmKY z?W5G>C;S>V6IPQqPMt1Q=#^ad z?tUoMaDIYwSx2h?CBF}@>?2$E7u1o+V~z_G-6W`CbwqI+H-OIj*Fv1N*7}X$HD3Ub zTl*x0_g{B_9QDZ>^JU7sLW|JQAY(ezvV3~@ts+7VfcT0jB8T-9PHiGFe(;#|dj4C& z72N!~z|YAOGLLKmH+Hc3k($as+qM4?@3^y(R_0>idcI!pIaJ`0>(++sd9pw$YOus; z6EfIu2wN|Q09ErcU39oGFgX@~%2xnSB(6~zHV6aoidWE*lO;`0I_Z_%tH}UnIyu(2 zr06@zek1b6^zxDw-x01qN2r|ME|Q; zBlflBciyUf);MIgTs6W%ecoT}^W9g>sP}J1S>_aLBr7x~(?50m7!E2~jjftB8EN6V zS9`ZZEWPc^l%5w4OPdcpZwz`D8rR4b`akDTDEjo`mMe5~;D62m>9M@mQ~3GFP6mna zB%j;%k|kkjWC*kbqKT*wAZRhouGnrVQ?Lzq3Sg-AhPiaV9iZn0KXZoHH0+hs}tG)5IyV7{nXNeTmpuU$SCuUT(dxhF9uuy;)%-ao?mm$SQg{| zfZY2_IP3PU?QMhL3zo{;OS=O&rJp^ChpZDM!ZV9!s~1>s;3(Mn=Tah1q)f3?ai&6JKy^BnxL+2&_U8bxKi49#55FI)f&(V=p! zW!{eTa1l| znPosX(U^~OPO)83aK6jU)STsIeZ1qTG$B4U=3U{u->bc3V+;W|?nsK1JiXk^lmwaG zrt@S@sTVhis~{LHr#59}rFSAgv?k5`ufTkp6FYOZVqy(fxza=#?N7)RJR*&*9LJZ* zx;KYOby}0+!IY>!pZNhk99iPac80J@aEobK68OMhllZ*)HWYUOe;h|F?W?!7DleTQ z#>UO`RLkx!dqJwHp*om1!zdAB-#7+TQY(16`MF9?Ovc5nS$kxjP(Hj$dh;`~*CMID z9a4H)opFqmiwU_#G=JrX8%5Dun}(ShCr7VbH#_-DkF@F(y?g*S_t?dE;w@o?cs>tg zntnTefP1Ej|1jX07E}<=x!EWVP2KQGf}JNxEQ|h=BI{XBb*YI6vjFs!0H0M~bQ5qf z(zaaqp@QA$fW9m>yLIS$F{f>o3{r?q)v>EYa~#c3IS#J>&?iStb93(37kCRgA34y> z#R&Xi^pw^Ep%tRVVIT7a#4Ux!q81qSW76V>gY&H~zZ=)JH5!H&N>S#tVIC9Azb)@u{GriNsf` z9V0>T67>4+zhB%gqeWFiYA%4yeBY*@|QT0LKB! z9PBqqggLGzk)}%DE;@b&1doshLC!!S%NNl0%W^aFpyMz0KabdLnh7aq#0p`QH+GNY zSdc^|3nEWf1 z0(J&JUu-57EH6csrgl{GnU6hwg1p<*x~c#lr}I|7`dKP! zJn9tdSy=MNn)c645EdJdiLHkAGPapmumKOHK7YPF#{8OE_=_423(nk}kdUI8QzL7Z zG9K)=A0=(oLn1vdX9z_N=Vym8qeJ_%k-BFIcSzo@GmpoK2JuaJ?a4SV&`Y~ak@XS> zhL1wOQbfhkt3S79&U}{4*E~CFFm!bQ21`-%rGVoy$9M6CZEs@BUZHwcyi;5sv4T*j zxwx-*xaS zvg?|kHuyxIblpbec0Kt^FBg0eAtx{frX{+}^iXFr*pDcC`Ax}c?<5%OG8{x;WpV~$ zA)_@eg__BdCU6MHq_Uqd4HgZBjqqBQHjk|yKs`f03ykcy^IM)p;!Q`)< zq=;O!>3H4o6!na?UD6t}CS^7F`UH%UAhoVh%KKU^V;YyF=>p)BzG;H3>p31QfeC`` z{|8TR9Tn9dcJUsDPU-HFZUzOE?vNG)WJo234k;NxI;2yO?v9}ZMnGEWZlpt`k^0^9 zzW3gD&0jNXF|5Vm8_)CX{nmhk?Qqjfe9`_GyUy@D=YjGT4F-@=W5lP-19g6%2nh>}q+>9;I121% z97mFBZWv2{LpD$8t%Ra4-)*|TM=fk%i0$c)FeDy1tF_(#JSC0PNwm&V3P;{L;lheS zg2JlQZb5A3=Yns3kpH_ONCp=-XPOgEM}Aq{Vr-#4Z{y+Bk0Oj2p@S5|i{?GQ4;28O zv0MtcGYw=`xcf!%H&f_vkxa^b#iHU~rmTnWmnd{Q98LLW%*4J&L^+yDb<_pDXf_E$ zywrndRnBvu>Mu~J%9fJ6JM3p_!iyB=PsR+#R(%#-LW^`y*xlLm+whA7?&@B4YD4NM z5&Wqdq@Qy1=nkN(yIlj0znv!fCv_lsWPyoF$0q@7Fn00Dtr+*A)=TOo;0vV^Ps#46 zpYeJf?y&0SaT)S~-#P@v#7%oE9v30&YLaE_7nii*ti83~D2@iz(=O<fki$4?bk6%&DUtv z7;MnQ9Vkp~^#K%ji)R+%CLA)ls?{&knPSGsk;CW=Yj7nV({I-s^+=^d4jW?7J(d%M zipRB>O^jb;D_75fSLG$@zC>RBV!wQynhJa_9IQL9$9ZM#ft}LH3Oq&_27rkKmM(jc zlqwaUoNt}-)`F&P2_!RH>{`O_T}~B3Tm6zFKDa~^GFm8CvBj)&Z*3oX+ezk%^dA}j z@&CPZuQ1dFo?M#$cXAyinQ?P02qvB~3jh5*@=|7YtWR2@WR34&^&iEgu9>pEx0-9W zn%LtM@Vw2Lw{(d|BU-vPjUTjndFebarE%4TRlbY$x~Dw?60jxZiZx zT&`AW@zkh#YCHED@$V_>zBb7>;3G-{P*Q-ARd?8GEDQ2S%7ABw!CU?;YQ%rN zKU}twi2aWou}KQ%_khvEL6Tb^MwBaq6l=JGS7q!)a_kp}L_rVCuqpfP6UqxQhoPZ*u=}1EYf;`CL*SgI1da?4&bAlX;83!X$J2fhpq8Y=+^LPqbFxp)eLOoIf35mqCu z=HD@%QSv(9d>&mS+>fI_$g?IR}m z^BF$cV$dbGvi?xHkSPJkXuynbRyvWGOE$DvocK!H@Z)KGIx-l}%XtDSf9ko6VWN(d zx>Zz=QHoGWG~BipN)bfuQ-|k8n0zk&o>L;Uk~JP`o<->9}P#h z2!vMz6yWTeM5e*te0o)J`{pjhd+&*v_75^Xk?l3IEPnP#tJEzrw2I=&qPOX){HiO9S+$xT#I7P17}WO<2jqNP0DE z*?&0c2zQWJpPS-xHHiira>~78k9}8>!|F>AqGrQKoe0BuQ6+fYs|=@2C9-X6S0ZmZ zrjQMfPMpidUPl=Q6S>^WU$^A-A6tKKIFm+}4G|>U8S0DeVLnrBswh8q6rAw8eA&vk zuhY;Wq%Mj5zTuzR&PWX`DTbk`t_{iz_91MERYdstQVCjOH5ypCI^%yt-Cdy907nKR zZ0Gb;czaZ`YPj>>QuFIHBD_66y_23sWvo?CU)E*Urw50Bk8Awj|}K zOrLCjhp|Mp?=dgIAG*(|sUSxzYibD?78z@MQk&)By;Nw^Wo>L-V)UO{c}8sUdPeFG zTTvn~xNQc7uJ!f5cz|@f(4K-AKH>t;cDuDi$u;b;ZjO{KxrNEHhiv0e^7SG7nD|?+ zQK(n?!mp?$$-j!dE?P5=||PIDh!al-yYApbOJ3C{+hFw>)Tz>GBww zzgGXVh)&{fuKgRpE)SLL&RPN8sz`v0O_hdR*lwA01$1Z$-m?_H-X&}?{Y_JDoL80f z&D+-2veIa7J}8%@67PvInwdR6tcWpVVv0;O1S{QkC1oAj>%(XKMkou+y*Zm-j~0=q zkoBA?4_S6V=DWd^em%HEbE@T~pfL%NIOn0YLS0TguKQ`Z988Ex+Z>mZVNevrFtP1T zF7zxB{u%_$q_V?VMM55K5uPz-OkDt^xlllSC29Q%V>LSlejF}APgHL-S*%+ z+S%t6{5NZF=K;eniFV3Lrd-s+Xu1iLq3$YfQRnRXPQ8=0NcH1XX^-ee?LMSA+#7fsuXoj=Dg+q;950J*Kd-X5wqP$ zD!XiY(bFX9$=>wcpkHFT3X)qycl_{o@cS=Q8e{X&HxD2%@-&b6nWUE_32cMUn|;Xz z-sws`zoFqVed1L|cyf*6dS9BwX6a6r=V7@mbqNL)Y>JIGC{_%s&aNkRes@fgU54Qj zktZ2?+vrdpQH$3I72w3?S{=NuLyOX*yTryE#}3pj$Zig9vl8qEg#`gZc%7L|&;NOT z<;lkP13#z%h~*3>4N>zZgOf^8dlFp$pg8GF^k_TnEZckxDl=)Q3liE38<5%fbQkv^ zzL;}|YlZs#bgxzmv|k5@$4G>lx4s@qCGl*W9L&#_Ub#TXfgX$p88Z>Uy{4ifQZ^;c zu^i(8rytQAt;X()wViegiR%ijM?pLjh(m}9awH(@OyHI!=4{YzlBuyzhj0cJtpXx_ zkrTfiW0FP`grl!DWX6RcDdjc2x(eYf1(_7pE&p^pd>;Jpq3iXJv zO>w4qzk~r$hT6I{2KpFzTA|`kB52K5rf-*)Xy37B3{##)cx<~o)sq3e4FPCF%vvI0 zDQc|m69R^P-rnSn9S9e;JPtN0%e-eIJXnL4s8u{$Vvk9oreVPJl!X*QUgj~C4G0*` zv&(0*l_t3nnQFXN)_j+#XYz`Fgsezt?c)`l^)C}I=keSx6;%~9PXv(4}wS3o&d#j$dKZ(ZACK+OP>%vP4bmNT5nU^Ls-dK^lw{w!6TqWRL$!@zT zh*y7jG@v+Hcgd~!xb^HfzO`o{TA5guD5ypTs&R%)5L$kX6zR$xddA#PSX2l6oBpl_bUp%YTtShD?ez(eO@DVgS2iZ%G>Pds+tSn7bbYUy>7kekyZyON_-h~eEo59O+txT#YElQrav!y zSTa<*Ll%maS`x|+H!9FERP`}*Vt29d+0$`R1@I@A=q9LOoQX|0G_os(C#Hn6=|aIM z%g@D~uPBwLIA^OGi;5~sG9Ax({GM|cC=369q%OPoFz2)|2u%#Vuh=HE`(QL6ZCpTaM7FiJW4~=VuehM0@+m zWp)Lv-#VR*8^7Gsm;J`hB=xIS6%OazBq6e8jnZRgA})!Gr-IAI%g>sY8E3K9F|=)I z3HRl53L-n2;QXIm0&%QG_PXzswZresKlYwe$9JUYie+NjS?1~*au~jmPOQIIvi_x` zJ&L_VlKVpLuc%U{&CZ9iPh<3x_~EH^YLIPXp{B?Lk zLlkkR!5(v`bW>s3u@-wiDaXY9u)!a(h9qO#>UufAJIXG~75xTcOqXzI%Z#58#3#)J z8FGiW2H3m{Nzsw^nwjyy{DRpuK1ls7sg42npX>$W%>ut>MC;<#Dbz4Qg+Xt>tj0ND z5osGsO7djh;v{NyK>n)WH|NOKh`asGV&jG$i)X3*Q=GbivQQT1kS(IFm8%~HhF{uB zZM(EDx#jp@d<28SqQsI1UehiaXth%w$ua$ya&GmtUf&hm-zom}o@2x#8mD5#WR3o+KjY9t+}V38IHgdsBAL;=D&^r1mD_?sNT0lon8b#U?BcW$ytG~O1^TQg4t2SmAL(LXmf$8H2 z_viAG>J{W?Re4^F-I~_;VEpqf`!voh3M8wfSb#93_a7=b^yh}v!=N*P7s;^|*rff> zs55ive1{$xJK|dUwg%O((0jgbMIkH;R*atrP2H}7K*mqYUlLl2vUOu2@`Tk-Q-d@`|-`^Wk$}uR_BhLthcK8nLiahhw8GN)y+6QsUhcIxOn|pR_A|h*#KzF z){{`6Rw?UTl5^XE4&)jbRO(En+oY6tXZEbFv_k<>dlV2(ppfmw4!;BgS9odUW3uB^ zhU}g$0azwQdepp)Z~GET9>12{VCmy-#Y%_i$u=(Yl9U_GE~AfM_B1;8kEqdWS8^hu zlQvYW^C%~gZTk5TRUkg6j*z&vhJ3Y`}M?>&g`P6j)1Jv3$ng%Iexni zAaskH!ZB7ilm<}%Onu2PI=2W0Wc5opk}6GGjLd<`b^_H|_*tT(Ckis0^atOw^<`)P zKw8sa^mF!6e^0m!l8}dACXy<5&e%Fmydmgp$|N3k=PU%^(2twI9WDMR!lXEqSb?_^ zZ8^r@7>0(7Y%&!|9G&UxUt-b(IL3tx>v@aTraS`lP%f@%-fqo^sXsC!mxln zc!jUcbdrgQ%U~NZW|c(g-LFOfLdoi0*4j}tzMg9g7>z13oz|j|hQJlbj>b&p+&<0t zqE>XkB$9;t*s6wO5iQd==cHT0<>x>~s z493Fm2wN$aeT5Mm1`Ka>94;qivcYF4zwKofPPKU4wYm$080RIbDQpYEYnV!K5a}@n z&bU)m5OQu>C%H#3UiXl9pOxXQhodL+A6G?4(jZ@=6ehomTXVPBGb{!*4mP;d@ji0x ze+&aLtp+}`g`_HN3ZZ-tQvU}EmI;q4-878QY??l_upqMFwTe)Q!kZqUD@Fu;mV1!kjSDSf3JXqL@-#N>ydnaFxYqTIAWi z;e5F0+ta(vpM%VVQ)qe3TEq>Lf2CVbKYJlWCy!xIu5CcaHBO^++#GyIfE5od$Ew36 z`rZtYNg>K<2W44`wSg)-&FM7&PEwg3YISpGmXfM%jWz+}K+`SX8?ybFYYp$pd;G8n z%#6k^#_%ucFTs!NdNSo<8osJef4V#Sfo)SD?>9@xUL3nb>(t_I4Z4zeAs^F7nZ5Jf z8Ym1v=aG&-E*HDJzP#$J1KvAbD#yuyO;H#52#_(h8CCTap=ZJ>}lfCpy#tW-Sd zvp@{jh=&?S*7F53=BV3VGSjXKS(fGWSd-*a z7zQJPiEs|fsYmfL3ga|2z^tZ}Vb|r=mtup~EOTG{}E@xEo zT>0XSdxb)qVxrcmXF}62_0SRg4=0ul%~c;)BLE+sLu)mjOyU>P6%j^Z4IzB0%=b$q z`-n-tzu$01_ztDaVkmTE{(_1mGt2^ZG8%xycK@P>mhepub*aX@AILgqL+E0S(qo>@ zFZt7b5nY!P0TliNhMlnWq%1d&uuzVvQi$BITu6EhPUPek`t0wn-j>yQ>}cFbjE2Rz z=8m}9?n8tnVW4w+Q+o`=547&Jz||~WG2q(_)d-9bntXHEQ}AB><ufGD*hL?c-CoGMs0HnY7UPjaQ ziGp7*MN>@0^|F@)Wq*>Bc8M@Pp!m+JVEw)of-?E?r_5-sb^bv(N>{dUB|jFu>|gp$ z^QBpCw;AC$J7z*1hFP4VF+A|Nvp&CUvnl%5K1WQlD{kvl%VE$I;K1=|#D#}9!BB1A zov)QZRgUM(#3ua>x|a->#4^+5JVp}vZMLq?5wTNC9r_X)BDVg$o!bA{&z*08r>1pE zi7^fbL|-j50j~0Q-t7M{W6;aisvq2%0EJN@VP*Oe2R#8)j_K0zEi8}DkbSV|`Scu@ zf&JprZwz$agZH`UTsAgz#C+6fYk}baMwikKqg6RGiF5WHJtct8)|5{Ttwi9?C&S}T zkP@wcnA52$<#s|H54OgUq#sF09Jp#l80dli&EFr9`%|Td!Iy!x_75&#e%{ zD-vi)TR-kz0(43tFJXe9+6-Ecb%nCUFjlIL^^*_jOLlXma`d}g1B{z@MW4zm*zeS) zMqVu4uH(~8Rp6>44glcxB#hitB9quyl-n_}h$uIk@y3=IZ#$(u#oGO~RxM000N@|a z9{f>5H41bN$U{M~lDaW=;t2mno}}9Z5-YwJe%Jj)=ypQR zfSndB8v!hNfU#hdo`O|e4f72wYg1hSk^EpxRdP`oR5Iuj$b<+72o!PPnd*x@cXaCj zg+ZLEf4r~>|G=Euv2-9Oz zZel&rehY@D>EY6kZjJ)XAfkf%@kWLw_syflg=mmH_?A|zGoA9uc*EW zGaDZZxbj=VyU%jKXYo-~y7|6)&+<<7bZqAY{9Z0lm@X|Xzw{sALKXfFCOx^!;Dq)c zupES>I!)g0-u@3zp!ml$Ad?W}_jvBO9&}{bpG$Sqi&|yb>Ei$3D~?U*hGmgy^h152 z@h68E1@oaE z{U$<1Usugk*Tcir2?j8^x}7KAq;L3&a9OoDw;BEi82Qz1tXbeZ*Vyvi71?~wk;@}C zd=MsD@N2x#z2s@3#|ytF>Hq3)#caNir;pyBxu`ws+8ftnn<(-!)^84W?!8sVdY!N!Z)@B#ypI$`mGfjJyZJ}Zt6rrEbrIhI%u|gS1^qOm ztohk|>aYa1K18_`T{t^8Z zhp!ka#Fk(8rY7pEHhJXP7Z~^Y%$urS08Ut(HPLKijL)`zroNMgHnV7`q)k)tca9Gt zW!|=4v~w*zZ%AEb+>T1$3UBsKYPS|O_u+Y|d;58e@mnHJmWyP6mW{0`-0m)~>(q#P(Dh=+ zVe?rty&WMqj{dQ93SaztIm7OEZIX4`;WtEF>Nu_K>RhYk?ziPBN~gFT16*CG5a5N2 zBYJ>uTei|~I%tZoAh}jTvjyp-E%|9?gsg9O9l;R`e{S42{j8s)v#IB=%ry3t;m_fX)E97 zB=tf@yap<|#%v@5Z00uYNyZC7HKKaX5c|fX9UAVQ_9iTIRw(6sPB82BVW?h06u8yd zzchWEc;pT(OYU!56GAVwG=B!}uK^y~^f|;iY#F1DmK@ydEbOmiJ!BJB=XBKev_L9W zfI}RcHsce{BfU~j!Y;#K18HVjU`ky zH-Em-Lci=mHxT0-!IuEYM4D6t z=FbZkyB7WZ3lxMt*D0EwlmhGF7pNjK10Wg#Wz&SR`z_UBe}UpeD=P%nBdORQaxnB! zIL+A78V>D@>_pXJpHTz)!h)jN=GESi&HowgsIe@ldhxWowCBY4b^(-ZJhjb(`<;Jj ztTYNuOfc`3wx8ZV)X_rZarwuCTZ)HJ0ERiIq{*KOx>rBDb@yXof*E4slqoPin<1?~mOr26%DZY$96Ny2nw z5B`s>$zfzEY8LI~2ZK4QR9v*C%*@F_wPp@*yaI(Bhxf?|rDXh>Ko(K($-w+kBU5MmUqMw6^Zm6A5@T%~#`wM3Rg3lSKW|C-st?y}m8BUG8@iu_mW z%~k?Ql;gqyiE>xB>x^L(_tqZBe3*>jaR8dU(x4S)?d0#$8j(0Og8G<2=}!QF9KgiE zl5H_^74RbqK|m)D%OtG8n0Htnshkd40&zge%EMnBqUJRD%J%GRpS3wmKozIk5gmTF z1VO9Q$irw}P@cO6y!#O}@r^98zDt4ZpFxYI8Z9xYS{?I=JJ!w(V3bMR7-oge&EN`5 zM}OBfZ^D#XCJO*K4O^#dDPd3EW&jFQHbzFYE)Q;b{h-TCE3%E}Wv8$sJ9~mgBO#xY z($AlruEvDW5(7u{^0*-eK9`63sEqlIPcNJ8rK_T6(OZzz!wsH1_2xZgd}!9QTi#4g z?6LfNJ6uMpMXT5N1?!=QL8XULd6-W3vw;S-&T=V>OHW?a2;upa{YvLk#datD^P+lk zMOZb{)>aWOx~d3psiV9&j!_(;F%E4+e@8%+%7E%0N{t**R(&lzh>IaKHEvCYMi6lH z!)b1^ySL@m>>2K~x_S{E{^EfQQVfD24CT^^NWikLRHZsw1468=Y5UeCiCgNV2&IVp64C-$?3c1Id}TJq%2V`I#wp&Ic+(gIJtg> zf~Yb@lyMtbHiUQUAE=_xD{AP}8%4P>4sM@l)lv7wd{gW|FHgz7Bkb11!nM)j!0^NO zw7dW#SkTUizr|HCNX@I3mNx$d3hBb!%W(K)S&$4^zia=3js=ZFt8}he3`)$fNZ>XW zkk;XCrF^P}y$1*FI2r&eNTEG2eX}zUa5xelcJ?$^xpaA`Y>(B6)JvG4lZbf zqSiict+^e!AtcrFqr@BpivFo6fxEiagIgS;h;UTkR27Hzez-3#YU!*8p)ndq`0xb2 zxVa*^4$M>Gxs=sr@IqSXK$#zH$h2>j`aBoAK>z0MV+JM}wKNh6Ql0_MLj>kv*6=^X zyOzvqux`Xa&-5Ig(+#`@cQNWgiufWPac9aS0V$#z8??8lm2qja%~i8hFJ7F)p}{M2 ztdznF;M2Lg_jOfRGZfZ5!N&XSPR{V_o;pTrpNUcLb9^$kfR?5FeuTudv+DWVJ{Tay z>tSo%cD8&3WqZs3^pndyTUpnWt(HtbW&j+zqvonv@aB`Q$8>67|6Vx9md0Jp;+5GO zR*Ys*v9QqAg%s}2^r_jK^S5L_CrBz=`y)0g-}fs}mzK>)YmlcUr!{rB!Lf+i`yk>ZCYA%@%i;$Rjv>N?xn=^C#|{Xf7D| zhB-`&D(mUh1G4R@TxwI@^1^Vw_G^{{MsbH4=1AM^_l54K2Bb8bV80W6{K?6u6t$lW zR2-^@XAa0X_vOiintDi2eq0wA8MJuSQjR`F+76|sp?##Us4Pn=%+^&Df1R zGFl>YMxMm!3o`0rg-6*}+7=e4?x+S-oG+*WIae=yo)Q$p(%A_HH8@P7sYE3>w8QlP zxbk7p;jxo_c>;r-!e|*ngoTnLLbnsShNi@f%SMNbGo1%i&dt?`L=evB<4L|9R)H_w z_u~o2iE?>cp>fI7%umAH3!ds>Uf^5MB3uHEtkfo7ezHQaa|@^9?_fsCxSXS;6;hFW`n1(`kSKFb)2=jom?$!V#>}58N zA=Ek3mvom8u9z`1@AwEr@O?ek{t2x^3Dd-b5opE<-vc_lT7pUd9ATH-*_jeh{q21L zYNKdVNM()o`b(|7ce(S!`4eUIfrZqDjYANj45 zBGfwS84$Nl(EdE!g=XH$aJ{&|cGI9SYa}(gi_JMr zqI*jjUCGgX5Z+!r#J^0`^?(J@)WBVox#6oI980Z|(FDk<;@HZ!!_dsdKs$7q{2`yj zmjVBn`quea0B4>vM})Ei-HWyVKfrMvSoku;nI{L8B9%k-)_}?rQN`k42~~tqo!70` z#|YBA=927?ThEm^B(wiR9j?$q4w{-^jX1C8GUG}R5={ZFz6X_*3igKqgsCOEXU^m? z7PT&2Lg;VRtjCnNtTDpd*8aE-;`9XzM-H(oi_b6#h};ih9@D&#ZIs9^xfy4SJ)8|D zoEr|-cUMEM-dQRU+A4qemy{~$nIQMH_{4d&I{*t1#cAi_w?112Cq9}0p;OI<)7zC= z82mU+mYItKO%IYlp=#+IT77+AU!udxEDHIb{~3B8i5DFfz@9jadax#@*rnUwo@o~` zzU_`s-8eE!J_5TaxG52ylLPt|{{f&utPBM(pNn-sibR_464sXk(=VS9Sov(TF|n}a zzd#(`!mPsev#8mNDC#D$mNs-Scle^u$S2dOs4!fnrb@U_<{6dc-uyca!FAy1^RZP2 z_g5hckJC{5AO6*A5aUDIpe~m3q^JRN-Yo9YOJe@>6o8xkcT&l!kHWF;#Ci_IY0=;t zS`AcRa81Gy6T%52$4bj?Wcm~fjZHN~Q3SVxde$)@V8Y?RqEzTvi&<=+L(=0D@q0!3! zh(mOcHJ{#m?)yyemMu$9eV6+5tk$D@{A13xAbGk%1YSx7Y4WR>ZpY%w>zd`Rf&4~o znwpVcW*3ts)$emW?lH3GBRrFmMTJ?k z_L2wSV7xTt~C5*NtDt&0WyR7tXh;n&F(na?Uu4R6Hi)uGv1SN0$M zEfS07ezNwbhg$G$9(Ay6;+DrI*^N((DOWQm=8J@hm;n^V7FvVu#m0o}WNVYY{***r z3=$^nCtI#wFD=szH~vZ1MDue{PUq+a8CvjvRPLy3R(ffhFqA&=<4tBNg#*nS+%Vq8 zd+9t21!O{(3pA-XwduJL^eN*j^9`BM-kBf#sZ+J-(|(=Uu{8}YAPpwj|3&~bO* zr=-J7jhgi*-Z$|`o9beR(ROrw!}N+_2bNgLO3DUuOs|lUTD^r@t0JPXb;0p3Ef2GJ z%O3;29F9I)4RB3iDwWX9sc<>za!4nCG|IB^*_aLGF8Nt08qk&F$n3kH@10nZ!X6(c zU%>EGkB(}xOW)QED*S6p7THGYer)GjIS z_AkCKBxp7)-aNdko({^(d3bfnh_3#=%!)?bT-?i1gi=|S>`f5&yL#%!{vW)(PX@bn z?^CB`R!aX_9J1=q&gji3X#Z6(NK)Z`?^2>lq6(1Bu2VAc$F*C!u;Gi07V z+e{Y9%nC5Q(}I(X~h>4diwPyGhV;cFvwnoB*+8(fkherwqq)J6ZrWU+ew`$Yxzu#>X&ik;9S~O|B zaDF#HgQdlp^sdM=8uf%F!R!I^^Y-W0z5m|v{i9WSW20(GYb0)<^Mb161@s63zez(= z;|0~~__DoQ7Nd)uKdVFH$ny>G51w`&$K}!5egZZKuV2ERT=fi7VwkNqpH&qUiWt~u zWr?yvv5RkQP+Yc&v)&N`n_D)I5Cr%;qd|&k24^gL( z;%^O{Z$s4070Q^+tEZ69&OF;IXtw7wvpxi;yAkQQtDb*puXo5mr?*n~MRz20>C^F| zL+}y)Vj=oE=*@lh@`tjGpqfI#7Z*DiUrWFs>An<|_j> z#pM{mlAmY$XU(5erA0iLeXw^NA#-$zonUBv1xAF207I2SZ`u>P)08k+L0;%UR-u zRIF`^FRq8pX*KywQ;q$u9*SQv8pmCEruv^}@ z0LBofSN?uh^_i+?!$&0r4!D&BDbq6~ePO=}D6i_ygiWt6jzhG^|FL}!GSA%-o>O7f zx5rasCwX5qW#uG!vPORl^@y$wj}g;1sV?wJgcr*Y78x=L9a>3wuHic~D~vviJIEvE zU@e-YG%2MseFdZQ|EftE-#;F`=uoxB6YkVNlAoMbAKvKjwERUp6kl$NkNd{R*(q6# zZcJ0Yx6vMUeOTH(M=0@rL^IB$heFwe%b~)mq$K1V_mUwqdLL71;BRi}O)Z$TK%<); zD&Y4zjb?B@V>B8oViJehqO*Q>M=HrWsmE?YNOn}aQ$fQ+(6z&~4jp%SIQ)!KeYNPO*`_KxWHJM|B3g5uOf z5@Ur1*sY^a0eX)^JL-%^KkmJ8gJ~Ys%|*1%&l-=V_*?`Tqq=;N?@F?D%qZS z@hN*ws(X`T09W2<#l32H<Z71{MVM!=RwijhfVKA2#rQYZ-WzvcDYM#h8#}1P{g@KWKicsbsdpw~AHM?d+cin9+G6LzDdz zBt1d4jSFvrdXiFAP92bD%TtvfY=Xy(`BhxF|C7iH+9 zBi8%%`-ouzEc;?x+*(!h76{1HVM61}lK+Qh&B{h7MdH7_4nPw)2qerEYwM^1c9=0` zy=^BFIk%61bRiR+4D8KM)^t#i>c_w7Fag=RmASUVe ztjo5mAW-F%rZv5HYG0Mnb|_s4e@oW9x$H-ta1vZYZ#j=8f3N;$nTmU&Zz}9DV)W#> zFepq9mP#mOS2WniCc9p^H#jh8u3E_mTdtLsuyIY$Iz7u_G`D|EXMfJagHLyfk@L4) z_%~J{h4mEs|Fv!Q`Du%#?J(-wViUH~Zr6~~I|l08jqN{a<##mra|FZ^_akC}&`h@1 zGN1Z|@F=HE=ZGL#Vi~z=pB641+9>rMYxl!oOsIwy5lL~W6^&t%wC}Z!WFI^GDO52S zq~{kji^I}SrROZ_TlT|}X$S|Hg~MlYrOf!-g?qBNkIT%X>fSyUtuqQBVynq2?%Ejh z=&}CrwKr7eO|CaGb!(!vF4VI}xWfY@ES#{8u(;vu*+Pcht}X~P*D-IP?(%K7CO|8! zBfq%L*jb+fF)5hDM|b7p|My$gaFA85&A9;7VyJI!r;rBXqJU}h?*?_-YiG8gMjefk znRrFerMMp;Ae?q=Mq+~$8aNcT_{vTypbNy&YE@fG%3^CU!O*Ay&Gr~dJI4@jFdU2k zACey|uS-qAypJMqK6EeEx=50vI$2s;s-aKFH6>xq=6$m(y97u4PXKAY_SIRM(H)o~ zE%0mz4#ieS2rkn*cw!8HSnyF*U550!N>KYlyy&}(XVqR8q|*9W7s11v`k%8TZZVIp zDxZ^fDwFGVeMrsK7skt1Lx1HA${=`oC{<*4eKBjkE1iJcULbse{2he%aJeEUo+~0U znKrUng<=pUR%&J+s;6QVGlKDCqidr*>2=wG>vlhxVSMc-;r8v@wLFV4+E2uTwH+~} zPaY<^6|9{z@~Ykzzt*FZC#ST+t-q8y`3Dnn5FL3^sn6>`AWmlK=G+RXv8n1IAd6vl zp;Nao;po@fxweD4j{AFBS17JT#-lP)dAZK3I`-@}0)(oaF&nQHpTNq=WAjJjaq?gF zZ7|I7AUXf9plZMi3)Yh!X1q1&0Qd^r`mQ)b*Q~P#NeU>Kgbe+*%!8IydrBvM^Og_ zyKPCHpz-0LQH(XQhRam=L zK6GnK7bx`Y%&Lh5lJu$4yay}*T_8ddVJBO#4rqhvYv>*1%!+=_F780{kt?>P{08mShhQAF-)NTIWLZEJ)XVB2JpXrEjI|AMEuwu`R9n4c0yLS=5 zk7ZR9!$Np=j2B}FsEv{pYbU2_m0;7Qdr~_MLo?zCrG7IzRo68S>`6F&fX9 zo71VQbDs;gHB6GLFk*y1{={MfeT{Y{VqH9*8_*U;(cM+NXdtkpBj0kDPPmaoD2FNgjIQHdKPkio!t)z3_Ii%$eu%6hzw}h z+~vCJ>o64p1q7W;{doOY3B`Q#Mnd(!>5}g1HO$vRzabL4)~0Z_JfxA1XT!)55wEB% z-=d5rIRh`(KDrvzBI=qB?j*0E>@7FSyI4^`KBDBt&0fK9poUq4@8oZ0dJ?O8*ndo< zGr*&6u}NX9s`zc{>~3v}LYHah^*envESQqP#4v+%yLl7{E?J4#4;QMM(>tmmG0+rf zp@WGNDOVIl8Az^a7j`|)qRcQb(SBsAp{$Abva*L;96hOV<4lc;UR>SRmqEO7izp#W zRtS#hj5)cLqUN7nvuzB=h!2q*a*2g{*!uo1|Hu%Q1Qg3HRvCb^;<+NqJniZV4n`kg z4*Tj#|7a%r*a1b8_prC)5y#|cxor8jP9aPn&pgJ|ax}S`E)7u-fCFC4#x#h=b3mXSUm- zMmTFc98O|YC6XF38rJ?XpYVAz~T>+i*hu zAg;@+b)^QgD{T#}K-QiOhu^x4_zk#6wNW3q7RPvkLSLDgcmP*}@1RUaXvQr_Vmj0j zCLCq1BrcPwo}(^3kVi+-V#M$y0D`FV^2F;f7Nx|#zz*O+b67McEf^!7Cxs-m?5}yt zq;a9xjd;sbQkkm}^YK~TrMqEhvccRWsRo9~&KKTmAcmQj&q>39i^>Ly5#Md)3b-e{ z9`?!>33kcAqU|m{5F!XJQ@vw}-&Mc6T)TV_qNN!rO9E@JZ!=+|?x~4ijq6X~b*FMR&hk-^(4``9T3qV`_CAUhVY@9glf$ly2dp2A3))^9U+QZc|P#@ zC+EkGt@yA@q25f8sez~O;Xx-nFGs5X1}yAOYZLlUi{IBdn?0mkxOglZKlaId`M(|w z!HnS+`2wc0eyY(9h2D&j{&am=K|H2X{PUDQ4Q=e_Dlk4GnfS9y1}2p$n}JZa?Ce*~ zh95m{6{xi4yv*N9KPYqH^oW?L{{bz|&4Kd&0gF7Mo7i_IBiS240gtB>mZRI}&7cVH zxW(eUUy!Qr-rVc+UgjN1)=T_5#zVN{QTv0u-A|FxM$YP?Gx}bkGP8;No076%m@d6d zofbreKw}@SqQNDP#uliKAg$cjIy zdjC9pu2^l2{1dtyGf(-*>sx8G_G-!X&8T9Y<=KSQ7KgY#R6St?r_i(Q2Vi_MO z`^Z7l!da<)JL3aN4L?(bUw#c$=zWKgPuY2OvN-j2@!yQk=A+k7fp;0brb;POit)4| zaphZlP&;ulRh;Exd!V~a#_U5*kOgCDac>Ckp2a>q_e5d*Rq;XNPI5Ce>wY47_q?E+ z(MRhsmnrpQr1+}IjcFW$kV@cC%@L-PQ6mm1UH ziek5S`gTxNrUdcuyEwLu{R73tSrz<7MG2PAyRjTrRwc!xL3^qxO&60RHECbe;UcL} zNdsXFQlxV_I_5++rc%Prf|n!Gp!-j|B}8%)C)PvYsNK}V@z!c+S^JCrOb%;wlJqb| zg|=zb^Z;9;FFMe5McMH9A^MMxatWiUA(ypQ!T-b4TSrCp{qNgn=$0H}K#8G;lr9PB zPKR!!OG3$^8|g;rZX^{L1P2gMBu79%N@=7N5Z}-F{MLHDYyO{^b>N)Y``-8Kx|*jS z@}%uLn71};eN*ke`QD?}3B4_n?-E=6Rn0fY)Z6gS0>pJ z7FG3u{IOkAaiqjU8}d|LYso~PJaw_lLO<~L`juOss5hO8dmG1fJPor%hNODY(D~+H zBb}=VxYequvw*Blp|vSr;y*_352j~5Qy>@U>5D>su$jC^N*s#YKZ=XMJ9Jl{L@R3fisyP>%b9G_Cjp~KeRwoMU|0wwzSG7>lDQk6lsk+rE%cy z%4q(>TDszVzjqH*_)&{&aw&eb7MFkcqrI-qOq899*~^)0cxQ%I(lETO@BY+$>MOVR zj8la%E~mc;72KB1`8ev0$g*VjJvuUEzKN(Or(LLq8@P(u5J zI__ip{PxaX3c(ZxaQ7%O8g+zc<~-{nV{q?3Uw(>&hoU@ z-5v0@0!yzM|XRS?$v^(Rdlj`Tl?`WQvgC+#u7>*uo~- zoQFpJixb*155ze}Z=CQ_*fSW`MtmfHq)T9{!q&?FgoJ2eC}+$(ovroNX2}$Jbw~NWc_#+e!HcU;~HZ)%9yef7^ucxBWY5bK8!L7 z9-0rnLhC-Rn|@Yaq6tuO@|7vF5R`tpNm9s%Ikc-kmd)HqtK-WfsH`o`vJP>YH_cH~ z+g-8BJQ`{r=2mt2gAhF&;&0IE|H^IFnrrP)&4**NRXtWea4b?>^-Me3O2~Y~yRgO0 zl=(|1gG-`%o!W0!x;_bx^2;{NeihYrDS z#=Ox?m-u?2f4d~2pK=LjBd{Gmc+X>teXo?NBY|u3fTtX6N%B;&i)JW2;hf1+cnVz= zmKRYM*>pbe-(P*jq~2WXY?4u_)j;x_mGoHT`LhWR4l(gA;$IKdWm(e@%Qz_V-y_~6 zCbH)0bcipL@I%e0Z!zBco{G5V!@akq+*efr{{f$if7z?h@{Y+V20kXX)3aB&VmbXn z;GdW5I{&fLok(dQiR&)pdx@@2$H!+OAr$VP(to|Pe;*(iNM(P?>hMgH>@{J~=^U5d zy>a;GhsMk=(j628Yr|(LB@$nal9njDwF_kz|6uHjq;*-4#@HJ^oRA`rs`44BwOkfc zIs|UlNlIyr7~WQSFy>n7uGofdgwZ9jyx-@%XoS>tz5OoDTb7V4tA)NN{(AbDfI`K% z;`CGKXxHw;CpSmL@Qya3}1XufXtw=yQd!}BEqy# z1s_$y8CkW1C>=K#YV%lRn~UL}t}v zi%BD%Yn&m5nHt!!Sv5;Q@v5T^jR<`~-Yj~Ldv^POCq+2jolVjB<8$EN80X%f=tVfL zzXZ3>NtiH16VX)sZQeS4@zFII0F#Rdz#_4^2R%BDif|nZ6MVvq=#ULp8kQ%f4k$L= z$C*_CLTT>`V-2%gY_3I;iG5W)>RAFNQhxOJra3JV?T;P&L(Y4}y!uIO?%mXnlJ$Ak zqksBxiP}$emJ88J;}+XMXRW*T2P&GZaicM&iTS^XW!+dC!AE8Nu7T>M_0Y^1K7ECe zY1U6`v+o+*iVu{!DTY;2Oi9&D_o8_@b^G@v+V5jMNbVKF0DDn|g5Q0(HFRy|-5Nzd z7YVV{!S{Q$MB@mEZoHKct=sfHx;>bgJ@Te>>JMs|v2$*Gri@CjC+(8mAb1ws>XL|* z@e3gi#aWX0S4ix}$hmGm%bT@r5F{ALx=^UmX@!SJ zGceGGW1lCJzI*qf{f%&;xma^fS|az%3IpK6^fjc6eLE0bu>!o+o<6=Bd?9gYoOw4^ z0*Sdcg_!>bb+~U9*ncP#^wSrRAP}v(>;pN{kRr_LNW;iSbr3cE=}oVsQT08LV1(l& z=@6O;G9X!lZ4sU@gFZAFc~^9CWgXrP?E3_<23A(ejKeGqB-8?j?s#tH`l`@JN+@EI z)U3$i)XcZI-RnY^I*tK2vjQ@o(g^E~LPH#35Hy}EabyOP=l5QetVk9|PQBePtR;v> zeV?jT^5HF;2bL-#2B6FKUg2@DVwC_R6coFS9s4$~B*$d?73{$v7^A#q!%wtK|G{K8 zslJ;gxxh-=Q+>LF`C|54xlE!WFeKo?98)ZVp8>}%j&dC9tKF4j5hY};+HSeoSyy@u z%o0Q{aYk6{X#f*N@o z<6o}z^`&;u3h-SRYV@TdPkC(Gm;4lHf5!PAkhsqpy5h+i_4u9unC8;=DyzO5+Ckxy z+o*xc5+XZ<`aczbgj>ImL&e3|AG37D0N{<2^(P^0c9f$l)~!HZ3X8@)EJng!B*?WJ;(=(o5#~Sw z1&eQvCIhRjis8bkx~?sr+1YuLCy(iHihST%Wp+a0&1)Z`hPoLEs+$oR!`UN-rp7fS z5^g4{cFscv`ZYXY5A{A@_2j_;wAtf{a2;VrheCC>R-73k2nT%>)&5jL!z3{9@<1<3 zU01IIW4bL!7WP@N@l}40-Z$ThzGY_V``*up>-b{EXI-fZGDP|MaBFSix0kU_a6fwU zKs{S^=CLHEB3flL2GikeqgANF^j@7K;}__SKUGvKGYd($Z59VGs1XMy%aPEg{7L3eKnp&&is|P7nQYr@=F{WNgu`=o#*~C z0l9x*tFlT|@KUnB1eaY*RFyQ>S;F>p__(~FtR14jzJzF;>;uZsexQ4KeI4kau8IYJ zhvqNudl&F1F@e{Vsw(Pw{bG=j6(A$>*G-{B2eY(oa2}&^-hcFYwhm0hemAw@FGm0p z2i}#ymcLagD-vrZX}JO9s`Pgq1ugVoBRv5u*5J(CL?-R=K6D+s!8O`v)G3$)LF*U3 z8w9c0w0Vxx-B+tDC*91wvBsbitNzmR_=(Q}3Y}l~l*26k?4@q&jV(Y<_ZJHBe2c@d z;*F&wrRcxp!)Bnrw)&F)UZE%j6k8wa7Ie@wpep<+H>9$$}(RxlrXRx_|#{%Yfx8eC@WWqR%g1TMU^`n1CGU~%XD1_GP*vDUJ z9jvG3rM7=H`(Z4xt_?=eaFI^BEL9m&Uc9K#&=P`ii6pi>Gge}vpQ>G+q3OLuv32h; z!bE~2JBJ!8o&a?Yd7G}QmoNFvO6bu#OwfRk&7TDC$-HVF+R|(KWQ8Wdm*!L23k$lH zqEVrOwAP4`8A}Qx(RS1UgD$SIPw4ea>r>hk8o`WIgOjRb@&tYTZd`TUA}5>?tyh)n z^L9+J(yST@ijXeq>E5;@Fo#N#iJ`34b~6w!<@N~KfY0bOPBH0TK#8=n^nNsh9G7!a z_nGL!re(bqYt$`x<`VUVFF`_xQF*%SO5#opmm&K>W{#}d@i!S51e_nL*q)z-zV%$9 z@7_qc_tUR#o`%9cy(Fidt=VxU6nBRFc1w*|lWY`AdE4A&895G$w2*_j^9+=4MLbYm zK0P`B-9qI~RDgpZ(EYCmjZ>Ib<)u|_vJY17AW!&y2LIGJr${LOePc)Irucj~$cKkR zx25c8_FIG@3ZHtr(_N7W7Dqo+hsH7c?uguWG9bR>-1;~= z6ngNb?H5F+yl@-e;A&ndO%ZKgcL%em{Wb8Mc9u~5(|GvwI^YNnPRKn>fe#CJ)ax@@!0wBAK-Um{!{z2jr8B1 zpXxJ$3LDq|HwS)tzw)Z=m%dPbHg}4#nWmS$+gS(~UI;bY$^(*rEHB)^(W(4(p%aDj z7bjzpd3QvV5sPa)Q#v%i(o7NQ1PPw~Y56CvK{uJ6ntvtFHh;|AAs)PX_w3qdCU){x z>(9+uSwi}PBSp`%Tq7xG_^NG|yvWP%CwZ0sv+4-lPE3A&B@2Eh)m-%>mMsl6+SL$i z8&QYCvl~|Fy!+O2josS;7@>p+OP@Q!J%@<(u*memda)@dX^|9R;3Dq$k+$U3?bd(5WJIib?Ojb+U|IvzMu-mbso>@Djj*Go ztpN^O-NkCki++dKDMLbPT7JW(S3@UIo3LMBYw3%Ju&FjG2u20d+aC{$bM+-N)EXKz zSpdN2C@lP0iuU8M_}P+@*Hb3Uk&4$UE`_B6&&Et;=VTEf?7kGmt1q6+zih#u>xa!b z=a9dDf!6$9_h&@=k1B61wP^DQKEa(J!MFC$a=IeU1Sw4TQ;skA&O2$U+;=6OJ!7(B z;Z&l>f)AHaA^g zJr$C3ITJ}`@ste>Z#&3guW|{q3wbUlzsbBCZOlBaHgi3lB&Y}jze?iwwy{;Gtf%>Z z)H$Xa&Zl1U|L<=o)X0&pt?ukPA~5gXbAIAI+&Xvvm1Z-K^>cZ)!feA)^E3-(BWk;N zSVkb8b^JG(l|lN-O*8dQrng7o>ws&HXI%RXAYQAIZ%w7v{s; zjx133}7$}Bn{!mg5iZTUTewrCiC8^IC9e#WN8+m_(+8=YZDW~PydY$AvuEzSJN(J76 zdHKgpKQ=b{jarL+oBmFSUC5aHevjecc5y#&);S!W>HD&yQW53X6=|^Hl9(NnfNLi@Ygpa}fQ+9bGGQ_YrE+ zm|URdEMjZHJD9NoTd5HlZK`x= zUY+wf-z00TDCQTEmwj2$qjbz?jwXy4Zn_(7;!<8yD{j(ZZTqT;q9Y_`opAtXBc$?S zI@7p)dOUJAbBbM~+QewYv5rpif#sd=&7p@z+CAF&IhWZHzBlta{Zve>3|`?X*izdJ z^kR8^)oanRx+rI>MJ?3=Vmj+477a-=eZMeXajMu!{`>8fj5#?^*y3a~2#a=-PgbV< zQUjz5Obq6n9bTMmr>n{JZp^=%P%;cM4$~9L5ME$>cX|0s{k4gSet!9*ShML+m=)4C zlyrSBeftl~!zWO`>{kzJqFs_mmFXz6LOrY8jzHmobcb%aRZMoAo0Gi`Z~dfLaN)>9 zuV=e+5nGYnlFX|y4(C@*(XT46N`4t>1hDCef10yXtCfze$`a}u3JZGthNUy_iIPbh z13OosclV*UkI!cc!wMJm{N@PPPPIl_v8EOqo49v*LtURQF}jfMPrWN*n`*USE(%pL z0-`tsmQfe?1nSW$(~cAB5ZzTFgNdyt4-R1Hj({<4#`xlOKEeB$dDR}qUz zr+t%Zu5S?fozy!zlSl$!@9?JgE^SYhkfC@S`|p!g08a6J$?uBb9o+WtQ0@g)&r@+b z7OzPMZ<$y55+glopMIuSOxNdRs1@6i>^O~nn?e`2n6=rHL9ZRYoCTC?E2Z5V6Ea01sj(EG7GdY!RA&u z#vQM!E|pMjuF-&%YN&0&Nxt`uxCA`&C*Y{Nh$EaQ=g5TV&H4r$?a`{tPcZnNIpCgpPaSQGJgGT+i zK~a>TBTvTtBHec*&yHdY@Dfy@0qnPcNeL4Q@I~?ix+#`^KHZ?#+RoAv5`J48aCMNz zgIgCX8O|}Kk`a8>ErRM=0tdi$?Oe9MY&w$xLfQwAa$?B&H;?mpavI(iBgERQj3ZQztE+U&0Ud+szW z%f$7wy?jkn$>Vsr`{Z(@MjYSw$~Tb5elWXxE#@d~d3r|E$fT0sn}w(b)@yOpu5Ct( z_PKVw^+mQ*JM`$^bIN{J;WW6JVQ4wM$I;#$>?DnxGM9|;+QR`+^S}GfgQ~p7d50Yw zt*x9I4zUHP!YN|Kd+pZnn1QC1^s$@oiW;fyd}7FJCGNU^*pI>r4l;aT;Q{xPB=)to z{ozMwKkh1lO>)H*-tw4F7?hTi$~2&o5^b$@Y21g2BOn%iOgWB^y+sydH%JJ@;Mpwq zP0p^>me90ZAC9Y9j_O{aAU%3_z6>+)CuWuwswMr?|DfK#r$8G+{Y=V^;qEcg>+Y07Lfg$u`O3uWb2zwj(PVp=uqg9t z4XcDhGE8SCO&7hQd$hU%F~g6VQS|FxemBqq3j~W%UOGdg0B)$IW9)uuik6AA(Zkl?Sf#00do195? z^Qnf$2UQSW-`@r7X?<-7u@=ZwcrM-E<8LM`Dl%1C@qf857zF*bL8fID27PsXf zKkBzh1S1+};8R!>Qv)?Cu*<1)7lzg3WJX)g)Gz;F5Ott)+W9LQ!Cr4ohBh}_iGTDy zqJU7jGK<%dw9}1AYe-pd^mQ2qsTw=VDnj$d9>qTsFv515_O2}9`N~IPJgQZigmOIj zF45>mV95EZC7ivtt!^HO#@wCar!5a7HA(^50C`{hSkJ0GFEcMbguvxNoUpgjyQDg73RSAk z=YnD?_KuTKa_%9=YcfO8Ek$gjPatRO%AHrLur?6osXFX;(|R?6N@{V46i>1ly-BeX z)sa`)258QfzX`45Cl`25S0nPcogqE&`$KuS2u%}%X%Adh&el-xN0#;(NW6I6w8J{+ zHnMw#jOVBBf+215#6nj;HK?ufoY66Le2P2SGBW=i1+)ddi}P_N4NV2OWE&CAMQTR+ z&+68eK#NUgOFVZfExCUK`7q9H%7%n;X7cA+1GaVFF<(R^0`IX3%-_+lDVM|S7IzTL zz#BT%cj2UsR&DJAFIQk^z;#B2huOR$*RTrB&QHmM*yL?@Tz3&whQX!u?;)XS7>17& z+I_jN74i7ILgR4f4#IG&Xd5)=rtHLE5br3<6-fD8_)DgyS*VbQWf*~%vUQ=VQU-YS z+i_=XA!*4AZsG(ETB~%B?w!{sC^bxXyf~5Kt_YO-8EAlXV&0>2?1=Z*lx!pK{&osT zcgB?Ewc#9Ak&?1BpUj=0m|!kc=wWpshbM}l>1MsviU7`|kYEiH?lXg}E%ofBDQ3X? zi87!F^CT~M$rYbauo*TP=h9!Q*ht!1{vRM~Cu}_7Bg8!iD5s)<4sN<><9f|4T& zhh#!5GM=EAr!$a#9VAPWh^HC)P+9A!W!!+Phuf1zfWX_yj6RYts;hYkAPNXbZ0tMR z=k`0=7X}XaCV6KX5IzkDd-0%pb($r)m<=k?CwaeMhVlUJx9?gYLa$&HdNj9Bcz5Us zQBCBMntYMn+@vWFqPuAES?o=>kp;zVZ($HeutTzmQ}ZHP$1v6%KQ|y+7aNQzbCBts zUDh0UH&!;H-=3%iB?>H!>X}}l;ZdGqMTjK1${Ctly zo?k|cvdJ}}_v}BwI5n*3=T}wp9Fs2}3>AW=dWc5X^W-o~bx5oo3Coe`PtL4Fd1xC8ALVx)Hm(!-CCe7VW`KYyT zSNoxx#ZRUXarqL4hKAzaZf0&d+Nbk>do_%_y=tG?&&`C|2S>72vyUY`_ZH+acdKvQ zThC=;Cz?*L@ba)&_e)mKN2|E`dWbe4I}Tg21x!h4DVvzP+ktyU2Oi$RJvt#$Tdv1d zMlQ^Rd7n)M9W%`4w=c7(nJk{wNGmi*{B4*1uA8_~n6%m8Ek9YACY4uH(-IH;E0dT) z8~K!v7-ri-XJ@8wL!N(lZJWszT(Wbimi4M4gi6j-h*fZs@ht{rbBIm}r{Gv{Twr05 zsR^tRQK5QHGVmXeF1x%^oPxcOkPRBkx~p}_#UGhBy;o2rY%I64D(A<`r$Gq zaes?bmi3vlcw#ddK?vVRYTaN9@)bX!ns+iv#HKQf-xSO29q|`~m5^uUyi_RbkroOtl`Dp_k zB%@VaZ#*w!@6m^4=H^xL4ZJx+gC=iVIHv*TM(XW_ZGlLM?$ys77csAyOzW(V7_|3 z?&sU1-w);hvd?&QetSi)h>h#tF(n=&LSZitqNgiiqi0jk2?Aw_hj%OprKNCOI@u$?Xi!`)mXu@=WfPo z-zVGrk8l1123=mcW4BFzAn`cH6GQ6|n9=1csi&?a)z$9iiJx4n>%uUqYH1Kt3dugM z6`z$k7!(tg-H_Zuviv5C*NsW`A?KwV9JV%k!6M)Np^{%FyF*Pihs(JnjHfkY_MK`* z?>$7aSIf8P!;q6uyByW*N;e8Mo}WeV>ga}2lQ({{M1`6I3&jI*-J>hzi0P#oZL2O1 zC023&PtKVjpW~jFOc3GDLGpVusC>rkT#a+1P}H^=a_1FS+#5yOyhV#epmY#x&SdE# zE|%LWmQwVAR)mG*7Dy;(4I#Z8EuyQ)7jCSMPk$nS?@1#%%i`q--LI>&@pS3rq2K43>Ha_cRZ|M0MR=*klLkuZPi5JUzlEb2bV^vo7r5x1G@+&9_Z$|DG>i!6!USlHww*5e^w7oW0HHGY&?74G=~9`a{MNKoJ(%C3X%@cY)g z_JiAp?-#Lw!-@pF9UZ-R5+83VO4U~wRan0kIxn%!R2+MUC;f}TXQG>Ob5RVN!c6UZQNgts(&bEk0sv0VYH@gTb z4q*`LgU39(6G^*ArL4+7nUlbS#s;#ckhhUZ*SFN~3-stahKUoiUgoDyP@u!AQF~qg zKwB@VapnV;5XI{)Uq=u3e=K9&v^Y!wNSken#e*;a(Or8# zXkOiBk);jD40^)qW`uUT53VKc3&vY!8_oMYfPYAUtb9GMzsnuFUJj1Dv#Yt;Qv_V+ zlrGh5jsPTV*sPD`Dahr`Ef9ZC7uYA@kL>tnf{gqI9@nRf(!$C}`_9%Obyg}ab_$lS z!Ocw`1Wt5~z->qgUIeSn`eF3Sq?|b(N(-94WBz)0hxj1fiaia_A1$TBFys)dNMn@a(;m+rj^jk z!S_CD^1gj-IJA@2Q9qNHp)paPA=;l_da<_Nx)+~lkcEdjr^x5ihJiWJ$RJK^r)ard zZQzPF*f|~FGB7BlyUxIWi>k5)pJgp*5VwwrBag28)HHfjml0yQv;<@kxf4YjZHow; zX9#!~;1MS-)NFL{ST5h5%z;Kt42(40BJ(LdJPiX;mhBhrV>%{Yxqf+thP0VElywH! zcOCltZ!jO;k@-!@bZ`Dpc!MF(&Cq*TwRLU%5)DCZ8P0j+;iV0?v;R+qjl;Zebf$q6 zES0dd4F3-(Lpvw1)?*he)U$MTL;p5s2&1Nn`iVR9x^6TUlF+0naeE zjiQacpzZqj@N0&f$A*c_Th!+IpT0fIba z5xisVgVM1OxPJ?%$Y$hBqNj$`PJ>4@py!*d<)mJ(Is31aBA)AeVni~UUwEhw4Z*S+ zRZRQ&tpK=r<5bCXCi8*R0mnq3PMZbMNtn7vGG!hTfR>`DN^OAhAu}|Ai~GI!j<0+j z5tuW|CMA04O}H;ZKoQ9cI}>5!_s*%yZk)bFGxdGI?@q_)1eWTS#+vEnih8#4%H%UO zBZ{z8PqmEz@2d-Ndcm3!UH%JR&m_Zix7s(A4 ze|If!5J7X=>*FPNWOa96{Zw)s>ZZ-f%@L1$@S1(Wi5qgEQtq?e)((evFGX0U z-DJnqq)>!YV76F{arPS*;mAa)c~VdLQ)0X?4qX9Ubki@^VH`LmOP1r?VM{kwmN44D zoUTo)iK&}i*Fw*JNKcIZkP=E5Vh`~yLl>VGfk{z8HxfcfDfK7m&`-YL) z(UFGHO$6?Q-@hr?EyoQZ$RIp2-&6!z+YS8*{)Z6v#0emCnw7lYJ4&SzWM>F5bZqn+ z>b>1e%B9>=j>Uf?n(=;XP+S<6L%@`a6mb9u2b$v{G?qDssooQ-YiqA(Hbi1oo>4xG z>p$uFfht%(Wu2?oJcR5Gg4oMccVOWSp+Ob(D9w_-Wu##|M8#5L!yR-9jt#WNIa3q0 zXU@d?GRaHaA2@AtspE1s8Z!jx6)SD@gNzJ>WFHvHyDo9PE4vwR(&7_I4p^`d2V!}! zT|`$Id47rU@ zO3zdff8VAO+^H8J^0@X@?>;mMTw863>0XMu?T^&2jqIWFr&O|N=kaGhpxV~f7SVQ| zQ?}~kRF6^I#(`aJfM$~%-y_O%B0ei9a0?zdRA%G_&pEoZ{RZ(bNSSgcCL|@@%g_{N zfLdgZ(eON;3ebS>JvHUd?{$IOhDI(e|CyIljdq-gnjOcr?(eBx@B7Wo zB;2u_n%W>?t+v*SLBynKc2~2C!qUM-ilDG!v_=JHq>a1bcFO>j(I#35+BjSe!tX?? z+4*(ep}k4nB-SC(D*cBnXrbLsuNgatgu!fdxoLFT^w{M3Iqtt>fJEZ?u+x{ec`bq8 z7hv&_`1#J4XVlGAmV#LbLSq!ZmGwn0t}!y!`&%2s2Ywqcxy+Nmv)0!GvrdBq6%I8b z5)FJG6!63h6^QzXL4Q+C0xw6YD2a`*f1o)wr{GtRxCyS};h{;sK=JDIJq{{lBnITyNK>FIT3=znQfJwKc2>a%4Czf+<&B+Y5cv0eMN!+#uK}T_ZOieG9tZg;8L*!;F z(sGhavy>Y&;H&Ae8BfkI7y>CMV~|hD5A!&-sfWoifJ9~-8lv`NU*ka@H-eUe*8L&# zn!DUhUh_H6{;LMctcQonee}+R_Fidkw_X`k=t9M>YspP~Z(Ra22E<&yEJ!&M@x5B% zi>h12+E%y5+?Tbhusl_;ld4H>mHg;`t~HO5x}VeWRny@2earRm0k3qdT}Lt}_8CW8 zojKjtsLcDq&EYo@$i1;)cgKFHyf$+d9@mY}zyQ%vd{(C-%M2}(DYIq%Jk_zxgF!~e zrEJ(6+tXmw3kt{YS1-1XLg#m)^Sbm-;v7AsyL4p-TxS6i*SPnRb}x!b$%q@Ju8Sm zHq^o!HwOmY)`d@&o=los^P4V|XTpnI^Gi%X8W|n*U%^$|NX$!~V|Bw3O3ciUYXNJM z51h!F`0|`*2(pm59S(y-0tsKJU}ovGTjP$oyRCG=yhLrmBvTu=-W~o8)lfIR#$U`O zWOz+@@pK1IT0Hoks<@mNTS#IJyEz;#t>Earh`|`V#BHiI`MxIeJ%YWeCNFEC{>SSA zU3n9|W=2DQ>AV^JuHWz9z42b@*pz9INuxG)5o>L_4$w<5yntO^ji|Fb>Mtj}NQ5&L zq&eJpdXHTXw6~APrI;Wl>ffM(U@5^DAA)s87m{S&9WaTUuX`PJGySaCaMyI8Ne;>+ z+jb|Cy%*-=*ZsgOOP)`IX&|jV`dg-lXi4_~QS-<04BW{4XU8O4Ms0jt8-MF^pY!q6 zYekFILmwDEyYscgX{n8SSl4vQ;C^Yvbp&Az_-7Zf;V1yXu^MbBdXnDF$jiCYx$ju- z<73z@_o>Dr!!4=QuCm5GxBj6DAN4E>!1<;pxBc7U%_l!=-692NsH2FeMSTQ=;)q6^ zHsy49bNKHM#qGRBBqb;ORf&sFyDl%vi#2c_&JCgHXQOROfXQO%te_f_*Xwi`OFGhdpJ z1)Z(Wb(1r+zMCZQ<=uA&#}>7rf~eJiz2wIZ$*n^C845P=g(9cyom%VtKZyZ@F%gc$yD8J-1tYw_*#c zBB^s;AXH=^u05bj(zx#5d_c;BBeu=vu*^wT8}ZK*W^9U)Yk^Rl-@UU!zdx{l z^mXVBW{s|Sl=g})u@46xbk5rQSN)=|efgX4=j`Wv=PztJj5*FYRXIHg%0ei9_x7}i z);T+zT{g8Akw`^1d48w;34PpWY=1F&2J^(~>)+Q`Ofy7YW*dJr+$L*4Wd@kxtsB^2 zw`(k2b1pND#=n65mLlG3-Nquu(xN-`gBD&GC#R#EWxlYDN595?Go$lLb1V=N~??Jw3zAV>QhXc91wm$g5l5VF>Zw#&un!)qp$ZEAOrS0>#vH8IZ@C-aRQ5 zLrz3v^i-zi6PdvnZDu1S{&dgp5pBly@i}f4(FJyW%FZq{u7`u5SOCT>VEyZjBy(t? zc={?8CXX!ghIz^tId}LhnD}ESK#Xnf|L$qo!D&U!V43ZlnX`9u7gxYS_Zx@K?P^fKdxM$iys>GqPdq2Oj&4v)!YO9T%MRjmYoqTM3iPvg=jePOZvf z@4hL>dDgSMG-^TAS+h-|ez-D5M%vKI;#*P5C*YU=W9eVz#juLhm#Ho_O+~BrY$XH) z`oB{HY6nB643uE?IVGV|_|XbL{w7MdxL%cGcDwjwdF8<%E)ThrvJ#J2CzT)QxkRgr zVvg#A&v7)n0m^>PBvYf0`UTm2uoW;d0>&+q`Jz5V3{ns9cPIYSuue=o?nvQu_Gd*a{GHWIPb7aH$bAQKNZQ;0!UBkg@~w@1t< zp2I@a8F_CVoJ=AnnVvGBeJFT{4ls^97Vc2wW6FoN;kVX&Yst2@1KZ9qtA z0LGvgX7RfXjobccbB`K-?wmzlz70_N6t8+#Dlvp0-GHc8nUR*KHFuqs@#{n@vyc~f z`pgkQ3ImRh`*SWY-%n;6)LmGNH{h~7&K@|eD|1nt3?H%eix?rxfQ&l;fZ$?#`yd1E z{6{C}pzAl89^Ro_4B!wbJ0C&XG5JXx#C4yOyiqR$Hn}fjN7r8QAion*_a8txh_(tq z3#602NuAQ{JL@{dkEP^ef#c7|qmeeZ8>Gy$AL1|>qhU2m=XRv`Op|6j2YqrZ%TEqa z2#W+h^YLIHv_LVdwY-~ zN}xynbRr6Ba@OwX0_9psU;mM&6CPh|$TOPhG!KyqC6DjVV_Wj|)J?z9ycn`yXK;F) zpvb@+5aRkP=d1sQC}lG-t_6UgaH~M31^EH)JrcBHdO4r4Sz>titovg}VQY?Yq}) zdS}W6tAZ3YJ$U)>-@dkO5yvOvfT|)CnX6>IBUD!|860tCFwt3$azl?Cgwlq1%?KwPL2omx8VYr@ znZrucsa1W#I{%SSsj>XV5lFVSa*en9Q2cKu)5YieKpRORyL5xDtlR`c{6`JWB%G@i z?=i?1YAFDTndPHdXdRjh!#z-jhsM8~WjQ$*6o)B^^tbzkPRt*zVaszwn|BN}6f84O zJRyouR(xFTmGhoEVa7W5ef`2G26!uaYcYU3LZT3^*dT$23>wBRlTkbIC@y+S?s~c}ZDdD(uqXN~ zLAmB-)^`E>4*`_c2uW_{(J9V1c861;;7q}Vn) zUfZh~QR19sG^6?L0t4iO(B8um11@3<^#$Ey)ki&-m`YD=gs$-JTtgb4KzR^f&3_4E9GFX6HkF6o6VKx_LDw#5BDTb@9<<4X-$?27l@@R6tM z`;wTc-u^vV97OZJ;z6pzxbPxLz`o2d)-%|0g@F}JVeoO{y3wyM*Z=j-vo`a&bG~sW zwOHbA5vx+H{l)Q&N;dcU-t@r*+Af=wicY}*^VxnKgeNGW@H@mOWn8(YaN<~2z|4W? zD$~r=MPeQ;FwAWy47hMPBNDX=#YMcDy?@`P_X0*uY43EZ6WMp7`s9x#z>e8S(jy89 zYWIcKaLKhR(i>cqL6y0*ziW1r4upLrs|*%Og95yE?3u2EjJ*bj3eh;k%-s|})yi3A z9;JXd6qf0U*->GcLc;v&9#DHreF)Kp5*o4woZMV6OtM^&C~E0do49i=LfOZdIA|T) z+8hEn>X_1(tP_8uiF_ZnlffS(hE-mx4#_zcd|}Afho6@ei|C%ts7>T3!CC#sKB006 zyDR@8_Z}m+9m?sQ43I2rQ6Qo^;C92w$zss@bOF-9QwZ7ldYB zI|`;5(WpNdYZzm8pY0Q==*TMj{lMmR9xwgy+dRM7$GKu+jBRy_Pz|hA1LpTXp}nM! zX5Lrseg&A$aD|$T1#8qmF6ay5XV=BZHpf#IvOarngh!x52C1j9;Q5| zv5wE2;bHmJyY+ZZV(r1`bBikDk^JP|bG({FQ)8XhDd%}{DnAFA2Q{)Bd_pvp)7rXL z(R}-65(a?;kgSc;mA-XaD>7q~ZSerZXKu_3Dt&yn8uSA%e}x~VsAq8c=HotHJxQPglLTOUk>4n zmgrCiQtOq~Znpi8$Lc#k>Kbv6G|Nn0-H#W+xXw9p6Gg?vTp74>MQ>i)V?*t!-ymvT zQvN@t-a07CKW^K;mhSEnVM*zf?(S}BM7oudlJ0J$I~I_o1QtmNX;=wCQd(N6-*esX zyw5$)?BBaH*X+*h_xt&r=W(!3;Job9qa3_4!WcT4LypxsthP=46#N4>QTepwjiJBI zo6@ZuP{k)+TLaPC=5iC^9GwWh9&L$Y!(`Ro=cs8kBJLmnvk5EQKDztV`iymE`v9xn0Tz znS1bt?I~b=V4uqJc8~uTme)kz4vsY~kjYJ9Q2C;Y-LR{YBtd40;0ux{(ZueT~ z_eyD+DpL->dsjhT;?2L{BHS9WU5b2MzVDn%F~CeES38!~q)$T~Mj3j(EXHP#eLsQL zgko~8Jdw}}6i=1jGt6SUagL#(E5^}EbiQ(sPY+~|Rk1l4;dbsm*Y)rOL;XNjYppR3 z6@BvtVYid%lC61HDBfZ_O_vwOThm)RDd20T=r}IFSuU|s!PoYDtuSd!2j>#vV?wXH zmOcNvnG{uoH+RUQ*aW~VyCXsAnTYSeNMqxJhO#xE7=#0?RWlzBW39I#*TmH z9-ORzA|p*Aa1VoD$WA!`=O;8>ONxvvYcAh0SL69-hg&W+=*PS=)s-?! z*txXR{GtvtZ4i}7`MDzwSwz#S<;rO1QIaL()#o2gzPfErpQq5grkF>W#I$XGcSwKT z{!Q2<8pNOTBUC;_VJ?L3YOH32GFe5#AA0uhmk7+wh%2yH(tFAxKc2$2(owOqi&>R< zA?1;e^@HuQnhx4*W2hz9mmIs=08u=ZtcXy#X8+#C4OBlzJ({|dHc>N%Xja{LnCWa_ zY)8S9&RFIOk`%U*5VF^58BM(a{roSAv;X5AR|oUb%_=lI=t1w2pZt{hyz}H5c=h&dv;kZ;z6jS|CnH%fS6>7BfWl!}76F=cF2Eu&F z;{I;i>A>`lg}6#vh}UDLr0#a}3Ulhq37&5kb>HcxeY+p#BC8lU$C;Pr48@YV)%2Ul zbJds-Yb%II{>!t;Ct8a6@#c&2rC0b%?eDYt_8&(jxB8KzH|aT*92X5yG5aa{(1I!k z-f<#GFxFMCewyr|EaQp$S@J9AKhwJ=KcB(Y-(KiHCG~ffCkR$t(P=XWumLc<svu8TT!j&3@e;KMsrWl6YfcE;vz7 z(|GM_(er2x|1fr!P(UYmm#O+q!Qh2*+@j%TTR3(apw`>#+!nJ~Brk>qbX$)36^f3F zP&Ip3)l!&=sWho(uK_qI_h)WOAkTG87?@} zF)Z`5^m^WXp?%$Vv*O&?1H179RJEP@*Q{z~J}h%V&oL*eo|gLrzOg6~xctlUETWs7 znXt;-5yUjxvTbr1SduNwNdLJa$KYATEQUOHnqZy~NRiHRq*GB*E8MxVJ|nk#rx5j` zsb)*OPha7ZW))fTd*ouj%Rc>Y@jc7y&m@c9I#K)~0fsHExts-#`^qcvvskQW?Qy6+ zS}}ir$7o=Fm3&&9qMLX8k)ftyhAaM!u0}8YWH#bpzK<&nCf=uf$qUk1In#*-Q{S2L zK|MqIrQsX$)n~_ELRM|LADO@wXRG(>LBnN(wo3+eb`t|nNQHMgfoOg~qvB{*H=vw9m@Ju7mM-~D6e=p1_ znn>ec`GL9M?(ox7_6Knk?6+Eq&3UV?KM|V%hc794q5_G9DvkRI*z`vcrFq-`s|J~- zQ+$6V-4EhB#jpv40MumTlZ|_u68aQ{NvsdTH^5czS+p~zea$j0g&mf79z(2e$3c=R zdO82B5BX=(XYfI6MtWsy;TfwP+%Pwg-NqHqakn@ECDa3`&&8Wyq|*J^nE&h*s8&R`UwzVzeoX zSbMWr(|gtn1E<|uw;~k%;J)u)RNgU<(8}h|)om&q^baoGei0pgWPniD%wq?dn-{4w znwacr)BfbhAb84!C0Ut9wTM!zHo|}`-PzVdpd4evOX>SYHVoLD!{}9TB?jkR=&a-9 zSU>**yu5(M$Sy}l%8ShZ0B(>BnXg_ZE?x_fJ(k_Lw>3RT(BE;ESy+ zf0RIsyJpO`jfn=~Bx#JiS|al#nduAMsI{PNB{a7>RMs-AE{2|8Wl46ka-0-*@lqlq z2`U*PK3g{uKcA35qL5>DN+K;T*aW&5DS18t7^9IOI3s*JULkAnnsTye!_luzlRHjs zt}&^E6xmTEVk9^-)`RHpL-{Ch6w%XOBoc~s_8Xif*@*J;SF|Y;-e3j~(U0ct%(K-CgjUQstxrdF)heSbRUoCAK0+7?vM9rVaQl zu>q3%jB&{GvLE;_KK9(+XNIGHP67*La_x7xi8esuideUcyPTW2d-%c&jWjH!g(NlT z>02p28Y-|)`>l?Jb-4?ab!6F5>X(XJ1m7UfK*qA}ZhE0Y4yT7Mz1^VgQ_m*oXLCTa zl#HM?=oI<6Z$I}J;I>^-F(xH9<}&gKCjLi62s%F0JMiD88I3!KI0+kc-GMH^s#--Jvo=i{$GqjA`={VkeQ(>our2 zt#nSh;F@3^oj!oq#^)M|-qf3_9fO1mLUF{Sg^kR#Uu&itqG3?WUQC_xhC+ias;KmP zjeEF-$%HXVuTHq&IHoTpA|)W9y|GQK2=~THL$sO3I^o5(;MKgbFS?Xe}#cD%;WP&Or6yaRZhuGqkn6PGh?!uVPj`l2eE z25up0*TzP*B_Kpi@lqjV(C7w9>*fVi^C*H#nzd~Dw@0xMJRx-)v98maL{P4TjpN{$ zs-9-P`NsOat3ZP+ojNS$M$gkAS(Zv#!8W+@&eURg1J4DUOh8%wq~1<+BLqcN zgJs9x$0$xlD+0mj-p1WD^ssReiNuVn{K=w1laK$fz67StLQv6KNwn5IM#qGaiJK&H z0(rYwTH`N-n?vCUb_;r}II>El@Kv%oeC=4P#|FU0ddp)N#gsO96b@wgRLW2IGyvA1 ztQS3xsRrH1$TV@ohX5;_rgFn19CI^Q%_*b}s@H$!!!IKfAo78szmB`+134T}t zPY3x-8+lMjxoz2t?g%^M9bAU{RWOiNR;+g!Mf=i@*A?m6dlw1=6#L?_nSN7uqXcd2 z!K4TPO5$~)zH}e=;ErZXK)8}#D-Iy3fE~1-fhJ!<)Y2alXn45`=udh0v?i!o(d7C1 zL)?FWpfV=a#dzLYw~`E%Qgm=s0T3-tvdE@?Eo{YS+Hb+w13j+csOWi6JG}~#%I+vv z!iNY@v?tlm6d1F{EQ5+_$w z=#aJ&W3(4UD)w!to|OysT7-YDp!S7TciI`49hUAiA1EF~ZIY6Jj=a@J&&VWa<0?E; zNZnmfA&Glv-(U6t$t9s-u;an3VX@;?? z8=P6S34>$QpmW)9M!B1xIa0zXR!OrOnBuNU=D#?a&FgZvp@vO7bvmO}7%U8bQ-&C- z*45A~G4yZH$E6JZ^E=U;6CYoCJM;CPxIO0M<1GT3NH$riJWHoF(c@yJl=X2nxeu9{ zETCm}HSj@PF8u!&yX4m#l2^6rid-*aL1 zMO+1WL!QbO`S-H7q8`1$XHl*qtUf{N3i^;rLkv+CclQ(5#GhaPHrT(G=k|FQKl!FL z?of%Z(ObD{*@!0qMt|mNTjbBC)#mhoIwUEeY@1~)sGzACKb}^SJkW-5ifMJSLDhQb z*PvD2xVyOUuTnru%S6Xu*Mx-%mUML}D|(mY_ca(bDndr{y|D?-PP$jX>R#^+#>htS z&2Ul8bcNer)KSaW8{6^AGfK8^Ow%u$MX}Sp9%+jq91EA6F7zXZZ?U9I2?psdEl7R| z%BB5HHX9aGv~>AgtH(<`?rWCKLya0vE^}I=`}U|m#M>#F@`v7Q2Is|q*(z!c3P-{& zs*e@xt^Pdr($;A0JQw|W&ibnxNL%yLZRedQnRjmu)W}uHwT5c6loY-xH}xw-O8K zFlo=JmM|jOEe>GP;r4|u#Ge!3uXU*@MYiz?s%I#$&^2I=<2)0zFA&JU*_qi13}v;D zUq~OD`evktb5{0synMmbnxrV@%e!ofg{$-yMymBn%sew}dVDy2n5L4&YY|J4$tR0x z7xOyaxavS>GzwMeh zQKni=0gT=}TvC|mMjz#$BR^^O9~!JWdV^g#zPbA{xO!$JAv984(Oqs!JxWOAtJ~|r ziMrN&JKeqf7W%5us6pu3*)ycOYrR@ z|46A~5q7qpu;0I;Ku5{>tOVSJUOwXP)?=cVKX(}N3Jqhp+o&$F*vl$^xxCl>MzyzO z2<(#F!a$Jl-Q6#ne~|pY6Ra^YpE$pBel8f^8K0s8+9v~cT|___C#L$%i@Yl%~;r>G$? zmh?^Psv!dsEIcyw>gINs?f@bX`+Vy8#7?bp{F_-K6;x=!j*}}9nEl^Lq_r0nZmnzP zR!`_I|JckALUIVi#kN?=v#IJo_7JJvtitgdvMLq+6k|^f4K;C0oFR? zt7kUb=o0=OzHvDK^X%hX5I3vg?2vVy^_Cv29kD+?;_`2d+Yu28 zHh~FdMfK>0hBsmH#BQW(iHX-&QUnaltd5s1E;w!@GzkU23?y~yg}J;)oowDh3-QP9 z?PeTHl1|hU8sHgrI3hVIM*Iha_U+%Fid#ZLT8GT4-^&{*j}|1>33>ZlkW<{9guG&{ zL4IjUQ18g7GVlCVZ9L0Tf%EkqX$@bJvGlzTXi*#Om07SxSr!W8e0+V(CHXAR$;*+A zHBN&&lrKZu?|i6eskQ)`Og$bs$<%3)BjSo`@41uo6Lr!5-O~H~4h}JE;ma_yM_fXN zgCNN+{_7BB@k+Hm;Y9bvhmH5^2_Ze)4O47<`p}?k|DFlXVLBK5rqdnHNcF@PlwB2l z{>`i2XC_O#SKd|+C(B1tr_D_V;#{t*)@Aql@c#fN%l_sI>#g=1cwoRI5YaYjQeFZE37Ch8W{3Sho(Lu>>7REYkiGn{2XQo?!t+e^6U-iV6u+lVkp~yQb(s;Zw(>l|0?? z@7GS+XWX@y;wee zKbP+pN6UsGt_%{yix&LQTrYmsng%F~Q+ahmr1>ESr}K6`v?hj0EQ9f|Y~TKRWavk| z6zaggYP54V;$^QBwU2ee#x8-Ma`kXg-aCKaw_QNte^(Sp$*ARNkFDxv0UR9MjTmA36d@!*_w-#!U@(R>zVnJxM4> zjbk-W9ACj>@2|OCN>AiAe6r+G=aEnMoU9e!rqx^<1>0^TlulsO?L~=D9XkR@7BNZqkr4GBI z`AW&>88*+jOi$cFz^rTVmJPBB3bRoV^dNM#AsNK3J=XI;Sf%z5g~{^K)`SyNY|W;G zv&o14!6+Y4t}xV9Z9g3u*@whq$1#;fXRD`FjoebKR>J{jqm^+(j!x`~6YtPl1iOQ1 zUaVY&b>&GPs&3hn-N5IF8RBI~yFA$^&Qe{&Tvva#DpZ4#ZK$O{;)xw7tu@DdzO~HR z(Af7hi1)&Ni#3p}RF~I%Y!wo9q}VUU=t4n@q_uY&0a6>yX2>ZIV#w&C`{GezTyyWQ z5NL0n8-f0?p$%#ZG@LZ;jHDH{pEt;$tsaQsIe8%STx|t$18t^nc1Bt82sy z64IOs<#ipoDzHT)mc?dir~Rsx`qfYjT5^+83@WN}ss%gRRhqSX^cZO7#T(2p%OY8Z zE%sXi?AbU&WB5;Yt7%evsBwvAX~_&mG_nl)_YvW`k+mi`ea5a4x1dN^6+z_xb8@}A zFbP0G89Oa^4CTJ`FqxkSM|)#HN}g;M`SU4eI|Se!vR#!A#|5y!d^5l}zPn8V^IGOC z0RbZdZ&aZzx%&0A8+-ls;Y3G~OA54YB2S77m=8bmFtsCD{zZ-R1Uxrr?tr7BIHIr^ zy}@S85CQIL?#X8yX$q2Q%YrYI7V4tBXZ)lQ z_E7X}R}O3I5NjF|`AeJvHI9Q}cL6Oj)q)C( zSYyzdH}~|{h0MtBbE6r#6}rB*5uQ6ZtI4(_v0S~hNzPWo9hAFoektOjJWOv{%hAWu&h>Mbd24FmbpqR z%RW(MY_#Zu{*B>wKslwBL@0pMLv3vZsrvSTC1~=q{~gs z2eInWn3;K$riZjAU{H(ipzf#MQ8-#o%78yP#7(lJ<$vVgk*LrH%E`AKl`z0Sv4VnY zeERjpYO`GU1kre4BK7-{mj1?$mncK3R0mE0IIS+L^WHJa$=&OYEJ&n9*e_ZSsq+%j zkX@q$_zpUAV|4soDhSp-1n`DX7m@%f{&>LVS3<F^jUnDOOfhE^E zGP8t40Hf8j`xj1v9nqH$Wy#XEt|zx6CB$Bn32(bkDhP8sH2G`rC!C2sCs)^1YY@4E z<02e?i6`EyNLf_l|A18>GW*qeT9rt9hec%XQyymA3R4`pjJ2LDI(rN=6-S8@isiM=>8z zUR#mW_$Z3nI09e^e6}^yiaJDDT?@LmiG#@Da({4r&cTBC;qjdy^J);tdQgf-&B5o> z;Q!Ir39wN1kOBA~_dWgsUOrp?ravs?E*cw`n*bs_9~1_O2;YiM3%_g8-vj_1ZlJ@n zuVhi)^}z(2s~uGUNu?)U%s5$u&0E%yM;MT~XM(E7qRLp@pw%3{#GclwL1ki*=z{5n zf&V$Ke)1wYt36-Gh&C9cm$fe8)swjBdJxLZgI}shfR+!cE4u5?$Hy1|V@Z*{B9%91 zGhD}N^bQVSYh`^P@T&u7mH$)n0x`{n_`Bu>K zCWWT;h;7(~M6=#eg!QiR3jPvYc?=89CY~$Ay!4IrO%OjW7Y|A0Z&Y9;9uqUB$6Y(d zUZtmM6QZ_vbW+)BvQ;q^_%1m9%lI9#(odos*DRj?=E(J9-l_f%y7rp=_^t&@1D#xr zoIKZ7aZa=-p7aeJai_HW3dcFDo>wb9Jj{{>L`7DG&3LtKeZ;>wO*7!iDdeU<9NhIz zA)J_h?(H*qTzT;VQrZTMX7*az)r=FW1V|NdVPH6Anwt8DVupy}krusVc2}|H1|%mX zQ~?8yNW>#$6FUs_9R4m^z1_#wIPH}2mi?w059Tq3$lx zXr{yEaNaOSy3;4MP-77ow(p($2I0icdMD@0DBdZ~-?f*N6JlgubxdP-(Ye;=w&i@t zNV64zy#|nNyQKJ^I(R&vc;dn7L=$TMUg=K^TAlF6 z=xG;1y)^}H@#`de1XWZf0@1txL1g@^PPRMk8cY0e$?-m3nmC$w8GL% zzoZOx&3p1MXs>#nB-A9n%)r-JSXfXwZ%K$(PMp9=W?K}w0L8u0-t-xDHRSty8L-wD zSS+08qXC=}Vd*1>JQeKwl^%Y%}7#TIon zZEP+M{L{7n171}m25tR`R~;Jav(I0CmhADCr3lk`3`*hXJ5L#NSVOmwaY^O z-d-ci=|`L6)io*9nkeKK{T&5;^w?QaEUJj{gpQsz%}1jcGjn~-F|E88#NJ>bWS+1c zr5N{v{(E*3ADb)47z%)xpf9#vzzSN5s1tzWK ztru)AwGIo_HdM87ht^8Z2HnD#{C{$7|RZVt8tpZ2tGp_IELlU01xs7K}{nT@MVtp;ymsooK3@=6O3orAmQvIv(ho+Ak;!m7s!%Q8LC6%?`m!d@WLX;Jh zat0+|Fku&?GmV$);*rHw(nffR9=WMBore(T8vS>(d)@E+xWFmm*3oyzTt=qiIa)xoWG5j^A3@NRa^_@1|0+bg5Oc) zbZYHXd1mt4x5h!z{84qOCQ&jvu?tCtnQE0A=BLXGI)xGwu63Lj-(Hlaa0mp2)N$TM zWYd>rP~&zgEg8=WFgq_+C{kJLF~oS1ABg8>R^_N`L94Be-iNiFN=`D6FJAS10#Bgr zVrpOS-3lr_`U3vduk@+F zaciegpMhi6@8SUVrDv33ZGLO`+ppr+Myd5vzYn-t)RY(D&4=zGbGr{o0n*G|9ffaQ zraMQ`sVV}uZ?=@eur4-Fs(xItm59pV>q|U(-)(&5kokd^Afx~2{BXO))E6o8&d24I zVa|+Dq}y24Nx-QzSaE+ghyD3I^(D2xk^YK240=u3aR;Y$66zxHi|fj$o%Pa+bxm>$ zBvDUkzvovq8FHKPCE`C>!(X;i zrUL0|uZb78 z)v4@CCgf6}5wk;=QcGiZp4vs%z1yN zIQ8F87P&>1jel|fTIU~t5*3Gyx#4?fHo7zWT5hHH|WRt9DywTh1J(cZ{JBk*PA0rXw%Hb+myFzX3x5VK0#@MS!D7iwH zTMbnK%GD|W6sUUx#M`UtsDF6_z-61N;X`w`lb{7-3ggVmo$k-LW1Ap@>Ch!yQvx_G z{vXIySFG6oo~a6C6>@M}x@!Pjf3O3lh*N_{J6W~!$?^7X6G$VWR&@2L;9y;ctZp2D zHI;gSk&+s54{q1@3LyM|^;gO-L(*NSXa6G?xH)H9DP2gAOh83#zu!EyN9F4Ve#*VAdJ!ck-(JXmxmfbGwYB5+l2Q2Jj2>^l zhHdJx%qr^!ic2s`*d>68gHV$v3d;UHV4wzfDNvDv^>E~=Fifi@BfbTiuMU$Ag<5ik zSKwZ;g;{Fo!|6OOI9+U1oq8WV?W1&6U%^{DQ*|LKOUH7LGEMk$DT1|_(=bYrqu|>C zaqFnr&@2W=*)m|wVGGtW~y@$IW_XGPb=F%jhU#;~)m zeP_-p(>U&C?go>#P*B`-9fVh?kSpK|khgN4CjVT=lKMktuW43v*lEXwJ0O*hHFMCyworsJX2V?yi9tAHJ}A^6w1;5m z>BltdAxK}vag_P7k{CyM5A=BE4fr|?@aMKc#CQ%lcFisK&^K4(p_u74nF(JcFMDx~ z+TYm#IeW@@n>;@)P~CfBG>ed;5+%H*vLQfn-3tqW@TFF_s1JYdS&Fd9(9}ikwz`Gk zcMXO^u`=R*p4VUunZ`2oY~sS)8X}^?4P98GNACUBY;a(ix?J+OB7;uQ)#eZY9rDj3 zVa3cWUB3O4Fl-(RB|AImgQbeygnJ%8%07T)MxEZY1SJ}}p7-vf(^W5!MXeQ&yzOt+ z136F?Tvkx_akVacFYJ2}fRvb^LZZt`;_7$kcW|`CXOQbIy6_c|3<672)Jlt^>XGKR z5<$r@TKh023g6ZEa_uJ?=NTudURQHretseXl90*!J zk49^t);xW7gh zzp=_|;HA|d5%L|8%T8BQ3#Epi9em&;u4vXWfb#rlX5^=dQeKis6j;Om)_ZTSYlQpj zH%-VlbLycf33lV^^sff=eFk3s;c9&t7?E#|e6XFqwX)kb3f+B~`@~dy6^&}Ab}F$! z>O3M8FKwSbzh_ouQhG?4Z63E;&P?9Xc#QrhfP+FW!OXe-hflSsBihQZylnVSJMIJy zTqfv`pgq3}oNq2nx|@Q^g&V!oT7Oot_|7YQi0S2h!X0I2|G` zisaAFa^$>Jkee@vtCK88$W2OH-K#6(WkS%uW~wff%b^FgfVc8xnsDbf=Tw8nUW}!i z4Kft*?A?qH?D{&op8rDmfJPO|m}MJPkY!BKQ|sUIqEkv4z#?g+vQf^3wH&n|P@o2O zTtk6IF-<+TX(%@fgSGwEa?3O*x$5I(L%1Zo@Y0TdN%jDyxPi(HyOi4PLA2=3%gdJK z>b8@Pzp@CGJr0Mqmr$bFs8Fdhkg;gOpmjRzfmsbkGy#a=^9)(I8^Aq7;?BNc)gb}O zUzmsvIU$hu;Su3zTuMK1A$w@Z)Ep$B80GJ@-n0pb%!c;e_CeBCBQ;u^bFHSpz9KQ+ zMdP;?df;Y|k-08n!U$g2Ves4JA$G^RgvdvRI3e;o)r(+|JCzFnj4G))W|rY(2fb6t z9S8_Jyig|JqnEekC>Hpf?L;^6M#sW`wF;p1tz&uNu7rcUgM_16Bg#N)RX$uM|ZYe=lI^D)qM@0J*<%<;D(Z?9eEWC^r{KhvS}@4|XePv~(AdUf0Hf zu&EJux8TD2W=DFJ9lm&SJqx&xW5&Z@%lzKsfa;-CS z?kz1_`sI2-tM!Z+>qMm*{8O0KBY(-u4I$aJEDGqyPh0n!7(+FSG;NfZ6libS;ZjfA zkQ|a6p9?hkp-3#5kAPeht$GLHOd16K7avDfQ^{@UfHt!W{FD93Dt=X1j}Bu4wPjE{ z=2s3S%5aohkT+)|sz*vG!~bl;clq*>4=4*eUgP;alxc%$`vi`!gdAzVguBRyXFINC z+9(9y1t1_kxeDk4sgUpH_)_lGn__#0lqlR#X#51dQ+3#N&{!8Y}f4EQ)UnttxvZ zr%WwJ;QK&KA_bll#EJQ0RGF%!GQACRNVr{pp~*^0MckBGbxZ&sl0`(6veftYI2Kc) zuuW?)V@KG`T^J+OaJH=?o@S8LtcYaj%_$11os_nK_u6kgLz%&cu%bUx68d`J{U?<; z$%x)%9N6Qamy?Pg>Lo6l6P4uOzA?-4Moal2?H~|eiiYKAAmr;JrQu$MFbyseA5@Xn z{5-AhGzP6PlRrUqlknNE@zQ|gtoolLK4w&~Cbenecm?#M}@^8F4lL#4nRk^?bNKb>{cCeViL; z0p(hme}3A^5ly_Fa;~f0weq6ghbVsBe6(m1s}!{St!+OrtMTf(bpSMiaP!`eV@JhX zUh$W`dK6#7bMw*VitfjnV>0U8FT+Q5kcvU1Om$SPZd-OrX_6=}byujUw=QT_a-?Ufg--Ou9HUY3?iyq4Qi zr_rY0R4tiU7-`l`$mpVXvwRG|q&t*o@s%dd;)S3yIA zf{sF-;)&@TG0VfUu9+c2V|~>=LxtnO1c!lOev^t>x_;M3Jua8Ml~QrDc=701H=)-y z`L4Nr-UW_fJ+I`mz{-LiJ3euHnp!Xs7Ax|HPekih;q%g%mtCo*{Wyr1$yjfqi0~ss zI;@7zXqD=?R1x9i`Ss-U8knz5E|0z);* zJ>m799H{NbB3&>25UrNQ7Y|=^G}E^dDk%etkevQDUvH$kx5UImlJTMWQ&72K`R}vO za|@;&7&rV;JNp$@NlN|cCwvdh4{XOX8sA)ge)l=oriE|uK|4UN_x5^dmw18eheM#f zi6Pon!&7%XZ%UKoS9Y38^N3v&M$0It9*&Dc$`<%GA+GSS;mV9G4c?8dYQGg{rY_ zTEd}WNy^7E2=QmPQ!}FEt3xTZpWqtRrGnVdXfH$}bJzRw@TeT`+{MI$F%ag7%h;D_yhU?C=sPm@splOknRC zkT;21z9A$zv#v)kVtF$3 zt(j+j#Gcr14Rh0F8|Ld^8BxvT5YHQC&=ET_>MSkGR{naTD=yE?7Ove#r`bVKk*FHI8u(lPXS(q};OvsB5Be^4L1X+sz*UB* z{%9`x0x!wJEXOmd*j^R_gHF16fM?0vuN!#(1jDEqo?nyg-;AHXRFA!7!~Wv^(OmpweZe21dQMo=q_gTe{JJKAbTEXyXez=bD3P>p8T_Ldru_N| zJIs3HNufFJC*zai{$s1836mLV>^ozjiX>sVAeX4hRNFU*7J9;inAESS*r#WpzW8}R zRo`Vs|XxXQp;YE5-{smI(;FG@j%3 z_yD;*m63FTkTX__;c2>+jSsyVx?!&E6AHW$k3HT|e;1qI8Lc-T`Eot$=jMOFZ{hqx z3)q@1RGEIVZt`0tx>*J_v6XTaU3;jMihRJM_cYX8pv-mGcX3SLMksn=2VdMw=`5~g ztzml4XW6jZFe+&qhX@kCOCCT1LWEL{2}TxI6b0gJ991 zUzFYxN%cb?rGGWV3jU(uOyF8(o?_oASC^a<vMWUKT_{jRHh^lliAv(pfM;ao!FUv@(ss7RaLZZ^Q6mJz~1&C2lz2YH(y2tuRdDf zGpt2S;k0D^6qv+6rMRWF`Io{tQpfyW4!43J=Wy?@F+#nT^nfMr1xsIH$5Uo(BBZPi zrqXC7CL+Rg7fyjnCX3vM4*6wSsT)S$C4%|*H#tyf851?Pk0@rai#7KnUk*!bW%kz( z!ML82QV>*p`%+FO3^|$~MpI8q&l*$6^k&Ae^apiq2H%$)W8Q)hANc=+@X;0OS|DDT ztcY^8#(FiRK=nng(cey(sJ=x4H!_qDJ-#*DKW#(ZrSDVLCxxq*C<8V5M3JLfStOp9 zVLN>22`~fK4DwXu22h$e(8JHBX7mvI&w|zlBTj(dc~Ko^C3_YsorfT1q%p`sbt^k| zpGo}G3}??zqDj87|;cNH59Fmac+8+*qlNswKrWrT! z)<`qm)sCTHM39vA0 zsgWETdHSwBblT7)N7Z-gYv08uMYOb7FE>#GvN0(#FC@?}(jTR=vJ3`1pm z6WOGdZ%$_wCo!_v48mN?3nuPBz}>bcc?I>Cn2~{ak6sZFd6~^#^BX0j!!CeJ1?2S3 z+H*A^DB2TE30CppM~{EPU*xPbop-@rchPf+limyv*{SN%UD=@G-WRi zMnNzBYzYZ4%4a?QL6CMQhvs+C@)8hn&6y)8#%<|%wF!=)cGtpce>h+Y;y3s+=o6db?<7?kQm)N10%SA{oHVUqULXhwl?RFMdD35%JavxTd# z;co7ec>du$0}xtELQ7&Bl-XKiX5YOz9>dVaL>>eqx-x_5LkSEEumkVb5=D$CeBEkv zU$y%F=du(6&@V1(@LRuJOHAo_+P?xXzYK1UGx#{Fx@9|$seJ&R^(4(eIF#1?G1vwm zkr&KC%RzFi-q};(I9PLLp}n#5@Zfd5zQYauQ{r)WSj(}(X*rTB;LWDO*!t?WYd#b{ zN}JxO`}hA4b(TSGwNblHaCes$2^4pCch^uT790Y_i@OutDJ|~M;8vta@Rp*%3KS_$ zTO11S$#>4ocV_=)2+3r#pSAA$x{`&pMG8YxCRyJN=ZHUEK&}vYn}obt;}fvj%6WS+ zpS`H?QOo|sExJ@AEK6eskHp3A9TMI~al}7yH5{4pu&4Wn7ggL6tsF~nFk0u zMIl6a0iuY~Xk}jYF_8V&Q4-Wi=%aWcmoP?CWdq+Ovrr{;&qx8YX{$y@$jn2-$aut9 zpW&ON85!pR2K!Hx?T{c7mTsm=C<~fsgB#mV&X2b+Sw+4TRmjb)L)QlBlDoQ!3Nzc) z7=n%n0%!>56MbS`YZp1TvK;rxxadIFkv(##C@!diZsLDqQY!v3Stw_TL z|B4NvU1z#Ws&%QzemX+UdR$2@t|W~d#C;bZZApYiqcuSV<2mWmm;W=Aeq&fC-57!z z|Kr+;<=UI1ta^B%#yVM8b!20WN2t={wunQVl(eg9A5qOZL8NA?)HoLlCKHLqB|wV! z1O_Msu{?m&oZM^JC z-U9%VrnD=I;do*04u7--R5|gIyLRK~{X{(8TyOmyLL0%bMax?EllI@R1@=pAV<3RM zpZ4jk&K^?TfSyCsY!jFM3Nq{TRuBKW=SY(M1&w&5Qv6Um5Z_3kGk&<`zayXE*IOtf zXJo&!RbsT6l167PwY(8D6|iK@MBq@%YK);58_@GSP8W%;5QN_VU#Rq@AmfkF?Sm+= zdPuyiM$u%%GTB22>5@{|H^)8ebmvns`IxKLf6 zMb7nhL@lZ!ZqZiYJzZ7+&PIY%e-17j?2bXyJ_B>3?MMybl(G zGg0v)12a4T@!tkc!?)^6{Bzr)Nvn0E&9E z02(;E!H9abopjyv&H;!|Yib%OCT@LuGJiKmS}`5qmm!R#>|R5$s=ZJo=N86&J7-{U zINNCi4}cTmFXa_dh98Md6%d$P;V$VY71dQhGUvtXR<^jWsZ@p~%L2(O-+aLm!bO^0@KPvHSd?1Z3!W~c;K-^xM*KKy@- zVTm~I;@+vd%b_XZd0Td^ist?4Slm;Z#QStXX_2|NUqjx2g5KY^iwY|hl?$nomD!8C z1E}@2k?4s!bnH$`@Fp3ScWIvAsnV3mUa<%Pc)mga_yO2BnK=%^9xDPhwJuenQ2n}1 z7uFh?Onm;_VvU8xkN=dZdX+80*rAywYD*m!sU0#gE_J(A89V{49ak|8`Zk@fLJOwe zH*@9utWRz!(A5XE78M-nsFr2D0>#7ay)GGtP7*dnK#<9{hIl@PI8>CgWH!{hUX7;x z?X)BhrG>Qhlpq^5dmv~J>z4H-TC7IP^T+FyUP&XZ?)Fu(XIx6;nh-*Myh@Gj|Ok|12$EMSmsyX@QDYo3*a z1`hiv?l*KAI|gMnb7O_$zaP@WAtz|8)c zj&Vi%1sg!Sh)|J|#I%n?g;pEFxkWScIYo6PCS_p`9ELt2o*MEWBz+px^+YFE?`}w+ zzdCl18TDwKjMB*`xX@T{h5C#g&(0$EMYno)g1BGR?~S7gYvKFJ#ZY8H-xJZ(7Ru=F z(7nqO30^gy8ErIUS%z;sc}FOH?|w?1nnd*EQ!RF&f1l)L3aHfF@5=k7g{r&qKtry+ z9J)uf*xxC)g%;;^Add+jJO#?TM-1La)r*QzD!AOvKujL9c9CkdhB`^lVh9_-B$u9!T2CVzQ+ zf>OT^eQr-1w{8$U5A*&|$}XvzHG~2hw^!vwJ32KJg)9gzQ*vP6mi&8ih`IqBPM(?%o zVb9QeZ&>YqWG%m2s;Ks+KGomJ^DbI;rWL1fS~Xd%a?OKBvI8ja9Tr0y9r{j=H6>I_ z8M#D9y<32&V?Gf>}HUvdcwQEJ-+jM zX+tSjo!lJM;8k;NB1coJI>Y?NIpek*pj4aRV-VJlyNO5DoD_jVgswT8iHsPaL?v4d zS6eDWreUL))NYZ%VIM`^Qr0cr*VYA9aMac`)-K~~R21TOi`jn^pq7Ou8gjC%x|ESN z>3M1EkdX5jmldGc97{q2IL5oJqv)VG|HMNzsnI=k2(|G>@b{TP6 zPByj(+c+}bCxg4KvuN5trfJT&9LoC!%ip`If8m@JEeK{4$IP&Sxs?&PVNG^Fw(|Jt z*&EH=t=LKxDLCenez;Pa6x+PT>ACzs{jLGBO1x4oaX6aeAH9&!aabDH1#s)DH}3fpED-9_mj`2IuR(pK(^o;5?U_6UtOtn{!bE3eP1rpVpJvp$@u-cQg! zN>ktH1(6+Uk^B{CImCu+(V%nVI2tsuqLpt4L4AEu02W4aPF49fFFM48c0`vI+lAMy z%&phd?nU$wa29|!;~7Z(!rjvA!QRa(&HwX%fZ_iD81H!Y3%#iB$`DCKOAdu#6jX2gCz z4+ejn`I))LBeJ~(^d)OAv`HM`Hq$zRiu_-#WAou~W~i}O6qAIXL3t~-zJe!ESDB|wF3Ia#_xX7l3ki4tk7WIC*~)ddu!wy@ zPcM&}3kqj_B#-4{$j7UI&Si+nHp|@sJfYe%ss`)4V^5!+Py<^4b4T0gC$iOyj8zT;<3=vTtPF#52M8i}%npJxv@`Q$zTRHq&Hw&^%u4J)~V1V}?>N%9! zq>Je8HYf3F0)#RLhBf=3(@oTfM8;K-Wtu{R)l%5fJLF+K>?{`O0?ZPqFU^eL*S_dP z{bC@=QUQYD^>Oj48WK$jcdwQ8h}V`lCp(1rulxtG#$pr6S%~Aj*;pjTnK$%FddEsP zQR5B71Z~CrxMS#H9h0JL+-Vt|v&Z^a-MU}|EpVWZ67KF1CL7iQ1MK9T)kS*@aSmGF zy;1OXQgdPvu4*<9gUzC}dP2!6HUc7+uE5O1rTrGMqX-WQ;cb71zLqpyaRTL+`)5hV zw#OwLMLUlFEXe}lJ*?Hj29SxN5(OAtQ~2E*O)QX8VoV)jGv7MnS&Xq!%NM?nTrg$@ z!og6zm)ih6ZE+#=A)oE`oM}9q98Zf+~*LF-!eL&6AdMwdsR%U zfRYjA*r0~Q13;G)?gsT*rF{={^cIXT0XiO;-VRq>e<~Xt0FQ)YrnaXd#(#hmn&IdS z6Bz<)B?tX{4>u=G16}9512Jj*Pt{nHF!z$VsC@#zsG5jQv_x6X8V!slMDcakpYU!h z`tj4>Ydf zS4@#pLq|*`#2%yp06e@%%WAj+vZdo;rvPm$nN)epvp+bzds>iykSF;;Uv)5lBjN+C zfSp`*+Rn?b8kVR6N!K+ip?xG$X9{;7J97lOfSCqz<V_ zfb-v|L<=%f-5reLg{QXk{@d=FoTD!pp@ZJJF%5Okq z>fu|Q2m_4B`g>a+&o0)G;abQ|MB^e)kwTKqFS0mit9{mn-$bVn9b9 z^d7mR179$nP*)B~=|ZI*k0N=11*&1v*aJVexA6%MmwOe>I=%pkZ=VD(qnRP$>v=u> zV}mGC6Sn#er&){G3zRw&0z8Xg^-0UFeW`0nz`;m__1Z0DK@#uaqA7_GE(Qqm9yMg) zRduwsV^6g9Etqv%fxe%CbBI>$9G+ZcCKF{ezTGF7=B;< znJ1r^C#^$5MnO-1;aQ}vxUs!$r_HHmgwl=%GW0QmGMze~KOohC4os7*G||oRsr4M_ zdp#GP-PCBORGmWDxy<`qL`~3BH5wB{{hq7M`)rjK+q4*ZRf7)WHhhT)>M#&%jo z&JwR9lB5y%Ouhp6>6S3kQu($e(Q>NpZ`btJStQmMQ!xP93X1K6R)IS3D%yK_dk6ue zMd~NJ7Xj-lM1XMc3cjR0qiWOo;LQ-4XX6>Y033sK4mFD=tRt|KpXn_?H3>B|R+X~d zGc+;I`8)a;QJf`M$C|9pmx5DF*b{sL9mYUOjbjpc(Tym=AkE)+RqPYiUMk-#m~|hU zS$v6>)vLdSXA8opWAg*mIC~}>B>?#l>Hx1Mg_l1&(G4izrUb*^W~gMb3v4-vTwJcn zrs{|H=wa#Af+@`7xw#C~c(f^K)FC$26pF9&9yKWaVENtB;3xS30D}M5Jh3U|*Rt$G0VuUIoLQv5{f#o=4Jsmti>Fs+tFWj@ zABaHyL=$cmw$Pl{czJ0Hr({-B&pvg-&j{qv1Sm_X#%ODSXN0 zq12-17SD~_@RUmbMsFX>+P@fY{}p_;5OEZZQ0>Tr3~G5r6KcEfcD)a)9@r%NL-a zGgS#FAUxLIqIsS+rq@r@o)fi5^Kbzi3A-qnYQkpt_t=ajtng=mzd~(ABVgeM1`bSMl6^n>tLPMoBGCsRxTKyX!Dq~ zEsg3kp00u2ouaH99+53|e#@d!4G`b1+9$zw3hezC_LKXDKBFl?z1OHF9dn8!f zQZ44xDptk#F(e`nfZ;JOV#k=4>5l;>{M2Q)0&G-@8^|-NI527oOB?`#-73|7&Wx@a ze2Be%w8h|v!>K$1aA0J2qSKK~;gQ;v@wW!DtUs#*qt5-2E$t34%XMhy5P*j|?+@Rw z?!Qi~y$>Hl0oN*U1(%!`4op#<%gC^c6<{=ki?$$>mHR&cbq)bfl?GAD$WnWBJY7uQf}&gut(HQi zhJ2B3r`+AoiL>`TD!Npr3iC_mPQo%S{*^2Z76)GYsso#!ZiW{!Cj-zMF0xIez&C0=20>fI;TIlMan5reWXZq( z@54=4o139gLq0w&xweTWK^#Hki(@a_5HMrE;IBZL;WmM)soj22lUxfwe-Gw&W)LRs zjSWT!Y5{SBa1%vk&i+2*XEHp^)7&2tUTNlV?M6{ap$xt*&EGW^HN&QKrBW zsFo)vuJ;s3HSnC^@mipwo|Be_Y@k@o+N9}^;?gb#nwhqRKN$|oulYv;KSXdvK#Qr# zwa~H_cYTosBwHs-uExDq>`LsY`zUU8GR1ak%!FhaC3P@TYT1m7W|XG99@H+m9ijA! z_Oqe=ouV+eS=}BL+u0sKpFU0Rry0mPqRwirlYvY!n`)w63RpKpJ+?V5j?WchSr!`c zWU4D&V&GvKlSSc`j28`MyPrnLA~-I&D$Inx%e_1>x!;N*8?jngaw0X?)Rk)YMsk>{ zE^g;`6E31Jgg|D$aXaZzr8+vf8{#*LylrU`?6$05#YMv5op+|lpP^FB-$uy3X}+)% znYuc!pHIyCN*fXrUXZ(*mb7YS{sLlhX{==gFts=IdK`a-Bn@DX(2H3(856Q-(psRs zDHP4pwj|fN+M!qW_oGzZZ%p!2h))i#Eu583Jmw*QPC{DLt}zH4R$DTc-Z>|bbC=a-nCYT0rV|eK zstGoNViLcmzM>%I$am$vR^ub!{Uq8d*wHaK8}6jbqkiB$OO_pwF8vPn@gD|sN zg?UHJkd7RW;A`E~Y6rc3yHC*`i zbiCY)VW)uMK(f>N=oI;yBDRWeLDNF;w*^u)4=OtO;$Dv*E%1m)T@Yg ze{v(0`PO;t+SppDsLrkNxd-s=m`RrMFwZUa>BY9gGwt4$m14;sMpGv*3*~Yxi7j0& zwHtp7!s!&Ck8T&u?)z+KGl_P;L6Uz8lc9p!rz>f9(cQ^B_3wL#6kHwt1GK?pC*kN= zGM0jWg@>Nm-W@$^zUxsg@NVLz)Uz4#_o+ADPb0k=u5`&7Kk)VTz37zH*xufmPKV?! zIa|d0=TrW&;3EA#8g4GuzjD;&AzqpY8BN~EW7|olx>I*fjptzV5kU(^hYs9*u;L_? zu`_iR4fJ`BU=CIJUwYbJ)CoG8!v{@FU zhkpMb*|>9a=XT0}i(h@7RYYfL%UwD+G7B~`=*ysy=ya8?ROuWum#^KdMkGDv@k$vG zttA}OHT#3AV(X#uJ}vC*Z$Rw=pube_;!2GarKq;4*y4}?>~(Y#ca>xbd$ba#`VS5; zeo!7>=W->#@xn=}AhV>bs7Z(Mq$vs)Yz&{30nQRfen26!_wTenr9Njmp&kvEtT%DQWlH@l|>&)^+Tas{t9HzkRphEYl zPcVZ_wHfbhE_f$d!A&a8y>Szb>XHc+8XdH9;}{mCu7ojJVxilXK_Om)`X?}wqlmW8 z?8Ick%pfw74Sd9mNhs=&SNhc%ek&`0;sDfSb7)Bxz1Ep87&cRAJ;KxdLe(<>pnk7% z|9+P{tAB?tMRcDE-|oP`=yp-$x+@fv>exl*JgYlWm$B$@c+D=F_MyoUXiAvET53E4 zN507$OPKW_HQ`KT=QMQu=gREpWMjGRX2##U7c6*Yr zBzSme(@w)XPAfs=`HnkAVPJUc^xzFFl*QYMVRP43o!3JNby$;Cd^xwvbd4fp`sR_SHV|$bT0GrfeV^vZrMY6QRka%B@q|3GUttrA!N5B~6@!D2c zeA(JBpWuw{Ulu)hlJdzP+iZyXRq5Y37s#@E6)oYp_|!ON;nGBZ-MM&|3qXJC?Vn$S zS0T6^Z#XK$|8pQH)0^b(b*~=guFEe}DwX(AxKMz3j*Be@@ z?lUu6dt@}?ggh6!-a1PBDwR&{n40CDX$?+s1NxJ8%l%kq(BqrtK((@y8HWCsRm?2} ziZT%}2$Z*s$P?tl|2OPJ@4`MT-tw@4{t(_hfk2|n?pm)6s13V8KUP>q2y6QYVOG;} zO+302)Yt1sXsS11lGaE*s8?ju1WmtgIJE>8ozi4bJ1|yY750{nZG+GjQ5vMTdF{y? zuaz2_WI%WDq<|5K*raVrQ>QO{fjY>tnl(iZ8PF-QxTnu(x+9_C{Wn|lcM3RUttly( zMuCia4Jlz!LyoacWC&$JJgPDEhyg&0&?;V}16L%xW#eq?D4NB9wD1a7bwnD@*b7`( zfh35S=+?8PvH&zef@s@edV=Zj8h;NdZu zc8MVjj))r9asf*LpcwayNH^*kp%zE?mP<1AP)DZHc(_XJ^(*t@?xX08EF8Hh&|))y zddL=Etkg88H5!UStAKSV;^~y>QA3d#lqJLnD|(yT>QoRGt&}J(&ept6tlLtBg*^B7 z&@1LZeeDb*72A5$0qKJ0)Lo0)z^X)&NR~W2Ohm)A>_sFN(uD(PT$M<_^%N!u1}tDY zFelj){u{oPU#9@p7*z7r4#{eY$qGh-a!DJowd>7fo9RZ^o~+XV(Va8(;*kF~10*mZ9*R++M-A4cnpjgN8xS6a zB~v$A9p=J7F?tm9vKb%Pk-W?YGGl+tgrFXfaUJIySebg+Zm;c7ufvR6Xu9lE_1<+S z1(p@3_Q~7fX?Q1c5rQ5BAUJ=c+Cjd4bPrL*4UnITb){w$ph(n(&=Q1hyNVsEKFNO_ z0Y>FZv0%wR3W!U;$iMyZ+z$tc0y%7Bej_*5M)qHY59A@Lf!y4L+-nbd4OTCfsjvx{ zLK64hw@_ZDhvW+5$9*j2r#w8#{i2zQjzqHLol z7P^7>L6+8i6Dw~Q!1^)lHvve6@}+_S-~@_t+tx~QIC9X#Qy63fp1N&YqV=doG5PF2W4!#?Lw|_5R zD3P*#$(6EplIrb%aIDN?PkYPy?#o|I`glC8s0EZeJ(Y&AX=3^Og%bF+JO|4!|Nh4C z?m(5jF<}w=le+dd?c5e|y*AKiWNfU~#I+Xzlq1-VGYfIL4*U<0fhfzOiC1;zC-A60 z28-y20Br-#Lf#9yl;NGCAaYvnuXHL5p}pwH?H7s7xNOB=@jnVfBRN!>D%p2BnfR;G zVuEvV#5%q?QI9auwiPTdGp#)YFrpa5oFZv1JyaQ+a6p3l2{jNL75)o=O;_Sh1M9Wu zTw9g^cv5)lq~K-F@phs5my!F7YAdV}$_)gOLTx8~m`4YQA`&3PV2I405z+Jrq2({2 z_m>L3&qdnl;4Y|3{F~A+!0qUI^++IrqgbxuKn=eX?ZCHL0oE!D%y;5d&v@Mq85j_N zCGp#bUO$pp(u`%;tt(?O>%~cXt>ZEMoDijicV>7&QL`7+n*K%Amf2zTRw#fA7i}1? z`jMFPb_hZ>qOHXcM-Sc#6GI(k&|+uy%UVnFO@~8kP$YUZ;KJR}A zgIVv^w|;zKaa+Ji$sbYA_mH$@i+S-@+W-0trhsc8tN}?BDMIXvqfYbr|9%fqBmjtWW=jkond-HK zOO7iPd6-!UlnJVqb@RlfCxv$c2)K`&vNe0UO^wh`neFpkl^n@93De@Y}HbC%uC~ z!SXr5#KQ_PrGPbr`rO#h%GrJ5avLcw-2Ei52}5DMq4O4AJ*&DGo||t0#j(07q*Va1 z!O}t0Ut}cWqT;RQ6_zAjOGS;Mg~I^{1CE>K#UW%UDAgcpM9!AnXjrTKV#EOBXt3#j ztWlX?t4*0|vwi5c&ma-XYd?^f)Gu-Z2>oUHUq$GnxJGEEw5MS49L96m-vEzUC^6X~ zh$Ji&39zu8FD_jsZwAoeEvtfuc*c^9TNopUBxqPJol=U-ja?bWs9r{(C|9*B2EeWs zA_{@4i?DC&fQlk0MdMe>qP=W;>Z!mMth*(~p}Du72KVm?ty~TvKC>@HD+7gKYU@<< z;%C*!G@ao0K`;g@SxeKIrz2V6FMxRqGa-A%tH@H$E?eGCNykjqTq+BHJZ)n}IU8Vg zb!7-M!BFoy#O@@{$d_*1y@6F&n^9!3eTQB#PnnkK_HRtYS0p{<$3gHlu?h{~|6TWc zWwo`n8b_S4HzG|<$HxFwmUGWqhG`)G&&zs-x0|9Re<*QxtuleTTsWMZ6Ys~S>B3@p(rsuPG#s@!KNyf3 zBW{E=RM+fPB7K(3Ud-s=BG+(rB0!O<(2Qbr?x zC?+Lp9b-Mv$Od`)p#NB}5p@E>lI_`^?0 zn^z{@bt)&=Up{}6udDfb=cCaev|7RDsBXxX*gutp+2NsjE=wdjm3g2~-QCmt+c$UE z5FTHzRWMDemqKIIt5?}I6U=zg%_!Ic%YLU*Df6n}yE;2mJD;gk37)i_wq`j$jsjBC za^#Gp6++l8WO+rr{R3vyWH?4JCky^IvNw7sicz88= z#=iWk@TVVmgvq&?9-h3uVNYMBc2RHs-#Ib6d`-Z?kr~&kcUMQ28^!on@DXvB@h{#Z zv?%n7G2SG1MUS!=VhByVOZVGRqJD<)-WdaZP2xNDL0&aaKLbswc=7u{fOjp(uwO%9<@CNL;9)%CdGW6!nI&pO%+p=>ALD&3okx=CZvS0ybdbO z;&jBAWVyE6KgVH|O4Ty-fV}=>Zj))W^zD5Uy(TU{29qYJ(oYIFxW(`nq2-Vo9ejHJ z^7d{g)BHE&y(BRWqd$J=@|AojV-omoid(2YEJ!u91;pDriyP+P1z&P}c#gqY%`>0Z zEHqn8KC0s^%`!FGiIHSfcI{bT$LSF^a@6Is1E)~3+n*h5cQ57!CAP7Ec$E&YL?t}pFc6~`FEWESN06u}4FG%!f&k*Reuw%u8} z59qg z4b}0SC5nGi@0|;m_iG(dJPK=j9FbPEi653n$SqB@b?D;OqG3huR|IPmhwINO0*|8E zbfa+}uM5ci4t<$zqf<3A7~%#iqi=P)zc@(?ZRI~g-lx&pSSf2}l6vxBZDOWLE;w6r zCG)=Ih)>`%Hrih7+#!5?h)U8!xclgANa}xZY$_a4)4A#;=~WP!yp@LEo+Ne#m8j7X z^G_Un$aVRUS71Dj^X|TrkRV^2OMx!o!z2U| zFBE-nsR8_6&A1~`+5-aLR(PfyKI9ASFOgMIrd7jPjZ*m}e6Trx6&`aPV_7rk(73Kt z=$C|R`yHhrVckZ|jaeM2uY*2S9*cw3${;smhV>9pgG9z%i`2U6X%?)`z1yx%5g(b_ zCeRe{$1&ihQxr)3dYUR@98xJv7=J4rCNRdtQhNMO!=DZIIkOc>RD#--RM02w;iQnZ zD7vHRBA&ay6LmaFn`*y$vEqxBhdN;gkrsM-ymJ>szs7hD?;b}IxvW+>d_ISQ1^V0^ zZ5#cf4!ujQ9KYrqcih8#)&B_Q2{+ocllImbo`etFSL}_jQpJ2Ay2OCl z3Tu|SJxldW)!>=VChy?k_A_wAud$a;3cf#1E2EuFEi7uhcTmkf0$2 zCixFwAj>DUV=}jVdU(&(9?|xR!I#CsWY?3T6Yc?3{z!tYd*L->SFBdYu;WMi>8sgJ z`%y=~*YZ<~6y($|;^j{Zi4)G$49lmK?Z3)b=uhvbpLBES@zqm+(jC(AI?u6SIk8b#m3jM*B=C`Uw zC&;cWR#uy?`=#to^%kwE?<`qQRFIzIGgj5_zQoV(cIg`Ps?+(Wws!`2%F1hIYTU98 ztg`w(ie{jFuXH(9YseA9AOl=@*II~u4cbjOebn9#&>tyB)!%;VtVq_@7n&6ns8dLW zLaVVtsPX>7eW-5nim%s%K8)G=29dNPK!HnDq0N1qw-_PswGT@So#&f*eEy$%5*_eT zFJmbRM59MkA_!%104Y^fgr~He>TbBbkkpU`M zN@y~R&SgdHkoS(5QC8GSMWnhpc}CLj1qAKO5`~VJoIn|2>Q4e_wib8- z3dFho^iX72#HV$d_>`8hjFyF_Oa*@s^LT5ZYqVy%y7yb7VS$f3d|@<2!7E%Ld%*?QUU4CZ*My2(|REMWMDP z@pwYf{Rf#PW>2{G0O^<~xIvC+9Zeg1iohaK8JzlRgxxld3g9zasKE4LWDqs2AV|Kn zoQ38Hg#YM7L}tI*nXEjk1RtH;A}VO|jF|UNaR6kmZ!ezy_txmvZEg8Y@{ewZ`J9ry{l7$`BNQ#>(;10&83gvj-=j;|b^A zZ%nM4ce{Vv>UuU%0)qmez-&U^nCg8o|I{X7f>(~$MI;YfARp*^A6A>ZLRIg}P^_dj zA4>zvjm%nUzx);0yjYDq>^H2CYlaY~fRek_|sBjLMv zuUYtpI5K0sYTW=oka&bT+a)iDHPKJD=psFBA2x#8dC#HGJO)^_BLZla9_Ey6Z09{$|`bIMu zIxLlMWCg7MrFUnHhMHZ@_(iYNZ#kme-3%C6JHxqnXjV-Z&>Qg~Ai#vBze8NV8aVbw zX#;a7z-~QImWl80O(cR+rmqi znnAWE9i15TP?!i<*PTWd4D?YSGi8(TMSS-H-(HFv`(+~*r zm)ZoT%u02`>Rqg>>T4aifRU3Oo`IjWIb5-~*N9^!~MJ|6`iAOT-C1aBZR(I@o(Wy~!J8#G1P6RRv zF~h#dczUXJA82~?U<;==GwTt&_qP}*Nnfei696xANY}Y%(2P>+8+j_OZTOWu7Hq0;L?f$>vS~u0yV@`EU|JsU=n+y{#Nl=01!DU z0O>#q7#;?q5XrwnE92{Y?~9Cl;)g zrD#s*6Tlsc9zW{)(ULGDJ4n1^9M$rA144|MzJBBg*|4QZFgGOi8GyPobBM%*B{cA( zbo5jsLs_H*lK_j|hT6n*xespgD|DvuG)`-RQ=fOVMkH@LWQ0b2; z>bXIn!tK}BqSqB00fU_^Yp*~*!_Xi|SrS{o|Gm(EAvoL)dq}D%bxlNx&WFKMZB(Fq z&^gxK-}3MKPb?nO+Ulxox@zWZ5_v3{<#|5@H<%bvq##!MrXqv+Kb7S@LIVubE=MSa zI~!E@YPP2S0YR)y|0HmN`4>~x*x7hVQB^L&oYy=3rbS>>LxF_WKGu+w7sm-eioQ&A4&4jV+$ zL#JwzgSFAkLfE#$7{Qi@vfSq)hN3;O#;aP=CMTxqqEDVI%%XE{?7c<&YcBk5N*E_IKU=~= zcOsW=uiLD5Rhw8j>Ltv1*Y&OYWn|3 zReWS^lQq<+Drv9JVPqBW9^o`Bn$yfzXf)oLt{*foo0x!4xf~t+oHJ0W z(vTvtK&|4WfBHg{xU^=|6LRgNTgn{R+`uHtp=-hE-OmLv{PC5CL+>`fP+GiiUEUY< z(r7PhfYf9H$r3hm#N9~(#`Sf%?Dyj|wlxiAEGF`m(N?-!>$CXNE%c+P2~-vHYE_!5 zJ{JWr(bvkaqwkH0O!kQU59VG_)>M|IYdj+kCBx7R{pQKBk zeI_Lujan4W^C5I-t7UY)T*b6BD=Kvhoo;x>WAbw=8CD+1%WHrZ?#*UkTBT}rHLEdP z#X5&C=q}g;m5ZJZLKeT-!_O+K>7zCn3qWa^v&c=Bf10OP*QZ zUGXo!ClUm3>KmKnGRrg>F>kl%^8(UR-@SkHVZ}b~&Dz?>5G;B~5=wAnh(Hl6h%xVO zM~mD%7Dm1@msiem9{ngRtqrWnthgT}DnobeIj$mnq`X*K`1L*WYvb<=>UWSaIco1t zyAe6oCRXRx5lY*;z$^^{9l|_uThTZc#FOB0bB%)!M~-oAmXbru{`JZh!>W+S)G(Tf zbs0`rB#OOb;r)@djOooJI+%_AF3~s@=~AISaxbeF!KEwM!61Y#g4q9M_aDHS_9)FG z?P~#-`1`JkngfMsE)K}DDGreQtylPs^iNXQM39=W8YHDVLbS
E93`J*6-8~B{9 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