From 85c12b7b731512d549ed808ac2b381d11fc90a77 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 4 Sep 2023 21:02:17 +0900 Subject: [PATCH 01/27] tmp Signed-off-by: Songki Choi --- src/otx/algorithms/classification/adapters/mmcls/configurer.py | 3 +++ src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py | 2 ++ src/otx/algorithms/common/configs/configuration_enums.py | 1 + 3 files changed, 6 insertions(+) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index 036e5f7e681..aca5163237e 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -192,6 +192,9 @@ def configure_input_size( if input_size is None: return + if input_size == (0, 0): + # Auto-adapt + InputSizeManager(cfg.data).set_input_size(input_size) logger.info("Input size is changed to {}".format(input_size)) 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 b6bced67130..5611e86b529 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -911,6 +911,8 @@ def get_configured_input_size( return None logger.info("Given model weight was trained with {} input size.".format(input_size)) + elif input_size_config == InputSizePreset.AUTO: + return (0, 0) else: input_size = input_size_config.value diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py index 4a0344a9786..5c23798c37f 100644 --- a/src/otx/algorithms/common/configs/configuration_enums.py +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -52,6 +52,7 @@ class InputSizePreset(ConfigurableEnum): """Configurable input size preset.""" DEFAULT = "Default" + AUTO = "Auto" _64x64 = "64x64" _128x128 = "128x128" _256x256 = "256x256" From 7dcb9eeb9cb81f2825176edeb801a057403c3d26 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Thu, 7 Sep 2023 21:17:15 +0900 Subject: [PATCH 02/27] Refactor robust stat functions for common use Signed-off-by: Songki Choi --- src/otx/algorithms/common/utils/data.py | 108 +++++++++++++++--- src/otx/algorithms/detection/utils/data.py | 57 ++------- .../unit/algorithms/common/utils/test_data.py | 103 +++++++++++++++++ 3 files changed, 209 insertions(+), 59 deletions(-) create mode 100644 tests/unit/algorithms/common/utils/test_data.py diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index 8716315e083..0456b821cde 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -1,18 +1,6 @@ """Collections of Dataset 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 # pylint: disable=invalid-name @@ -230,3 +218,95 @@ def __len__(self): """Get length of dataset.""" return len(self.dataset) + + +def compute_robust_statistics(values: np.array) -> Dict[str, float]: + """Computes robust statistics of given samples. + + Args: + values (np.array): Array of samples + + Returns: + Dict[str, float]: Robust avg, min, max values + """ + stat = {} + if values.size == 0: + return stat + + avg_value = np.mean(values) + std_value = np.std(values) + avg_3std_min_value = avg_value - 3 * std_value + avg_3std_max_value = avg_value + 3 * std_value + min_value = np.min(values) + max_value = np.max(values) + + # Refine min/max to reduce outlier effect + robust_min_value = max(min_value, avg_3std_min_value) + robust_max_value = min(max_value, avg_3std_max_value) + + stat["avg"] = avg_value + stat["std"] = std_value + stat["min"] = min_value + stat["max"] = max_value + stat["robust_min"] = robust_min_value + stat["robust_max"] = robust_max_value + return stat + + +def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: + """Computes robust statistics of scale values. + + Average of 0.5x scale and 2x scale should be 1x + + Args: + values (np.array): Array of positive scale values + + Returns: + Dict[str, float]: Robust avg, min, max values + """ + # Compute stat in log scale & convert back to original scale + stat = compute_robust_statistics(np.log(values)) + return {k: np.exp(v) for k, v in stat.items()} + + +def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> Dict[str, Dict[str, float]]: + """Computes robust statistics of image & annotation sizes. + + Args: + dataset (DatasetEntity): Input dataset. + ann_stat (bool): Whether to compute annotation size statistics. Defaults to False. + + Returns: + Dict[str, Dict[str, float]]: Robust avg, min, max values for images, and annotations optionally. + e.x) stat = { + "image": {"avg": ...}, + "annotation": { + "num_per_image": {"avg": ...}, + "size_of_shape": {"avg": ...}, + } + } + """ + stat = {} + if len(dataset) == 0: + return stat + + image_sizes = [] + for data in dataset: + image_sizes.append(np.sqrt(data.width * data.height)) + stat["image"] = compute_robust_scale_statistics(np.array(image_sizes)) + + if ann_stat: + stat["annotation"] = {} + num_per_images = [] + size_of_shapes = [] + for data in dataset: + annotations = data.get_annotations() + num_per_images.append(len(annotations)) + image_area = data.width * data.height + def shape_size(ann): + return np.sqrt(image_area * ann.shape.get_area()) + size_of_shapes.extend(map(shape_size, annotations)) + stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) + stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) + + return stat diff --git a/src/otx/algorithms/detection/utils/data.py b/src/otx/algorithms/detection/utils/data.py index 59f50032463..d635050495d 100644 --- a/src/otx/algorithms/detection/utils/data.py +++ b/src/otx/algorithms/detection/utils/data.py @@ -1,18 +1,6 @@ """Collection of utils for data in Detection Task.""" - -# Copyright (C) 2021-2022 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. +# Copyright (C) 2021-2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 import json import os.path as osp @@ -22,6 +10,7 @@ from mmdet.datasets.api_wrappers.coco_api import COCO from otx.algorithms.common.utils.logger import get_logger +from otx.algorithms.common.utils.data import compute_robust_dataset_statistics from otx.algorithms.detection.adapters.mmdet.datasets.dataset import ( get_annotation_mmdet_format, ) @@ -460,37 +449,15 @@ def adaptive_tile_params( """ assert rule in ["min", "avg"], f"Unknown rule: {rule}" - all_sizes = np.zeros((0), dtype=np.float32) - labels = dataset.get_labels(include_empty=False) - domain = labels[0].domain - max_object = 0 - for dataset_item in dataset: - result = get_annotation_mmdet_format(dataset_item, labels, domain) - if len(result["bboxes"]): - bboxes = result["bboxes"] - sizes = 0.5 * (bboxes[:, 2] - bboxes[:, 0] + bboxes[:, 3] - bboxes[:, 1]) - all_sizes = np.concatenate((all_sizes, sizes), 0) - if len(bboxes) > max_object: - max_object = len(bboxes) - - log_sizes = np.log(all_sizes) - avg_log_size = np.mean(log_sizes) - std_log_size = np.std(log_sizes) - avg_size = np.exp(avg_log_size) - avg_3std_min_size = np.exp(avg_log_size - 3 * std_log_size) - avg_3std_max_size = np.exp(avg_log_size + 3 * std_log_size) - min_size = np.exp(np.min(log_sizes)) - max_size = np.exp(np.max(log_sizes)) - logger.info(f"----> [stat] log scale avg: {avg_size}") - logger.info(f"----> [stat] log scale avg - 3*std: {avg_3std_min_size}") - logger.info(f"----> [stat] log scale avg + 3*std: {avg_3std_max_size}") + stat = compute_robust_dataset_statistics(dataset, ann_stat=True) + max_num_objects = stat["annotation"]["num_per_image"]["max"] + avg_size = stat["annotation"]["size_of_shape"]["avg"] + min_size = stat["annotation"]["size_of_shape"]["robust_min"] + max_size = stat["annotation"]["size_of_shape"]["robust_max"] + logger.info(f"----> [stat] scale avg: {avg_size}") logger.info(f"----> [stat] scale min: {min_size}") logger.info(f"----> [stat] scale max: {max_size}") - # Refine min/max to reduce outlier effect - min_size = max(min_size, avg_3std_min_size) - max_size = min(max_size, avg_3std_max_size) - if rule == "min": object_size = min_size elif rule == "avg": @@ -520,11 +487,11 @@ def adaptive_tile_params( tiling_parameters.get_metadata("tile_overlap")["min_value"], min(tiling_parameters.get_metadata("tile_overlap")["max_value"], tile_overlap), ) - max_object = max( + max_num_objects = max( tiling_parameters.get_metadata("tile_max_number")["min_value"], - min(tiling_parameters.get_metadata("tile_max_number")["max_value"], max_object), + min(tiling_parameters.get_metadata("tile_max_number")["max_value"], max_num_objects), ) tiling_parameters.tile_size = tile_size - tiling_parameters.tile_max_number = max_object + tiling_parameters.tile_max_number = max_num_objects tiling_parameters.tile_overlap = tile_overlap diff --git a/tests/unit/algorithms/common/utils/test_data.py b/tests/unit/algorithms/common/utils/test_data.py new file mode 100644 index 00000000000..4b3dd94248e --- /dev/null +++ b/tests/unit/algorithms/common/utils/test_data.py @@ -0,0 +1,103 @@ +"""Tests for data utils for common OTX algorithms.""" +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.utils.data import ( + compute_robust_statistics, + compute_robust_scale_statistics, + compute_robust_dataset_statistics, +) +from otx.api.entities.datasets import DatasetEntity +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.image import Image +from otx.api.entities.annotation import Annotation, AnnotationSceneEntity, AnnotationSceneKind +from otx.api.entities.shapes.rectangle import Rectangle +from otx.api.entities.scored_label import ScoredLabel, LabelEntity, Domain + +import numpy as np + + +@e2e_pytest_unit +def test_compute_robust_statistics(): + values = np.array([]) + stat = compute_robust_statistics(values) + assert len(stat) == 0 + + values = np.array([0.5, 1, 1.5]) + stat = compute_robust_statistics(values) + assert np.isclose(stat["avg"], 1.0) + assert np.isclose(stat["min"], 0.5) + assert np.isclose(stat["max"], 1.5) + + values = np.random.rand(10) + stat = compute_robust_statistics(values) + assert np.isclose(stat["min"], np.min(values)) + assert np.isclose(stat["max"], np.max(values)) + assert stat["min"] <= stat["robust_min"] + assert stat["max"] <= stat["robust_max"] + + +@e2e_pytest_unit +def test_compute_robust_scale_statistics(): + scales = np.array([]) + stat = compute_robust_scale_statistics(scales) + assert len(stat) == 0 + + scales = np.array([0.5, 1, 2]) + stat = compute_robust_scale_statistics(scales) + assert np.isclose(stat["avg"], 1.0) + assert np.isclose(stat["min"], 0.5) + assert np.isclose(stat["max"], 2.0) + + scales = np.random.rand(10) + stat = compute_robust_scale_statistics(scales) + assert np.isclose(stat["min"], np.min(scales)) + assert np.isclose(stat["max"], np.max(scales)) + assert stat["min"] <= stat["robust_min"] + assert stat["max"] <= stat["robust_max"] + + +@e2e_pytest_unit +def test_compute_robuste_dataset_statistics(): + dataset = DatasetEntity() + stat = compute_robust_dataset_statistics(dataset) + assert len(stat) == 0 + + label = ScoredLabel(label=LabelEntity(name="test", domain=Domain.DETECTION)) + dataset = DatasetEntity( + items=[ + DatasetItemEntity( + Image(data=np.random.rand(50, 50)), + AnnotationSceneEntity( + annotations=[ + Annotation(shape=Rectangle(x1=0.0, y1=0.0, x2=0.1, y2=0.1), labels=[label]), + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + DatasetItemEntity( + Image(data=np.random.rand(100, 100)), + AnnotationSceneEntity( + annotations=[ + Annotation(shape=Rectangle(x1=0.0, y1=0.0, x2=0.1, y2=0.1), labels=[label]), + Annotation(shape=Rectangle(x1=0.1, y1=0.1, x2=0.3, y2=0.3), labels=[label]), + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + DatasetItemEntity( + Image(data=np.random.rand(200, 200)), + AnnotationSceneEntity( + annotations=[ + ], + kind=AnnotationSceneKind.ANNOTATION, + ), + ), + ] + ) + + stat = compute_robust_dataset_statistics(dataset, ann_stat=False) + assert np.isclose(stat["image"]["avg"], 100) + assert "annotation" not in stat + + stat = compute_robust_dataset_statistics(dataset, ann_stat=True) + assert np.isclose(stat["annotation"]["num_per_image"]["avg"], 1.0) + assert np.isclose(stat["annotation"]["size_of_shape"]["avg"], 10.0) From ebd626836295e2929b69d5bc94c710a60899c8f6 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Thu, 7 Sep 2023 22:30:40 +0900 Subject: [PATCH 03/27] Implement parse helper for InputSizePreset Signed-off-by: Songki Choi --- .../adapters/mmcls/configurer.py | 4 +-- .../adapters/mmcv/utils/config_utils.py | 21 ++---------- .../common/configs/configuration_enums.py | 31 +++++++++-------- .../algorithms/common/configs/__init__.py | 2 ++ .../configs/test_configuration_enums.py | 34 +++++++++++++++++++ 5 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 tests/unit/algorithms/common/configs/__init__.py create mode 100644 tests/unit/algorithms/common/configs/test_configuration_enums.py diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index aca5163237e..1a0a91bd0e6 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -53,7 +53,7 @@ def configure( self.configure_ckpt(cfg, model_ckpt) self.configure_model(cfg, ir_options) self.configure_data(cfg, data_cfg) - self.configure_input_size(cfg, input_size, model_ckpt) + self.configure_input_size(cfg, data_cfg, input_size, model_ckpt) self.configure_task(cfg) self.configure_samples_per_gpu(cfg) self.configure_fp16(cfg) @@ -185,7 +185,7 @@ def configure_topk(cfg): @staticmethod def configure_input_size( - cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt: Optional[str] = None + cfg, data_cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt: Optional[str] = None ): """Change input size if necessary.""" input_size = get_configured_input_size(input_size_config, model_ckpt) 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 5611e86b529..6dae292561a 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -1,18 +1,6 @@ """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 copy import glob @@ -911,10 +899,7 @@ def get_configured_input_size( return None logger.info("Given model weight was trained with {} input size.".format(input_size)) - elif input_size_config == InputSizePreset.AUTO: - return (0, 0) else: input_size = input_size_config.value - parsed_tocken = re.match("(\\d+)x(\\d+)", input_size) - return (int(parsed_tocken.group(1)), int(parsed_tocken.group(2))) + return InputSizePreset.parse(input_size) diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py index 5c23798c37f..b39d717c753 100644 --- a/src/otx/algorithms/common/configs/configuration_enums.py +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -1,20 +1,10 @@ """Quantization preset Enums for post training optimization.""" - -# 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 from otx.api.configuration import ConfigurableEnum +from typing import Tuple, Optional +import re class POTQuantizationPreset(ConfigurableEnum): @@ -60,3 +50,16 @@ class InputSizePreset(ConfigurableEnum): _512x512 = "512x512" _768x768 = "768x768" _1024x1024 = "1024x1024" + + @staticmethod + def parse(preset: str) -> Optional[Tuple[int, int]]: + if preset == "Default": + return None + if preset == "Auto": + return (0, 0) + parsed_tocken = re.match("(\\d+)x(\\d+)", preset) + return (int(parsed_tocken.group(1)), int(parsed_tocken.group(2))) + + @classmethod + def input_sizes(cls): + return [InputSizePreset.parse(e.value) for e in cls if e.value[0].isdigit()] diff --git a/tests/unit/algorithms/common/configs/__init__.py b/tests/unit/algorithms/common/configs/__init__.py new file mode 100644 index 00000000000..6a16273c024 --- /dev/null +++ b/tests/unit/algorithms/common/configs/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/algorithms/common/configs/test_configuration_enums.py b/tests/unit/algorithms/common/configs/test_configuration_enums.py new file mode 100644 index 00000000000..fe8ba8f4f90 --- /dev/null +++ b/tests/unit/algorithms/common/configs/test_configuration_enums.py @@ -0,0 +1,34 @@ +"""Tests for common configuration enums in OTX algorithms.""" +from tests.test_suite.e2e_test_system import e2e_pytest_unit +from otx.algorithms.common.configs.configuration_enums import ( + POTQuantizationPreset, + StorageCacheScheme, + BatchSizeAdaptType, + InputSizePreset, +) + + +@e2e_pytest_unit +def test_pot_quansization_preset(): + assert len(POTQuantizationPreset) == 2 + + +@e2e_pytest_unit +def test_storage_cache_scheme(): + assert len(StorageCacheScheme) == 6 + + +@e2e_pytest_unit +def test_batsh_size_adapt_type(): + assert len(BatchSizeAdaptType) == 3 + + +@e2e_pytest_unit +def test_input_size_preset(): + assert len(InputSizePreset) == 9 + assert InputSizePreset.parse(InputSizePreset.DEFAULT.value) == None + assert InputSizePreset.parse(InputSizePreset.AUTO.value) == (0, 0) + assert InputSizePreset.parse(InputSizePreset._64x64.value) == (64, 64) + input_sizes = InputSizePreset.input_sizes() + assert len(input_sizes) == 7 + assert input_sizes[-1] == (1024, 1024) From bff9415748f1090b7b38d5cc6431cb1cf4a89ce0 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 8 Sep 2023 14:35:52 +0900 Subject: [PATCH 04/27] Implement input size preset selection Signed-off-by: Songki Choi --- .../adapters/mmcv/utils/config_utils.py | 19 ++++++++++++++++ .../adapters/mmcv/utils/test_config_utils.py | 22 ++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) 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 6dae292561a..23071ea1456 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -5,6 +5,7 @@ import copy import glob import multiprocessing +import numpy as np import os import os.path as osp import platform @@ -713,6 +714,24 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): for pipeline in pipelines: self._set_pipeline_size_value(pipeline, resize_ratio) + @staticmethod + def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[int, int]]): + """Select the most closest size from preset sizes in terms of area. + + Args: + input_size (Tuple[int, int]): Query input size + preset_sizes (List[Tuple[int, int]]): List of preset input sizes + + Returns: + Tuple[int, int]: Best matching size out of preset. Returns input_size if preset is empty. + """ + if len(preset_sizes) == 0: + return input_size + input_area = input_size[0]*input_size[1] + preset_areas = np.array(list(map(lambda x: x[0]*x[1], preset_sizes))) + abs_diff = np.abs(preset_areas - input_area) + return preset_sizes[np.argmin(abs_diff)] + @property def base_input_size(self) -> Union[Tuple[int, int], Dict[str, Tuple[int, int]]]: """Getter function of `base_input_size` attirbute. 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 a70a80fadbb..62de8a327a1 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 @@ -380,6 +380,16 @@ def test_get_input_size_from_cfg(self, test_case): # check input size is estimated as expected assert input_size_manager.get_input_size_from_cfg("train") == input_size + def test_select_closest_size(self): + manager = InputSizeManager({}) + input_size = (100, 100) + preset_sizes = [] + assert manager.select_closest_size(input_size, preset_sizes) == input_size + preset_sizes = [(99, 99), (101, 101)] + assert manager.select_closest_size(input_size, preset_sizes) == (99, 99) + preset_sizes = InputSizePreset.input_sizes() + assert manager.select_closest_size(input_size, preset_sizes) == (64, 64) + def get_mock_model_ckpt(case): if case == "none": @@ -393,7 +403,7 @@ def get_mock_model_ckpt(case): @e2e_pytest_unit -@pytest.mark.parametrize("input_size_config", [InputSizePreset.DEFAULT, InputSizePreset._1024x1024]) +@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(mocker, input_size_config, model_ckpt_case): # prepare @@ -403,17 +413,19 @@ def test_get_configured_input_size(mocker, input_size_config, model_ckpt_case): 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": - expeted_value = None + 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) - expeted_value = (int(pattern.group(1)), int(pattern.group(2))) + expected_value = (int(pattern.group(1)), int(pattern.group(2))) + elif input_size_config == InputSizePreset.AUTO: + expected_value = (0, 0) else: pattern = input_size_parser.search(input_size_config.value) - expeted_value = (int(pattern.group(1)), int(pattern.group(2))) + expected_value = (int(pattern.group(1)), int(pattern.group(2))) # check expected value is returned assert ( get_configured_input_size(input_size_config, None if model_ckpt_case == "none" else mocker.MagicMock()) - == expeted_value + == expected_value ) From 53b1a32910a2eca51c690abf7e819fa28eb93b7e Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 8 Sep 2023 14:37:36 +0900 Subject: [PATCH 05/27] Implement common adaptive input size logic Signed-off-by: Songki Choi --- .../common/adapters/mmcv/configurer.py | 49 ++++++++++++++++++- .../adapters/mmcv/{ => hooks}/test_hooks.py | 0 .../common/adapters/mmcv/test_configurer.py | 49 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) rename tests/unit/algorithms/common/adapters/mmcv/{ => hooks}/test_hooks.py (100%) create mode 100644 tests/unit/algorithms/common/adapters/mmcv/test_configurer.py diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 96ae5de1374..08d1cd97fc1 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -4,14 +4,16 @@ # import os -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional, Tuple import numpy as np import torch +import json from mmcv.runner import CheckpointLoader from mmcv.utils import Config, ConfigDict from torch import distributed as dist +from otx.algorithms.common.utils.data import compute_robust_dataset_statistics from otx.algorithms.common.adapters.mmcv.utils import ( patch_adaptive_interval_training, patch_early_stopping, @@ -19,6 +21,8 @@ ) from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( recursively_update_cfg, + InputSizePreset, + InputSizeManager, ) from otx.algorithms.common.utils import append_dist_rank_suffix from otx.algorithms.common.utils.logger import get_logger @@ -365,3 +369,46 @@ def get_data_cfg(cfg, subset): dataset = dataset.dataset return dataset return cfg.data[subset] + + @staticmethod + def get_input_size_to_fit_dataset(data_cfg, use_annotations: bool = False) -> Optional[Tuple[int, int]]: + """ Compute appropriate model input size w.r.t. dataset statistics. + + Args: + data_cfg (Dict): dataset configuration. + use_annotations (bool): whether to consider annotation shapes to compute input size. Defaults to False. + + Returns: + Tuple[int, int]: (width, height) or None + """ + MIN_RECOGNIZABLE_OBJECT_SIZE = 32 # Minimum object size recognizable by NNs: typically 16 ~ 32 + # meaning NxN input pixels being downscaled to 1x1 on feature map + + data_cfg = BaseConfigurer.get_data_cfg(data_cfg, "train") + dataset = data_cfg.get("otx_dataset", None) + if dataset is None: + return None + + stat = compute_robust_dataset_statistics(dataset, use_annotations) + if not stat: + return None + logger.info(f"Adapting model input size based on dataset stat: {json.dumps(stat, indent=4)}") + + # Fit to typical large image size (conservative) + # -> "avg" size might be preferrable for efficiency + input_size = stat["image"]["robust_max"] + logger.info(f"-> Based on typical large image size: {input_size}") + + # Refine using annotation shape size stat + if use_annotations and stat["annotation"]: + small_object_size = stat["annotation"].get("size_of_shape", {}).get("robust_min", None) + if small_object_size is not None and small_object_size > 0: + input_size = input_size * MIN_RECOGNIZABLE_OBJECT_SIZE / small_object_size + logger.info(f"-> Based on typical small object size {small_object_size}: {input_size}") + + # Closest preset + input_size = (round(input_size), round(input_size)) + input_size_preset = InputSizePreset.input_sizes() + input_size = InputSizeManager.select_closest_size(input_size, input_size_preset) + logger.info(f"-> Closest preset: {input_size}") + return input_size diff --git a/tests/unit/algorithms/common/adapters/mmcv/test_hooks.py b/tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py similarity index 100% rename from tests/unit/algorithms/common/adapters/mmcv/test_hooks.py rename to tests/unit/algorithms/common/adapters/mmcv/hooks/test_hooks.py diff --git a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py new file mode 100644 index 00000000000..ef94b527d3d --- /dev/null +++ b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py @@ -0,0 +1,49 @@ +import pytest +from mmcv.utils import Config +from otx.algorithms.common.adapters.mmcv import configurer +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@e2e_pytest_unit +class TestBaseConfigurer: + def test_get_input_size_to_fit_dataset(self, mocker): + data_cfg = Config({"data": {"train": {"otx_dataset": None}}}) + input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + assert input_size is None + + data_cfg = Config({"data": {"train": {"otx_dataset": True}}}) + mock_stat = mocker.patch.object(configurer, "compute_robust_dataset_statistics") + + mock_stat.return_value = {} + input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + assert input_size is None + + mock_stat.return_value = dict( + image=dict( + robust_max=150, + ), + ) + input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + assert input_size == (128, 128) + + mock_stat.return_value = dict( + image=dict( + robust_max=150, + ), + annotation=dict() + ) + input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg, use_annotations=True) + assert input_size == (128, 128) + + mock_stat.return_value = dict( + image=dict( + robust_max=150, + ), + annotation=dict( + size_of_shape=dict( + robust_min=64 + ) + ) + ) + input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg, use_annotations=True) + assert input_size == (64, 64) From 65b569f94cf348d8100e6c28c777e09c048355af Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 13:07:36 +0900 Subject: [PATCH 06/27] Minor fix Signed-off-by: Songki Choi --- .../common/configs/configuration_enums.py | 19 +++++++++++-------- src/otx/algorithms/common/utils/data.py | 16 +++++++++------- src/otx/algorithms/detection/utils/data.py | 5 +---- .../configs/test_configuration_enums.py | 6 +++--- .../unit/algorithms/common/utils/test_data.py | 3 +-- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py index b39d717c753..d9044b1bb4c 100644 --- a/src/otx/algorithms/common/configs/configuration_enums.py +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -2,9 +2,10 @@ # Copyright (C) 2021-2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from otx.api.configuration import ConfigurableEnum -from typing import Tuple, Optional import re +from typing import Optional, Tuple + +from otx.api.configuration import ConfigurableEnum class POTQuantizationPreset(ConfigurableEnum): @@ -51,15 +52,17 @@ class InputSizePreset(ConfigurableEnum): _768x768 = "768x768" _1024x1024 = "1024x1024" - @staticmethod - def parse(preset: str) -> Optional[Tuple[int, int]]: - if preset == "Default": + @property + def tuple(self) -> Optional[Tuple[int, int]]: + """Returns parsed tuple.""" + if self.value == "Default": return None - if preset == "Auto": + if self.value == "Auto": return (0, 0) - parsed_tocken = re.match("(\\d+)x(\\d+)", preset) + parsed_tocken = re.match("(\\d+)x(\\d+)", self.value) return (int(parsed_tocken.group(1)), int(parsed_tocken.group(2))) @classmethod def input_sizes(cls): - return [InputSizePreset.parse(e.value) for e in cls if e.value[0].isdigit()] + """Returns list of actual size tuples.""" + return [e.tuple for e in cls if e.value[0].isdigit()] diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index 0456b821cde..a1853cb6234 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -8,7 +8,7 @@ import logging import os import random -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union import cv2 import numpy as np @@ -229,7 +229,7 @@ def compute_robust_statistics(values: np.array) -> Dict[str, float]: Returns: Dict[str, float]: Robust avg, min, max values """ - stat = {} + stat: Dict = {} if values.size == 0: return stat @@ -269,7 +269,7 @@ def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: return {k: np.exp(v) for k, v in stat.items()} -def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> Dict[str, Dict[str, float]]: +def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> Dict[str, Any]: """Computes robust statistics of image & annotation sizes. Args: @@ -277,7 +277,7 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> ann_stat (bool): Whether to compute annotation size statistics. Defaults to False. Returns: - Dict[str, Dict[str, float]]: Robust avg, min, max values for images, and annotations optionally. + Dict[str, Any]: Robust avg, min, max values for images, and annotations optionally. e.x) stat = { "image": {"avg": ...}, "annotation": { @@ -286,7 +286,7 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> } } """ - stat = {} + stat: Dict = {} if len(dataset) == 0: return stat @@ -297,14 +297,16 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> if ann_stat: stat["annotation"] = {} - num_per_images = [] - size_of_shapes = [] + num_per_images: List[int] = [] + size_of_shapes: List[float] = [] for data in dataset: annotations = data.get_annotations() num_per_images.append(len(annotations)) image_area = data.width * data.height + def shape_size(ann): return np.sqrt(image_area * ann.shape.get_area()) + size_of_shapes.extend(map(shape_size, annotations)) stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) diff --git a/src/otx/algorithms/detection/utils/data.py b/src/otx/algorithms/detection/utils/data.py index d635050495d..03eb89e1abd 100644 --- a/src/otx/algorithms/detection/utils/data.py +++ b/src/otx/algorithms/detection/utils/data.py @@ -9,11 +9,8 @@ import numpy as np from mmdet.datasets.api_wrappers.coco_api import COCO -from otx.algorithms.common.utils.logger import get_logger from otx.algorithms.common.utils.data import compute_robust_dataset_statistics -from otx.algorithms.detection.adapters.mmdet.datasets.dataset import ( - get_annotation_mmdet_format, -) +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, diff --git a/tests/unit/algorithms/common/configs/test_configuration_enums.py b/tests/unit/algorithms/common/configs/test_configuration_enums.py index fe8ba8f4f90..37f9eee0831 100644 --- a/tests/unit/algorithms/common/configs/test_configuration_enums.py +++ b/tests/unit/algorithms/common/configs/test_configuration_enums.py @@ -26,9 +26,9 @@ def test_batsh_size_adapt_type(): @e2e_pytest_unit def test_input_size_preset(): assert len(InputSizePreset) == 9 - assert InputSizePreset.parse(InputSizePreset.DEFAULT.value) == None - assert InputSizePreset.parse(InputSizePreset.AUTO.value) == (0, 0) - assert InputSizePreset.parse(InputSizePreset._64x64.value) == (64, 64) + assert InputSizePreset.DEFAULT.tuple == None + assert InputSizePreset.AUTO.tuple == (0, 0) + assert InputSizePreset._64x64.tuple == (64, 64) input_sizes = InputSizePreset.input_sizes() assert len(input_sizes) == 7 assert input_sizes[-1] == (1024, 1024) diff --git a/tests/unit/algorithms/common/utils/test_data.py b/tests/unit/algorithms/common/utils/test_data.py index 4b3dd94248e..ff420d5d380 100644 --- a/tests/unit/algorithms/common/utils/test_data.py +++ b/tests/unit/algorithms/common/utils/test_data.py @@ -86,8 +86,7 @@ def test_compute_robuste_dataset_statistics(): DatasetItemEntity( Image(data=np.random.rand(200, 200)), AnnotationSceneEntity( - annotations=[ - ], + annotations=[], kind=AnnotationSceneKind.ANNOTATION, ), ), From 7c4f9146037d38f9710efa68e81aa1b51a95df65 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 13:49:33 +0900 Subject: [PATCH 07/27] Refine parse() & tuple attr for InputSizePreset Signed-off-by: Songki Choi --- .../common/configs/configuration_enums.py | 19 +++++++++++++------ .../configs/test_configuration_enums.py | 4 ++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py index d9044b1bb4c..f8b02cf7f2f 100644 --- a/src/otx/algorithms/common/configs/configuration_enums.py +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -52,16 +52,23 @@ class InputSizePreset(ConfigurableEnum): _768x768 = "768x768" _1024x1024 = "1024x1024" - @property - def tuple(self) -> Optional[Tuple[int, int]]: - """Returns parsed tuple.""" - if self.value == "Default": + @staticmethod + def parse(value: str) -> Optional[Tuple[int, int]]: + """Parse string value to tuple.""" + if value == "Default": return None - if self.value == "Auto": + if value == "Auto": return (0, 0) - parsed_tocken = re.match("(\\d+)x(\\d+)", self.value) + parsed_tocken = re.match("(\\d+)x(\\d+)", value) + if parsed_tocken is None: + return None return (int(parsed_tocken.group(1)), int(parsed_tocken.group(2))) + @property + def tuple(self) -> Optional[Tuple[int, int]]: + """Returns parsed tuple.""" + return InputSizePreset.parse(self.value) + @classmethod def input_sizes(cls): """Returns list of actual size tuples.""" diff --git a/tests/unit/algorithms/common/configs/test_configuration_enums.py b/tests/unit/algorithms/common/configs/test_configuration_enums.py index 37f9eee0831..d603a73d7db 100644 --- a/tests/unit/algorithms/common/configs/test_configuration_enums.py +++ b/tests/unit/algorithms/common/configs/test_configuration_enums.py @@ -26,6 +26,10 @@ def test_batsh_size_adapt_type(): @e2e_pytest_unit def test_input_size_preset(): assert len(InputSizePreset) == 9 + assert InputSizePreset.parse("xxx") == None + assert InputSizePreset.parse("Default") == None + assert InputSizePreset.parse("Auto") == (0, 0) + assert InputSizePreset.parse("1x1") == (1, 1) assert InputSizePreset.DEFAULT.tuple == None assert InputSizePreset.AUTO.tuple == (0, 0) assert InputSizePreset._64x64.tuple == (64, 64) From 69d31f3ca7e930f133009b6b39e2d5c00c740a1c Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 15:14:19 +0900 Subject: [PATCH 08/27] Implement input size adaptation logic in InputSizeManager Signed-off-by: Songki Choi --- .../classification/configs/configuration.yaml | 1 + .../adapters/mmcv/utils/config_utils.py | 167 ++++++++++++------ .../common/configs/configuration_enums.py | 1 + .../adapters/mmcv/utils/test_config_utils.py | 146 +++++++++------ .../configs/test_configuration_enums.py | 4 +- 5 files changed, 210 insertions(+), 109 deletions(-) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index e91a28405b0..31f4eac7a1f 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -289,6 +289,7 @@ learning_parameters: DEFAULT: "Default" _64x64: "64x64" _128x128: "128x128" + _224x224: "224x224" _256x256: "256x256" _384x384: "384x384" _512x512: "512x512" 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 617d0f6c98f..be06fc2b21d 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -5,11 +5,9 @@ import copy import glob import multiprocessing -import numpy as np import os import os.path as osp import platform -import re import shutil import sys import tempfile @@ -18,6 +16,7 @@ from importlib import import_module from typing import Any, Callable, Dict, List, Optional, Tuple, Union +import numpy as np import torch from mmcv import Config, ConfigDict from mmcv.utils.config import BASE_KEY, DEPRECATION_KEY @@ -644,7 +643,7 @@ class InputSizeManager: are considered at now. If other data pipelines exist, it can work differently than expected. Args: - data_config (Dict): Data configuration expected to have "train", "val" or "test" data pipeline. + config (Dict): Global configuration including data config w/ "train", "val" or "test" data pipeline. base_input_size (Optional[Union[int, List[int], Dict[str, Union[int, List[int]]]]], optional): Default input size. If it's a None, it's estimated based on data pipeline. If it's an integer, it's expected that all data pipeline have (base_input_size x base_input_size) input size. @@ -669,20 +668,24 @@ class InputSizeManager: } SUBSET_TYPES: Tuple[str, str, str, str] = ("train", "val", "test", "unlabeled") + MIN_RECOGNIZABLE_OBJECT_SIZE = 32 # Minimum object size recognizable by NNs: typically 16 ~ 32 + # meaning NxN input pixels being downscaled to 1x1 on feature map + def __init__( self, - data_config: Dict, + config: Dict, base_input_size: Optional[Union[int, Tuple[int, int], Dict[str, int], Dict[str, Tuple[int, int]]]] = None, ): - self._data_config = data_config + self._config = config + self._data_config = config.get("data", {}) if isinstance(base_input_size, int): base_input_size = (base_input_size, base_input_size) elif isinstance(base_input_size, dict): - for task in base_input_size.keys(): - if isinstance(base_input_size[task], int): - base_input_size[task] = (base_input_size[task], base_input_size[task]) # type: ignore[assignment] + for subset_type in base_input_size.keys(): + if isinstance(base_input_size[subset_type], int): + base_input_size[subset_type] = (base_input_size[subset_type], base_input_size[subset_type]) # type: ignore[assignment] for subset_type in self.SUBSET_TYPES: - if subset_type in data_config and subset_type not in base_input_size: + if subset_type in self._data_config and subset_type not in base_input_size: raise ValueError( f"There is {subset_type} data configuration but base input size for it doesn't exists." ) @@ -698,11 +701,11 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): If input_size is an integer list, (input_size[0] x input_size[1]) will be set. """ if isinstance(input_size, int): - input_size = [input_size, input_size] + input_size = (input_size, input_size) if not isinstance(self.base_input_size, dict): resize_ratio = (input_size[0] / self.base_input_size[0], input_size[1] / self.base_input_size[1]) - # scale size values + # Scale size values in data pipelines for subset_type in self.SUBSET_TYPES: if subset_type in self._data_config: if isinstance(self.base_input_size, dict): @@ -714,23 +717,13 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): for pipeline in pipelines: self._set_pipeline_size_value(pipeline, resize_ratio) - @staticmethod - def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[int, int]]): - """Select the most closest size from preset sizes in terms of area. - - Args: - input_size (Tuple[int, int]): Query input size - preset_sizes (List[Tuple[int, int]]): List of preset input sizes - - Returns: - Tuple[int, int]: Best matching size out of preset. Returns input_size if preset is empty. - """ - if len(preset_sizes) == 0: - return input_size - input_area = input_size[0]*input_size[1] - preset_areas = np.array(list(map(lambda x: x[0]*x[1], preset_sizes))) - abs_diff = np.abs(preset_areas - input_area) - return preset_sizes[np.argmin(abs_diff)] + # Set model size + # - needed only for YOLOX + model_cfg = self._config.get("model", {}) + if model_cfg.get("type", "") == "CustomYOLOX": + 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]]]: @@ -889,36 +882,102 @@ def _set_size_value(pipeline: Dict, attr: str, scale: Tuple[Union[int, float], U else: 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_configured_input_size( - input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt: Optional[str] = None -) -> Union[None, Tuple[int, int]]: - """Get configurable input size configuration. 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. - Args: - input_size_config (InputSizePreset, optional): Input size configuration. 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 - Returns: - Union[None, 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: + 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 - 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)) + else: + input_size = input_size_config.value - logger.info("Given model weight was trained with {} input size.".format(input_size)) - else: - input_size = input_size_config.value + return InputSizePreset.parse(input_size) + + @staticmethod + def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[int, int]]): + """Select the most closest size from preset sizes in log scale. + + Args: + input_size (Tuple[int, int]): Query input size + preset_sizes (List[Tuple[int, int]]): List of preset input sizes + + Returns: + Tuple[int, int]: Best matching size out of preset. Returns input_size if preset is empty. + """ + if len(preset_sizes) == 0: + return input_size + scale_of = lambda x: np.log(np.sqrt(x[0] * x[1])) + input_scale = scale_of(input_size) + preset_scales = np.array(list(map(scale_of, preset_sizes))) + abs_diff = np.abs(preset_scales - input_scale) + return preset_sizes[np.argmin(abs_diff)] + + def adapt_input_size_to_dataset( + self, max_image_size: float, min_object_size: float = None, downscale_only: bool = True + ) -> Tuple[int, int]: + """Compute appropriate model input size w.r.t. dataset statistics. - return InputSizePreset.parse(input_size) + Args: + max_image_size (int): Typical large image size of dataset in pixels. + min_object_size (int): Typical small object size of dataset in pixels. + None to consider only image size. Defaults to None. + downscale_only (bool) : Whether to allow only smaller size than default setting. Defaults to True. + + Returns: + Tuple[int, int]: (width, height) + """ + + logger.info("Adapting model input size based on dataset stat") + + base_input_size = self.base_input_size + if isinstance(base_input_size, Dict): + base_input_size = base_input_size.get("test", None) + + if max_image_size <= 0: + return base_input_size + + image_size = max_image_size + logger.info(f"-> Based on typical large image size: {image_size}") + + # Refine using annotation shape size stat + if min_object_size is not None and min_object_size > 0: + image_size = image_size * self.MIN_RECOGNIZABLE_OBJECT_SIZE / min_object_size + logger.info(f"-> Based on typical small object size {min_object_size}: {image_size}") + + input_size = (round(image_size), round(image_size)) + + if downscale_only: + def area(x): + return x[0] * x[1] + if base_input_size and area(input_size) >= area(base_input_size): + logger.info(f"-> Downscale only: {input_size} -> {base_input_size}") + return base_input_size + + # Closest preset + input_size_preset = InputSizePreset.input_sizes() + input_size = InputSizeManager.select_closest_size(input_size, input_size_preset) + logger.info(f"-> Closest preset: {input_size}") + return input_size diff --git a/src/otx/algorithms/common/configs/configuration_enums.py b/src/otx/algorithms/common/configs/configuration_enums.py index f8b02cf7f2f..dceea219f4f 100644 --- a/src/otx/algorithms/common/configs/configuration_enums.py +++ b/src/otx/algorithms/common/configs/configuration_enums.py @@ -46,6 +46,7 @@ class InputSizePreset(ConfigurableEnum): AUTO = "Auto" _64x64 = "64x64" _128x128 = "128x128" + _224x224 = "224x224" _256x256 = "256x256" _384x384 = "384x384" _512x512 = "512x512" 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 62de8a327a1..d1b4f347133 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,7 +7,6 @@ patch_persistent_workers, get_adaptive_num_workers, InputSizeManager, - get_configured_input_size, ) from otx.algorithms.common.configs.configuration_enums import InputSizePreset @@ -291,24 +290,35 @@ def mock_data_pipeline(): } +def get_mock_model_ckpt(case): + if case == "none": + return None + if case == "no_input_size": + return {"config": {}} + if case == "input_size_default": + return {"config": {"learning_parameters": {"input_size": {"value": "Default"}}}} + if case == "input_size_exist": + return {"config": {"learning_parameters": {"input_size": {"value": "512x512"}}}} + + @e2e_pytest_unit class TestInputSizeManager: @pytest.mark.parametrize("base_input_size", [None, 100, [100, 200], {"train": 100}]) def test_init(self, base_input_size): # prepare - mock_data_config = {"train": {"pipeline": []}} + mock_config = {"data": {"train": {"pipeline": []}}} # check - InputSizeManager(mock_data_config, base_input_size) + InputSizeManager(mock_config, base_input_size) def test_init_insufficient_base_input_size(self): # prepare - mock_data_config = {"train": {"pipeline": []}} + mock_config = {"data": {"train": {"pipeline": []}}} base_input_size = {"val": 100} # check if data pipeline has train but base_input_size doesn't have it, error is raised with pytest.raises(ValueError): - InputSizeManager(mock_data_config, base_input_size) + InputSizeManager(mock_config, base_input_size) @pytest.mark.parametrize("input_size", [200, (200, 100)]) def test_set_input_size(self, mock_data_pipeline, input_size): @@ -321,10 +331,10 @@ def test_set_input_size(self, mock_data_pipeline, input_size): expected_input_size_tuple = (input_size, input_size) expected_input_size_int = input_size - mock_data_config = {"train": {"pipeline": mock_data_pipeline}} + mock_config = {"data": {"train": {"pipeline": mock_data_pipeline}}} # execute - InputSizeManager(mock_data_config, base_input_size).set_input_size(input_size) + InputSizeManager(mock_config, base_input_size).set_input_size(input_size) # check all input sizes are updated as expected def check_val_changed(pipelines): @@ -344,7 +354,7 @@ def check_val_changed(pipelines): @pytest.mark.parametrize("base_input_size", [100, [100, 200], {"train": 100}]) def test_base_input_size_with_given_args(self, base_input_size): # prepare - mock_data_config = {"train": {"pipeline": []}} + mock_config = {"data": {"train": {"pipeline": []}}} if isinstance(base_input_size, int): base_input_size = [base_input_size, base_input_size] elif isinstance(base_input_size, dict): @@ -353,7 +363,7 @@ def test_base_input_size_with_given_args(self, base_input_size): base_input_size[task] = [base_input_size[task], base_input_size[task]] # execute - input_size_manager = InputSizeManager(mock_data_config, base_input_size) + input_size_manager = InputSizeManager(mock_config, base_input_size) # check base_input_size attribute is same as argument given when class initialization assert input_size_manager.base_input_size == base_input_size @@ -374,58 +384,88 @@ def test_get_input_size_from_cfg(self, test_case): # prepare pipeline = mock_data_pipeline_to_estimate[test_case]["pipeline"] input_size = mock_data_pipeline_to_estimate[test_case]["input_size"] - mock_data_config = {"train": {"pipeline": pipeline}} - input_size_manager = InputSizeManager(mock_data_config) + mock_config = {"data": {"train": {"pipeline": pipeline}}} + input_size_manager = InputSizeManager(mock_config) # check input size is estimated as expected assert input_size_manager.get_input_size_from_cfg("train") == input_size + @e2e_pytest_unit + @pytest.mark.parametrize( + "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): + # 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) + else: + pattern = input_size_parser.search(input_size_config.value) + expected_value = (int(pattern.group(1)), int(pattern.group(2))) + + # check expected value is returned + assert ( + InputSizeManager.get_configured_input_size( + input_size_config, None if model_ckpt_case == "none" else mocker.MagicMock() + ) + == expected_value + ) + def test_select_closest_size(self): manager = InputSizeManager({}) input_size = (100, 100) preset_sizes = [] assert manager.select_closest_size(input_size, preset_sizes) == input_size - preset_sizes = [(99, 99), (101, 101)] + preset_sizes = [(99, 99), (102, 102)] assert manager.select_closest_size(input_size, preset_sizes) == (99, 99) preset_sizes = InputSizePreset.input_sizes() - assert manager.select_closest_size(input_size, preset_sizes) == (64, 64) - - -def get_mock_model_ckpt(case): - if case == "none": - return None - if case == "no_input_size": - return {"config": {}} - if case == "input_size_default": - return {"config": {"learning_parameters": {"input_size": {"value": "Default"}}}} - if case == "input_size_exist": - return {"config": {"learning_parameters": {"input_size": {"value": "512x512"}}}} - - -@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(mocker, input_size_config, 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) - else: - pattern = input_size_parser.search(input_size_config.value) - expected_value = (int(pattern.group(1)), int(pattern.group(2))) - - # check expected value is returned - assert ( - get_configured_input_size(input_size_config, None if model_ckpt_case == "none" else mocker.MagicMock()) - == expected_value - ) + assert manager.select_closest_size(input_size, preset_sizes) == (128, 128) + + def test_adapt_input_size_to_dataset(self): + base_input_size = (128, 128) + manager = InputSizeManager({}, base_input_size) + input_size = manager.adapt_input_size_to_dataset( + max_image_size=-1, + ) + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=200, + ) # 200 -> 128 + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=200, + downscale_only=False, + ) # 200 -> 224 + assert input_size == (224, 224) + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=200, + min_object_size = 128, + ) # 50 -> 64 + assert input_size == (64, 64) + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=200, + min_object_size = 16, + ) # 400 -> 128 + assert input_size == base_input_size + + input_size = manager.adapt_input_size_to_dataset( + max_image_size=200, + min_object_size = 16, + downscale_only=False, + ) # 400 -> 384 + assert input_size == (384, 384) diff --git a/tests/unit/algorithms/common/configs/test_configuration_enums.py b/tests/unit/algorithms/common/configs/test_configuration_enums.py index d603a73d7db..f434b1cf71e 100644 --- a/tests/unit/algorithms/common/configs/test_configuration_enums.py +++ b/tests/unit/algorithms/common/configs/test_configuration_enums.py @@ -25,7 +25,7 @@ def test_batsh_size_adapt_type(): @e2e_pytest_unit def test_input_size_preset(): - assert len(InputSizePreset) == 9 + assert len(InputSizePreset) == 10 assert InputSizePreset.parse("xxx") == None assert InputSizePreset.parse("Default") == None assert InputSizePreset.parse("Auto") == (0, 0) @@ -34,5 +34,5 @@ def test_input_size_preset(): assert InputSizePreset.AUTO.tuple == (0, 0) assert InputSizePreset._64x64.tuple == (64, 64) input_sizes = InputSizePreset.input_sizes() - assert len(input_sizes) == 7 + assert len(input_sizes) == 8 assert input_sizes[-1] == (1024, 1024) From 68ea4976677aeb51e9bbd449ecf1db7e4ee0bdab Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 15:44:59 +0900 Subject: [PATCH 09/27] Adapt input size based on dataset stat in BaseConfigurer Signed-off-by: Songki Choi --- .../common/adapters/mmcv/configurer.py | 60 +++++++++---------- .../common/adapters/mmcv/test_configurer.py | 37 +++++------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 63f42cdc126..a3e42bd4e7d 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -3,30 +3,29 @@ # SPDX-License-Identifier: Apache-2.0 # +import json import os from typing import Any, Dict, List, Optional, Tuple import numpy as np import torch -import json from mmcv.runner import CheckpointLoader from mmcv.utils import Config, ConfigDict from torch import distributed as dist -from otx.algorithms.common.utils.data import compute_robust_dataset_statistics from otx.algorithms.common.adapters.mmcv.utils import ( patch_adaptive_interval_training, patch_early_stopping, patch_persistent_workers, ) from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( + InputSizeManager, + InputSizePreset, patch_color_conversion, recursively_update_cfg, - InputSizePreset, - InputSizeManager, ) -from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.utils import append_dist_rank_suffix +from otx.algorithms.common.utils.data import compute_robust_dataset_statistics from otx.algorithms.common.utils.logger import get_logger logger = get_logger() @@ -404,20 +403,22 @@ def get_data_cfg(cfg, subset): return cfg.data[subset] @staticmethod - def get_input_size_to_fit_dataset(data_cfg, use_annotations: bool = False) -> Optional[Tuple[int, int]]: - """ Compute appropriate model input size w.r.t. dataset statistics. - - Args: - data_cfg (Dict): dataset configuration. - use_annotations (bool): whether to consider annotation shapes to compute input size. Defaults to False. - - Returns: - Tuple[int, int]: (width, height) or None + def adapt_input_size_to_dataset( + cfg, input_size_manager: InputSizeManager, downscale_only: bool = True, use_annotations: bool = False + ) -> Optional[Tuple[int, int]]: + """Compute appropriate model input size w.r.t. dataset statistics. + + Args: + cfg (Dict): Global configuration. + input_size_manager: (InputSizeManager): Pre-configured input size manager + downscale_only (bool) : Whether to allow only smaller size than default setting. Defaults to True. + use_annotations (bool): Whether to consider annotation shapes to compute input size. Defaults to False. + + Returns: + Tuple[int, int]: (width, height) or None """ - MIN_RECOGNIZABLE_OBJECT_SIZE = 32 # Minimum object size recognizable by NNs: typically 16 ~ 32 - # meaning NxN input pixels being downscaled to 1x1 on feature map - data_cfg = BaseConfigurer.get_data_cfg(data_cfg, "train") + data_cfg = BaseConfigurer.get_data_cfg(cfg, "train") dataset = data_cfg.get("otx_dataset", None) if dataset is None: return None @@ -425,23 +426,16 @@ def get_input_size_to_fit_dataset(data_cfg, use_annotations: bool = False) -> Op stat = compute_robust_dataset_statistics(dataset, use_annotations) if not stat: return None - logger.info(f"Adapting model input size based on dataset stat: {json.dumps(stat, indent=4)}") + logger.info(f"Dataset stat: {json.dumps(stat, indent=4)}") # Fit to typical large image size (conservative) # -> "avg" size might be preferrable for efficiency - input_size = stat["image"]["robust_max"] - logger.info(f"-> Based on typical large image size: {input_size}") - - # Refine using annotation shape size stat + image_size = stat["image"]["robust_max"] + object_size = None if use_annotations and stat["annotation"]: - small_object_size = stat["annotation"].get("size_of_shape", {}).get("robust_min", None) - if small_object_size is not None and small_object_size > 0: - input_size = input_size * MIN_RECOGNIZABLE_OBJECT_SIZE / small_object_size - logger.info(f"-> Based on typical small object size {small_object_size}: {input_size}") - - # Closest preset - input_size = (round(input_size), round(input_size)) - input_size_preset = InputSizePreset.input_sizes() - input_size = InputSizeManager.select_closest_size(input_size, input_size_preset) - logger.info(f"-> Closest preset: {input_size}") - return input_size + # Refine using annotation shape size stat + # Fit to typical small object size (conservative) + # -> "avg" size might be preferrable for efficiency + object_size = stat["annotation"].get("size_of_shape", {}).get("robust_min", None) + + return input_size_manager.adapt_input_size_to_dataset(image_size, object_size, downscale_only) diff --git a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py index ef94b527d3d..d1ef435a176 100644 --- a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py +++ b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py @@ -1,49 +1,42 @@ import pytest from mmcv.utils import Config from otx.algorithms.common.adapters.mmcv import configurer +from otx.algorithms.common.adapters.mmcv.utils.config_utils import InputSizeManager from tests.test_suite.e2e_test_system import e2e_pytest_unit @e2e_pytest_unit class TestBaseConfigurer: def test_get_input_size_to_fit_dataset(self, mocker): - data_cfg = Config({"data": {"train": {"otx_dataset": None}}}) - input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + cfg = Config({"data": {"train": {"otx_dataset": None}}}) + input_size_manager = InputSizeManager(cfg) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) assert input_size is None - data_cfg = Config({"data": {"train": {"otx_dataset": True}}}) + cfg = Config({"data": {"train": {"otx_dataset": True}}}) + input_size_manager = InputSizeManager(cfg, base_input_size=128) mock_stat = mocker.patch.object(configurer, "compute_robust_dataset_statistics") mock_stat.return_value = {} - input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) assert input_size is None mock_stat.return_value = dict( - image=dict( - robust_max=150, - ), + image=dict(robust_max=150), ) - input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager) assert input_size == (128, 128) mock_stat.return_value = dict( - image=dict( - robust_max=150, - ), - annotation=dict() + image=dict(robust_max=150), + annotation=dict(), ) - input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg, use_annotations=True) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager, use_annotations=True) assert input_size == (128, 128) mock_stat.return_value = dict( - image=dict( - robust_max=150, - ), - annotation=dict( - size_of_shape=dict( - robust_min=64 - ) - ) + image=dict(robust_max=150), + annotation=dict(size_of_shape=dict(robust_min=64)), ) - input_size = configurer.BaseConfigurer.get_input_size_to_fit_dataset(data_cfg, use_annotations=True) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager, use_annotations=True) assert input_size == (64, 64) From 773a28627c1bd0ff1e913ace705ef37777f9575a Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 16:44:19 +0900 Subject: [PATCH 10/27] Implement auto input size for classification Signed-off-by: Songki Choi --- .../adapters/mmcls/configurer.py | 14 +++++----- .../adapters/mmcls/test_configurer.py | 26 +++++++++++++------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/configurer.py b/src/otx/algorithms/classification/adapters/mmcls/configurer.py index 3fb079a437a..3d5be63a4f9 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/configurer.py +++ b/src/otx/algorithms/classification/adapters/mmcls/configurer.py @@ -19,7 +19,6 @@ from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, - get_configured_input_size, recursively_update_cfg, update_or_add_custom_hook, ) @@ -166,14 +165,17 @@ def configure_input_size( cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None ): """Change input size if necessary.""" - input_size = get_configured_input_size(input_size_config, model_ckpt_path) - if input_size is None: + manager = InputSizeManager(cfg) + 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): - # Auto-adapt + if input_size == (0, 0): # InputSizePreset.AUTO + input_size = BaseConfigurer.adapt_input_size_to_dataset(cfg, manager) + if input_size is None: + return - InputSizeManager(cfg.data).set_input_size(input_size) + manager.set_input_size(input_size) logger.info("Input size is changed to {}".format(input_size)) diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index e553cd9960b..60cc2759e24 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -140,24 +140,34 @@ def test_configure_samples_per_gpu(self): assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} @e2e_pytest_unit - @pytest.mark.parametrize("input_size", [None, (128, 128)]) + @pytest.mark.parametrize("input_size", [None, (0, 0), (128, 128)]) def test_configure_input_size(self, mocker, input_size): # prepare mock_cfg = mocker.MagicMock() - mocker.patch.object(configurer, "get_configured_input_size", return_value=input_size) - mock_input_manager = 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_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) - # excute + # execute self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) # check - if input_size is not None: - mock_input_manager_cls.assert_called_once_with(mock_cfg.data) - mock_input_manager.set_input_size.assert_called_once_with(input_size) + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: - mock_input_manager_cls.assert_not_called() + 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): From a82dd21ddf20cbfe4fa2f5d54102b4e2bdad423e Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 16:55:31 +0900 Subject: [PATCH 11/27] Implement auto input size for segmentation Signed-off-by: Songki Choi --- .../segmentation/adapters/mmseg/configurer.py | 20 +++++++---- .../adapters/mmseg/test_mmseg_configurer.py | 34 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py index e7b0863c5cb..0c785567653 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py @@ -16,7 +16,6 @@ from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, - get_configured_input_size, remove_custom_hook, ) from otx.algorithms.common.configs.configuration_enums import InputSizePreset @@ -151,11 +150,7 @@ def configure_input_size( cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None ): """Change input size if necessary.""" - input_size = get_configured_input_size(input_size_config, model_ckpt_path) - if input_size is None: - return - - # segmentation models have different input size in train and val data pipeline + # Segmentation models have different input size in train and val data pipeline base_input_size = { "train": 512, "val": 544, @@ -163,7 +158,18 @@ def configure_input_size( "unlabeled": 512, } - InputSizeManager(cfg.data, base_input_size).set_input_size(input_size) + manager = InputSizeManager(cfg.data, 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 input_size is None: + return + + manager.set_input_size(input_size) logger.info("Input size is changed to {}".format(input_size)) 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 7cf93ab61a4..ec87ed04d32 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py @@ -140,30 +140,34 @@ def test_configure_samples_per_gpu(self): assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} @e2e_pytest_unit - @pytest.mark.parametrize("input_size", [None, (256, 256)]) + @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) def test_configure_input_size(self, mocker, input_size): # prepare mock_cfg = mocker.MagicMock() - mocker.patch.object(configurer, "get_configured_input_size", return_value=input_size) - mock_input_manager = 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_cls.return_value = mock_input_manager - base_input_size = { - "train": 512, - "val": 544, - "test": 544, - "unlabeled": 512, - } - - # excute + 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) # check - if input_size is not None: - mock_input_manager_cls.assert_called_once_with(mock_cfg.data, base_input_size) - mock_input_manager.set_input_size.assert_called_once_with(input_size) + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: - mock_input_manager_cls.assert_not_called() + 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): From c07598aa27202b8929ed4795cb0431e17a80d04a Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 20:55:19 +0900 Subject: [PATCH 12/27] Implement auto input size for detection Signed-off-by: Songki Choi --- .../detection/adapters/mmdet/configurer.py | 41 +++++++++-------- .../adapters/mmcv/utils/test_config_utils.py | 16 +++++++ .../adapters/mmdet/test_configurer.py | 44 ++++++++++--------- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index 0c699a5cd25..f9f3050fc39 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -12,7 +12,6 @@ from otx.algorithms.common.adapters.mmcv.semisl_mixin import SemiSLConfigurerMixin from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, - get_configured_input_size, ) from otx.algorithms.common.configs.configuration_enums import InputSizePreset from otx.algorithms.common.utils.logger import get_logger @@ -151,27 +150,31 @@ def configure_input_size( cfg, input_size_config: InputSizePreset = InputSizePreset.DEFAULT, model_ckpt_path: Optional[str] = None ): """Change input size if necessary.""" - input_size = get_configured_input_size(input_size_config, model_ckpt_path) - if input_size is None: - return - + # YOLOX tiny has a different input size in train and val data pipeline base_input_size = None model_cfg = cfg.get("model") if model_cfg is not None: - if cfg.model.type == "CustomYOLOX": - if input_size[0] % 32 != 0 or input_size[1] % 32 != 0: - raise ValueError("YOLOX should have input size being multiple of 32.") - if cfg.model.backbone.widen_factor == 0.375: # YOLOX tiny case - # YOLOX tiny has a different input size in train and val data pipeline - cfg.model.input_size = (input_size[0], input_size[1]) - base_input_size = { - "train": (640, 640), - "val": (416, 416), - "test": (416, 416), - "unlabeled": (992, 736), - } - - InputSizeManager(cfg.data, base_input_size).set_input_size(input_size) + if cfg.model.type == "CustomYOLOX" and cfg.model.backbone.widen_factor == 0.375: # YOLOX tiny case + base_input_size = { + "train": (640, 640), + "val": (416, 416), + "test": (416, 416), + "unlabeled": (992, 736), + } + + manager = InputSizeManager(cfg.data, base_input_size) + InputSizeManager.assert_called_once() + + 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 input_size is None: + return + + manager.set_input_size(input_size) logger.info("Input size is changed to {}".format(input_size)) 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 d1b4f347133..29728fabdac 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 @@ -351,6 +351,22 @@ def check_val_changed(pipelines): check_val_changed(mock_data_pipeline) + @pytest.mark.parametrize("input_size", [(256, 256), (300, 300)]) + def test_set_input_size_yolox(self, mock_data_pipeline, input_size): + mock_config = { + "model": {"type": "CustomYOLOX"}, + "data": {"train": {"pipeline": mock_data_pipeline}}, + } + + manager = InputSizeManager(mock_config) + + if input_size[0] % 32 != 0: + with pytest.raises(ValueError): + manager.set_input_size(input_size) + else: + manager.set_input_size(input_size) + assert mock_config["model"]["input_size"] == input_size + @pytest.mark.parametrize("base_input_size", [100, [100, 200], {"train": 100}]) def test_base_input_size_with_given_args(self, base_input_size): # prepare diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index afad3f53bcd..af46601ce0b 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -148,29 +148,38 @@ def test_configure_samples_per_gpu(self): assert model_cfg.data.train_dataloader == {"samples_per_gpu": 1, "drop_last": True} @e2e_pytest_unit - @pytest.mark.parametrize("input_size", [None, (256, 256)]) + @pytest.mark.parametrize("input_size", [None, (0, 0), (256, 256)]) def test_configure_input_size_not_yolox(self, mocker, input_size): # prepare mock_cfg = mocker.MagicMock() - mocker.patch.object(configurer, "get_configured_input_size", return_value=input_size) - mock_input_manager = 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_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) - # excute + # execute self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) # check - if input_size is not None: - mock_input_manager_cls.assert_called_once_with(mock_cfg.data, None) - mock_input_manager.set_input_size.assert_called_once_with(input_size) + if input_size is None: + mock_input_manager.set_input_size.assert_not_called() + elif input_size == (0, 0): + #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() + mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: - mock_input_manager_cls.assert_not_called() + 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("input_size", [(256, 256), (300, 300)]) @pytest.mark.parametrize("is_yolox_tiny", [True, False]) - def test_configure_input_size_yolox(self, mocker, input_size, is_yolox_tiny): + def test_configure_input_size_yolox(self, mocker, is_yolox_tiny): # prepare mock_cfg = mocker.MagicMock() mock_cfg.model.type = "CustomYOLOX" @@ -184,23 +193,16 @@ def test_configure_input_size_yolox(self, mocker, input_size, is_yolox_tiny): } else: base_input_size = None - mocker.patch.object(configurer, "get_configured_input_size", return_value=input_size) - mock_input_manager = mocker.MagicMock() - mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") - mock_input_manager_cls.return_value = mock_input_manager - # If model is one of yolox variants and input size isn't multiple of 32, error should be raised. - if input_size[0] % 32 != 0: - with pytest.raises(ValueError): - self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) - return + mock_input_manager_cls = mocker.patch.object(configurer, "InputSizeManager") + mock_input_manager = mock_input_manager_cls.return_value + mock_input_manager.get_configured_input_size.return_value = None # excute - self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT, self.data_cfg) + self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT) # check mock_input_manager_cls.assert_called_once_with(mock_cfg.data, base_input_size) - mock_input_manager.set_input_size.assert_called_once_with(input_size) @e2e_pytest_unit def test_configure_fp16(self): From c55adc9f6cb444b93df99595b1d76e17182d280d Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 12 Sep 2023 23:34:56 +0900 Subject: [PATCH 13/27] Enable 'Auto' input size via CLI Signed-off-by: Songki Choi --- .../classification/configs/configuration.yaml | 1 + .../adapters/mmcv/utils/config_utils.py | 2 ++ src/otx/algorithms/common/utils/data.py | 20 +++++++++---------- .../detection/adapters/mmdet/configurer.py | 3 +-- .../configs/detection/configuration.yaml | 1 + .../instance_segmentation/configuration.yaml | 1 + .../rotated_detection/configuration.yaml | 1 + .../segmentation/adapters/mmseg/configurer.py | 2 +- .../segmentation/configs/configuration.yaml | 1 + 9 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index 31f4eac7a1f..e0c62f7032a 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -287,6 +287,7 @@ learning_parameters: header: Configure model input size. options: DEFAULT: "Default" + AUTO: "Auto" _64x64: "64x64" _128x128: "128x128" _224x224: "224x224" 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 be06fc2b21d..5ab56f48309 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -911,6 +911,7 @@ def get_configured_input_size( return None logger.info("Given model weight was trained with {} input size.".format(input_size)) + else: input_size = input_size_config.value @@ -955,6 +956,7 @@ def adapt_input_size_to_dataset( base_input_size = self.base_input_size if isinstance(base_input_size, Dict): base_input_size = base_input_size.get("test", None) + logger.info(f"-> Current base input size: {base_input_size}") if max_image_size <= 0: return base_input_size diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index a1853cb6234..7c185eb03b6 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -244,12 +244,12 @@ def compute_robust_statistics(values: np.array) -> Dict[str, float]: robust_min_value = max(min_value, avg_3std_min_value) robust_max_value = min(max_value, avg_3std_max_value) - stat["avg"] = avg_value - stat["std"] = std_value - stat["min"] = min_value - stat["max"] = max_value - stat["robust_min"] = robust_min_value - stat["robust_max"] = robust_max_value + stat["avg"] = float(avg_value) + stat["std"] = float(std_value) + stat["min"] = float(min_value) + stat["max"] = float(max_value) + stat["robust_min"] = float(robust_min_value) + stat["robust_max"] = float(robust_max_value) return stat @@ -266,7 +266,7 @@ def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: """ # Compute stat in log scale & convert back to original scale stat = compute_robust_statistics(np.log(values)) - return {k: np.exp(v) for k, v in stat.items()} + return {k: float(np.exp(v)) for k, v in stat.items()} def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> Dict[str, Any]: @@ -293,7 +293,7 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> image_sizes = [] for data in dataset: image_sizes.append(np.sqrt(data.width * data.height)) - stat["image"] = compute_robust_scale_statistics(np.array(image_sizes)) + stat["image"] = compute_robust_statistics(np.array(image_sizes)) if ann_stat: stat["annotation"] = {} @@ -307,8 +307,8 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> def shape_size(ann): return np.sqrt(image_area * ann.shape.get_area()) - size_of_shapes.extend(map(shape_size, annotations)) + size_of_shapes.extend(filter(lambda x: x >= 1, map(shape_size, annotations))) stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) - stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) + stat["annotation"]["size_of_shape"] = compute_robust_statistics(np.array(size_of_shapes)) return stat diff --git a/src/otx/algorithms/detection/adapters/mmdet/configurer.py b/src/otx/algorithms/detection/adapters/mmdet/configurer.py index f9f3050fc39..f652ecb6893 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/configurer.py +++ b/src/otx/algorithms/detection/adapters/mmdet/configurer.py @@ -162,8 +162,7 @@ def configure_input_size( "unlabeled": (992, 736), } - manager = InputSizeManager(cfg.data, base_input_size) - InputSizeManager.assert_called_once() + 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 diff --git a/src/otx/algorithms/detection/configs/detection/configuration.yaml b/src/otx/algorithms/detection/configs/detection/configuration.yaml index 84589e60e16..1425af1effc 100644 --- a/src/otx/algorithms/detection/configs/detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -255,6 +255,7 @@ learning_parameters: header: Configure model input size. options: DEFAULT: "Default" + AUTO: "Auto" _256x256: "256x256" _384x384: "384x384" _512x512: "512x512" diff --git a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml index 9060bf10747..53883e4be57 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -255,6 +255,7 @@ learning_parameters: header: Configure model input size. options: DEFAULT: "Default" + AUTO: "Auto" _256x256: "256x256" _384x384: "384x384" _512x512: "512x512" diff --git a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml index 9812fe0d38c..a34862d8f16 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -233,6 +233,7 @@ learning_parameters: header: Configure model input size. options: DEFAULT: "Default" + AUTO: "Auto" _256x256: "256x256" _384x384: "384x384" _512x512: "512x512" diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py index 0c785567653..f0f89cd22b6 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/configurer.py @@ -158,7 +158,7 @@ def configure_input_size( "unlabeled": 512, } - manager = InputSizeManager(cfg.data, base_input_size) + 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 diff --git a/src/otx/algorithms/segmentation/configs/configuration.yaml b/src/otx/algorithms/segmentation/configs/configuration.yaml index 384ce60bb6a..2ec201d27b3 100644 --- a/src/otx/algorithms/segmentation/configs/configuration.yaml +++ b/src/otx/algorithms/segmentation/configs/configuration.yaml @@ -242,6 +242,7 @@ learning_parameters: header: Configure model input size. options: DEFAULT: "Default" + AUTO: "Auto" _256x256: "256x256" _384x384: "384x384" _512x512: "512x512" From 0c0a979d7162df344a91a1669532d56a2c84d43d Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 13 Sep 2023 09:59:58 +0900 Subject: [PATCH 14/27] Fix pre-commit Signed-off-by: Songki Choi --- .../common/adapters/mmcv/utils/config_utils.py | 5 ++++- .../adapters/mmcls/test_configurer.py | 1 - .../common/adapters/mmcv/test_configurer.py | 8 ++++++-- .../adapters/mmcv/utils/test_config_utils.py | 16 +++++++++++----- .../detection/adapters/mmdet/test_configurer.py | 1 - .../adapters/mmseg/test_mmseg_configurer.py | 1 - 6 files changed, 21 insertions(+), 11 deletions(-) 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 5ab56f48309..014e05568b6 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -930,7 +930,8 @@ def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[in """ if len(preset_sizes) == 0: return input_size - scale_of = lambda x: np.log(np.sqrt(x[0] * x[1])) + def scale_of(x): + return np.log(np.sqrt(x[0] * x[1])) input_scale = scale_of(input_size) preset_scales = np.array(list(map(scale_of, preset_sizes))) abs_diff = np.abs(preset_scales - input_scale) @@ -972,8 +973,10 @@ def adapt_input_size_to_dataset( input_size = (round(image_size), round(image_size)) if downscale_only: + def area(x): return x[0] * x[1] + if base_input_size and area(input_size) >= area(base_input_size): logger.info(f"-> Downscale only: {input_size} -> {base_input_size}") return base_input_size diff --git a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py index 60cc2759e24..6710d07a974 100644 --- a/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py +++ b/tests/unit/algorithms/classification/adapters/mmcls/test_configurer.py @@ -158,7 +158,6 @@ def test_configure_input_size(self, mocker, input_size): if input_size is None: mock_input_manager.set_input_size.assert_not_called() elif input_size == (0, 0): - #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: mock_input_manager.set_input_size.assert_called_once_with(input_size) diff --git a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py index d1ef435a176..65d60f2b436 100644 --- a/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py +++ b/tests/unit/algorithms/common/adapters/mmcv/test_configurer.py @@ -31,12 +31,16 @@ def test_get_input_size_to_fit_dataset(self, mocker): image=dict(robust_max=150), annotation=dict(), ) - input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager, use_annotations=True) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) assert input_size == (128, 128) mock_stat.return_value = dict( image=dict(robust_max=150), annotation=dict(size_of_shape=dict(robust_min=64)), ) - input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset(cfg, input_size_manager, use_annotations=True) + input_size = configurer.BaseConfigurer.adapt_input_size_to_dataset( + cfg, input_size_manager, use_annotations=True + ) assert input_size == (64, 64) 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 29728fabdac..fd3b6db1570 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 @@ -418,10 +418,16 @@ def test_get_configured_input_size(self, mocker, input_size_config, model_ckpt_c 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": + 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"] + 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: @@ -469,19 +475,19 @@ def test_adapt_input_size_to_dataset(self): input_size = manager.adapt_input_size_to_dataset( max_image_size=200, - min_object_size = 128, + min_object_size=128, ) # 50 -> 64 assert input_size == (64, 64) input_size = manager.adapt_input_size_to_dataset( max_image_size=200, - min_object_size = 16, + min_object_size=16, ) # 400 -> 128 assert input_size == base_input_size input_size = manager.adapt_input_size_to_dataset( max_image_size=200, - min_object_size = 16, + min_object_size=16, downscale_only=False, ) # 400 -> 384 assert input_size == (384, 384) diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index af46601ce0b..de8774113f6 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -166,7 +166,6 @@ def test_configure_input_size_not_yolox(self, mocker, input_size): if input_size is None: mock_input_manager.set_input_size.assert_not_called() elif input_size == (0, 0): - #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: mock_input_manager.set_input_size.assert_called_once_with(input_size) 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 ec87ed04d32..87fb6716955 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/test_mmseg_configurer.py @@ -158,7 +158,6 @@ def test_configure_input_size(self, mocker, input_size): if input_size is None: mock_input_manager.set_input_size.assert_not_called() elif input_size == (0, 0): - #mock_base_configurer_cls.adapt_input_size_to_dataset.assert_called_once_with() mock_input_manager.set_input_size.assert_called_once_with((64, 64)) else: mock_input_manager.set_input_size.assert_called_once_with(input_size) From 61c5e8dd4107b01e14a8bbf160f120f80864f1ff Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 13 Sep 2023 10:36:22 +0900 Subject: [PATCH 15/27] Subsample for dataset stat compute Signed-off-by: Songki Choi --- src/otx/algorithms/common/utils/data.py | 33 ++++++++++++------- .../unit/algorithms/common/utils/test_data.py | 5 +++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index 7c185eb03b6..a62786b18fb 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -266,15 +266,18 @@ def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: """ # Compute stat in log scale & convert back to original scale stat = compute_robust_statistics(np.log(values)) - return {k: float(np.exp(v)) for k, v in stat.items()} + stat = {k: float(np.exp(v)) for k, v in stat.items()} + stat["std"] = float(np.std(values)) # Normal scale std is better for understanding + return stat -def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> Dict[str, Any]: +def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False, max_samples=1000) -> Dict[str, Any]: """Computes robust statistics of image & annotation sizes. Args: dataset (DatasetEntity): Input dataset. - ann_stat (bool): Whether to compute annotation size statistics. Defaults to False. + ann_stat (bool, optional): Whether to compute annotation size statistics. Defaults to False. + max_samples (int, optional): Maximum number of dataset subsamples to analyze. Defaults to 1000. Returns: Dict[str, Any]: Robust avg, min, max values for images, and annotations optionally. @@ -287,28 +290,36 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False) -> } """ stat: Dict = {} - if len(dataset) == 0: + if len(dataset) == 0 or max_samples <= 0: return stat + max_image_samples = min(max_samples, len(dataset)) + image_indices = np.random.permutation(len(dataset))[:max_image_samples] + image_sizes = [] - for data in dataset: + for i in image_indices: + data = dataset[int(i)] image_sizes.append(np.sqrt(data.width * data.height)) - stat["image"] = compute_robust_statistics(np.array(image_sizes)) + stat["image"] = compute_robust_scale_statistics(np.array(image_sizes)) if ann_stat: stat["annotation"] = {} num_per_images: List[int] = [] size_of_shapes: List[float] = [] - for data in dataset: + for i in image_indices: + data = dataset[int(i)] annotations = data.get_annotations() num_per_images.append(len(annotations)) - image_area = data.width * data.height - def shape_size(ann): + if len(size_of_shapes) >= max_samples: + continue + + image_area = data.width * data.height + def scale_of(ann): return np.sqrt(image_area * ann.shape.get_area()) + size_of_shapes.extend(filter(lambda x: x >= 1, map(scale_of, annotations))) # Filter out shapes smaller than 1 pixel as outlier - size_of_shapes.extend(filter(lambda x: x >= 1, map(shape_size, annotations))) stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) - stat["annotation"]["size_of_shape"] = compute_robust_statistics(np.array(size_of_shapes)) + stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) return stat diff --git a/tests/unit/algorithms/common/utils/test_data.py b/tests/unit/algorithms/common/utils/test_data.py index ff420d5d380..1b7f2e2df1d 100644 --- a/tests/unit/algorithms/common/utils/test_data.py +++ b/tests/unit/algorithms/common/utils/test_data.py @@ -93,6 +93,11 @@ def test_compute_robuste_dataset_statistics(): ] ) + stat = compute_robust_dataset_statistics(dataset, max_samples=0) + assert len(stat) == 0 + stat = compute_robust_dataset_statistics(dataset, max_samples=-1) + assert len(stat) == 0 + stat = compute_robust_dataset_statistics(dataset, ann_stat=False) assert np.isclose(stat["image"]["avg"], 100) assert "annotation" not in stat From 1144187a4761fb132d3e56f6358f6d175f68bc05 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 13 Sep 2023 10:59:40 +0900 Subject: [PATCH 16/27] Fix pre-commit Signed-off-by: Songki Choi --- .../adapters/mmcv/utils/config_utils.py | 37 ++++++++++--------- src/otx/algorithms/common/utils/data.py | 6 ++- 2 files changed, 24 insertions(+), 19 deletions(-) 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 014e05568b6..7783fb1eced 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -681,14 +681,13 @@ def __init__( if isinstance(base_input_size, int): base_input_size = (base_input_size, base_input_size) elif isinstance(base_input_size, dict): - for subset_type in base_input_size.keys(): - if isinstance(base_input_size[subset_type], int): - base_input_size[subset_type] = (base_input_size[subset_type], base_input_size[subset_type]) # type: ignore[assignment] - for subset_type in self.SUBSET_TYPES: - if subset_type in self._data_config and subset_type not in base_input_size: - raise ValueError( - f"There is {subset_type} data configuration but base input size for it doesn't exists." - ) + for subset in base_input_size.keys(): + if isinstance(base_input_size[subset], int): + size = base_input_size[subset] + base_input_size[subset] = (size, size) # type: ignore[assignment] + for subset in self.SUBSET_TYPES: + if subset in self._data_config and subset not in base_input_size: + raise ValueError(f"There is {subset} data configuration but base input size for it doesn't exists.") self._base_input_size = base_input_size @@ -706,14 +705,14 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): resize_ratio = (input_size[0] / self.base_input_size[0], input_size[1] / self.base_input_size[1]) # Scale size values in data pipelines - for subset_type in self.SUBSET_TYPES: - if subset_type in self._data_config: + for subset in self.SUBSET_TYPES: + if subset in self._data_config: if isinstance(self.base_input_size, dict): resize_ratio = ( - input_size[0] / self.base_input_size[subset_type][0], - input_size[1] / self.base_input_size[subset_type][1], + input_size[0] / self.base_input_size[subset][0], + input_size[1] / self.base_input_size[subset][1], ) - pipelines = self._get_pipelines(subset_type) + pipelines = self._get_pipelines(subset) for pipeline in pipelines: self._set_pipeline_size_value(pipeline, resize_ratio) @@ -832,11 +831,11 @@ def _get_size_value(cls, pipeline: Dict, attr: str) -> Union[List[int], None]: return None - def _get_pipelines(self, subset_type: str): - if "pipeline" in self._data_config[subset_type]: - return self._data_config[subset_type]["pipeline"] - if "dataset" in self._data_config[subset_type]: - return self._data_config[subset_type]["dataset"]["pipeline"] + def _get_pipelines(self, subset: str): + if "pipeline" in self._data_config[subset]: + return self._data_config[subset]["pipeline"] + if "dataset" in self._data_config[subset]: + return self._data_config[subset]["dataset"]["pipeline"] raise RuntimeError("Failed to find pipeline.") def _set_pipeline_size_value(self, pipeline: Dict, scale: Tuple[Union[int, float], Union[int, float]]): @@ -930,8 +929,10 @@ def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[in """ if len(preset_sizes) == 0: return input_size + def scale_of(x): return np.log(np.sqrt(x[0] * x[1])) + input_scale = scale_of(input_size) preset_scales = np.array(list(map(scale_of, preset_sizes))) abs_diff = np.abs(preset_scales - input_scale) diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index a62786b18fb..d3908b566e3 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -315,9 +315,13 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False, ma continue image_area = data.width * data.height + def scale_of(ann): return np.sqrt(image_area * ann.shape.get_area()) - size_of_shapes.extend(filter(lambda x: x >= 1, map(scale_of, annotations))) # Filter out shapes smaller than 1 pixel as outlier + + size_of_shapes.extend( + filter(lambda x: x >= 1, map(scale_of, annotations)) + ) # Filter out shapes smaller than 1 pixel as outlier stat["annotation"]["num_per_image"] = compute_robust_statistics(np.array(num_per_images)) stat["annotation"]["size_of_shape"] = compute_robust_scale_statistics(np.array(size_of_shapes)) From 7e4bf30a024eae5321b598cc043e2ee5cc63b500 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 13 Sep 2023 12:39:35 +0900 Subject: [PATCH 17/27] Set 'Auto' as the default input size setting Signed-off-by: Songki Choi --- src/otx/algorithms/classification/configs/configuration.yaml | 4 ++-- .../algorithms/detection/configs/detection/configuration.yaml | 4 ++-- .../configs/instance_segmentation/configuration.yaml | 4 ++-- src/otx/algorithms/segmentation/configs/configuration.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index e0c62f7032a..472f9a1e7aa 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -277,11 +277,11 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Default + default_value: Auto description: The input size of the given model could be configured to one of the predefined resolutions. Reduced training and inference time could be expected by using smaller input size. - Defaults to per-model default resolution. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. 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 1425af1effc..1c4a22a628a 100644 --- a/src/otx/algorithms/detection/configs/detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -245,11 +245,11 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Default + default_value: Auto description: The input size of the given model could be configured to one of the predefined resolutions. Reduced training and inference time could be expected by using smaller input size. - Defaults to per-model default resolution. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. 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 53883e4be57..2fd3d2b4f7b 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -245,11 +245,11 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Default + default_value: Auto description: The input size of the given model could be configured to one of the predefined resolutions. Reduced training and inference time could be expected by using smaller input size. - Defaults to per-model default resolution. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. editable: true enum_name: InputSizePreset header: Configure model input size. diff --git a/src/otx/algorithms/segmentation/configs/configuration.yaml b/src/otx/algorithms/segmentation/configs/configuration.yaml index 2ec201d27b3..18f43f2a363 100644 --- a/src/otx/algorithms/segmentation/configs/configuration.yaml +++ b/src/otx/algorithms/segmentation/configs/configuration.yaml @@ -232,11 +232,11 @@ learning_parameters: warning: null input_size: affects_outcome_of: INFERENCE - default_value: Default + default_value: Auto description: The input size of the given model could be configured to one of the predefined resolutions. Reduced training and inference time could be expected by using smaller input size. - Defaults to per-model default resolution. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. editable: true enum_name: InputSizePreset header: Configure model input size. From 2eb4798a00f217da5a98c585bcd010acbdaad036 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 15 Sep 2023 16:51:41 +0900 Subject: [PATCH 18/27] Fix unit test failures Signed-off-by: Songki Choi --- src/otx/algorithms/classification/adapters/mmcls/task.py | 2 +- src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py | 2 +- src/otx/algorithms/common/utils/data.py | 3 +++ .../algorithms/detection/adapters/mmdet/utils/config_utils.py | 2 +- .../detection/configs/rotated_detection/configuration.yaml | 2 +- src/otx/algorithms/detection/utils/data.py | 2 +- src/otx/algorithms/segmentation/adapters/mmseg/task.py | 2 +- .../algorithms/detection/adapters/mmdet/test_configurer.py | 2 +- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/otx/algorithms/classification/adapters/mmcls/task.py b/src/otx/algorithms/classification/adapters/mmcls/task.py index 4b3ba682cce..e7f8fc68c52 100644 --- a/src/otx/algorithms/classification/adapters/mmcls/task.py +++ b/src/otx/algorithms/classification/adapters/mmcls/task.py @@ -676,7 +676,7 @@ def patch_input_preprocessing(deploy_cfg): mo_options.flags = list(set(mo_options.flags)) def patch_input_shape(deploy_cfg): - input_size_manager = InputSizeManager(cfg.data) + input_size_manager = InputSizeManager(cfg) size = input_size_manager.get_input_size_from_cfg("test") assert all(isinstance(i, int) and i > 0 for i in size) # default is static shape to prevent an unexpected error 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 7783fb1eced..3f69392e531 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -984,6 +984,6 @@ def area(x): # Closest preset input_size_preset = InputSizePreset.input_sizes() - input_size = InputSizeManager.select_closest_size(input_size, input_size_preset) + input_size = self.select_closest_size(input_size, input_size_preset) logger.info(f"-> Closest preset: {input_size}") return input_size diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index d3908b566e3..3c5a67f0f65 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -265,6 +265,9 @@ def compute_robust_scale_statistics(values: np.array) -> Dict[str, float]: Dict[str, float]: Robust avg, min, max values """ # Compute stat in log scale & convert back to original scale + if values.size == 0: + return {} + stat = compute_robust_statistics(np.log(values)) stat = {k: float(np.exp(v)) for k, v in stat.items()} stat["std"] = float(np.std(values)) # Normal scale std is better for understanding 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 6d8a1efe0ab..6935eb65e46 100644 --- a/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py +++ b/src/otx/algorithms/detection/adapters/mmdet/utils/config_utils.py @@ -210,7 +210,7 @@ def patch_input_shape(cfg: ConfigDict, deploy_cfg: ConfigDict): Returns: None: This function updates the input `deploy_cfg` object directly. """ - input_size_manager = InputSizeManager(cfg.data) + input_size_manager = InputSizeManager(cfg) size = input_size_manager.get_input_size_from_cfg("test") assert all(isinstance(i, int) and i > 0 for i in size) diff --git a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml index a34862d8f16..a4ff7c8bda7 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -227,7 +227,7 @@ learning_parameters: description: The input size of the given model could be configured to one of the predefined resolutions. Reduced training and inference time could be expected by using smaller input size. - Defaults to per-model default resolution. + Defaults to Auto, in which input size is automatically determined based on dataset statistics. editable: true enum_name: InputSizePreset header: Configure model input size. diff --git a/src/otx/algorithms/detection/utils/data.py b/src/otx/algorithms/detection/utils/data.py index 03eb89e1abd..5830e067ea1 100644 --- a/src/otx/algorithms/detection/utils/data.py +++ b/src/otx/algorithms/detection/utils/data.py @@ -447,7 +447,7 @@ def adaptive_tile_params( assert rule in ["min", "avg"], f"Unknown rule: {rule}" stat = compute_robust_dataset_statistics(dataset, ann_stat=True) - max_num_objects = stat["annotation"]["num_per_image"]["max"] + max_num_objects = round(stat["annotation"]["num_per_image"]["max"]) avg_size = stat["annotation"]["size_of_shape"]["avg"] min_size = stat["annotation"]["size_of_shape"]["robust_min"] max_size = stat["annotation"]["size_of_shape"]["robust_max"] diff --git a/src/otx/algorithms/segmentation/adapters/mmseg/task.py b/src/otx/algorithms/segmentation/adapters/mmseg/task.py index f371605e21f..b785003a284 100644 --- a/src/otx/algorithms/segmentation/adapters/mmseg/task.py +++ b/src/otx/algorithms/segmentation/adapters/mmseg/task.py @@ -535,7 +535,7 @@ def patch_input_preprocessing(deploy_cfg): mo_options.flags = list(set(mo_options.flags)) def patch_input_shape(deploy_cfg): - input_size_manager = InputSizeManager(cfg.data) + input_size_manager = InputSizeManager(cfg) size = input_size_manager.get_input_size_from_cfg("test") assert all(isinstance(i, int) and i > 0 for i in size) # default is static shape to prevent an unexpected error diff --git a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py index de8774113f6..0acc40d8746 100644 --- a/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py +++ b/tests/unit/algorithms/detection/adapters/mmdet/test_configurer.py @@ -201,7 +201,7 @@ def test_configure_input_size_yolox(self, mocker, is_yolox_tiny): self.configurer.configure_input_size(mock_cfg, InputSizePreset.DEFAULT) # check - mock_input_manager_cls.assert_called_once_with(mock_cfg.data, base_input_size) + mock_input_manager_cls.assert_called_once_with(mock_cfg, base_input_size) @e2e_pytest_unit def test_configure_fp16(self): From 448b72bcaae153bd50966aff8b33dfe92f047b50 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Fri, 15 Sep 2023 17:06:46 +0900 Subject: [PATCH 19/27] Update changelog.md Signed-off-by: Songki Choi --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8db0295ead..9b77377d757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ All notable changes to this project will be documented in this file. - 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 new argument to track resource usage in train command () +- Adapt input size automatically based on dataset statistics () ### Enhancements From 43cad8df9885196773009fc5487a45ebecb90aa1 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 18 Sep 2023 14:36:56 +0900 Subject: [PATCH 20/27] Fix Self-SL pipeline issue Signed-off-by: Songki Choi --- .../adapters/mmcv/utils/config_utils.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) 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 3f69392e531..1daece1911f 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -713,8 +713,12 @@ def set_input_size(self, input_size: Union[int, List[int], Tuple[int, int]]): input_size[1] / self.base_input_size[subset][1], ) pipelines = self._get_pipelines(subset) - for pipeline in pipelines: - self._set_pipeline_size_value(pipeline, resize_ratio) + if isinstance(pipelines, dict): + # Deals with {"view0": [...], "view1": [...]} + for pipeline in pipelines.values(): + self._set_pipeline_size_value(pipeline, resize_ratio) + else: + self._set_pipeline_size_value(pipelines, resize_ratio) # Set model size # - needed only for YOLOX @@ -770,10 +774,17 @@ def get_input_size_from_cfg( return None def _estimate_post_img_size( - self, pipelines: List[Dict], default_size: Optional[List[int]] = None + self, pipelines: Union[Dict, List[Dict]], default_size: Optional[List[int]] = None ) -> Union[List[int], None]: # NOTE: Mosaic isn't considered in this step because Mosaic and following RandomAffine don't change image size post_img_size = default_size + + if isinstance(pipelines, dict): + for pipeline in pipelines.values(): + # Deals with {"view0": [...], "view1": [...]} + # Just using the first one to estimate + return self._estimate_post_img_size(pipeline, post_img_size) + for pipeline in pipelines: if "resize" in pipeline["type"].lower(): img_size = self._get_size_value(pipeline, "resize") @@ -838,7 +849,13 @@ def _get_pipelines(self, subset: str): return self._data_config[subset]["dataset"]["pipeline"] raise RuntimeError("Failed to find pipeline.") - def _set_pipeline_size_value(self, pipeline: Dict, scale: Tuple[Union[int, float], Union[int, float]]): + def _set_pipeline_size_value(self, pipeline: Union[Dict, List[Dict]], scale: Tuple[Union[int, float], Union[int, float]]): + + if isinstance(pipeline, list): + for sub_pipeline in pipeline: + self._set_pipeline_size_value(sub_pipeline, scale) + return + updated = False for pipeline_name, pipeline_attrs in self.PIPELINE_TO_CHANGE.items(): if pipeline_name in pipeline["type"].lower(): From a245f249a466a776dc31628288cbf1f4bb53f354 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Mon, 18 Sep 2023 18:46:58 +0900 Subject: [PATCH 21/27] Diable Auto mode test for DINO variants temporarily Signed-off-by: Songki Choi --- tests/integration/cli/detection/test_detection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index 866b8e322fe..0d4be167b06 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -96,7 +96,11 @@ class TestDetectionCLI: @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) def test_otx_train(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "detection" - otx_train_testing(template, tmp_dir_path, otx_dir, args) + # FIXME: remove this block once Issue#2054 resolved + _args = args.copy() + if "DINO" in template.name: + _args["train_params"] = ["params", "--learning_parameters.num_iters", "1", "--learning_parameters.batch_size", "4", "--learning_parameters.input_size", "Default"] + otx_train_testing(template, tmp_dir_path, otx_dir, _args) @e2e_pytest_component @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) From 85102991ce1e99f869a52c6105d3ea33dce7fd64 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 19 Sep 2023 09:50:37 +0900 Subject: [PATCH 22/27] Fix pre-commit Signed-off-by: Songki Choi --- src/otx/algorithms/common/adapters/mmcv/configurer.py | 1 - .../common/adapters/mmcv/utils/config_utils.py | 4 +++- tests/integration/cli/detection/test_detection.py | 10 +++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/otx/algorithms/common/adapters/mmcv/configurer.py b/src/otx/algorithms/common/adapters/mmcv/configurer.py index 8c43f9ba636..6b1c1aaa4ef 100644 --- a/src/otx/algorithms/common/adapters/mmcv/configurer.py +++ b/src/otx/algorithms/common/adapters/mmcv/configurer.py @@ -20,7 +20,6 @@ ) from otx.algorithms.common.adapters.mmcv.utils.config_utils import ( InputSizeManager, - InputSizePreset, patch_color_conversion, patch_from_hyperparams, recursively_update_cfg, 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 efb7032450e..f4643117bd9 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -820,7 +820,9 @@ def _get_pipelines(self, subset: str): return self._data_config[subset]["dataset"]["pipeline"] raise RuntimeError("Failed to find pipeline.") - def _set_pipeline_size_value(self, pipeline: Union[Dict, List[Dict]], scale: Tuple[Union[int, float], Union[int, float]]): + def _set_pipeline_size_value( + self, pipeline: Union[Dict, List[Dict]], scale: Tuple[Union[int, float], Union[int, float]] + ): if isinstance(pipeline, list): for sub_pipeline in pipeline: diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index 0d4be167b06..400499d60ae 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -99,7 +99,15 @@ def test_otx_train(self, template, tmp_dir_path): # FIXME: remove this block once Issue#2054 resolved _args = args.copy() if "DINO" in template.name: - _args["train_params"] = ["params", "--learning_parameters.num_iters", "1", "--learning_parameters.batch_size", "4", "--learning_parameters.input_size", "Default"] + _args["train_params"] = [ + "params", + "--learning_parameters.num_iters", + "1", + "--learning_parameters.batch_size", + "4", + "--learning_parameters.input_size", + "Default", + ] otx_train_testing(template, tmp_dir_path, otx_dir, _args) @e2e_pytest_component From 6d76217fe541bb0a2486307b0a0213bb0b668ea1 Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Tue, 19 Sep 2023 11:41:52 +0900 Subject: [PATCH 23/27] Fix integration test failure Signed-off-by: Songki Choi --- .../integration/cli/detection/test_detection.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index 400499d60ae..76d838543f9 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -96,8 +96,8 @@ class TestDetectionCLI: @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) def test_otx_train(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "detection" - # FIXME: remove this block once Issue#2054 resolved _args = args.copy() + # FIXME: remove this block once Issue#2054 resolved if "DINO" in template.name: _args["train_params"] = [ "params", @@ -114,14 +114,19 @@ def test_otx_train(self, template, tmp_dir_path): @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) def test_otx_resume(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "detection/test_resume" - otx_resume_testing(template, tmp_dir_path, otx_dir, args) + _args = args.copy() + _resume_params = resume_params.copy() + # FIXME: remove this block once Issue#2054 resolved + if "DINO" in template.name: + _args["train_params"].extend(["--learning_parameters.input_size", "Default"]) + _resume_params.extend(["--learning_parameters.input_size", "Default"]) + otx_resume_testing(template, tmp_dir_path, otx_dir, _args) template_work_dir = get_template_dir(template, tmp_dir_path) - args1 = copy.deepcopy(args) - args1["train_params"] = resume_params - args1[ + _args["train_params"] = _resume_params + _args[ "--resume-from" ] = f"{template_work_dir}/trained_for_resume_{template.model_template_id}/models/weights.pth" - otx_resume_testing(template, tmp_dir_path, otx_dir, args1) + otx_resume_testing(template, tmp_dir_path, otx_dir, _args) @e2e_pytest_component @pytest.mark.parametrize("template", templates_w_experimental, ids=templates_ids_w_experimental) From e137dca1f870fdd44bd543d521fdc6a9bacb33eb Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 20 Sep 2023 13:56:43 +0900 Subject: [PATCH 24/27] Address code review suggestions Signed-off-by: Songki Choi --- .../common/adapters/mmcv/utils/config_utils.py | 14 +++++++------- src/otx/algorithms/common/utils/data.py | 14 +++++++------- tests/integration/cli/detection/test_detection.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) 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 f4643117bd9..f8853eef1e7 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -724,7 +724,7 @@ def base_input_size(self) -> Union[Tuple[int, int], Dict[str, Tuple[int, int]]]: def get_input_size_from_cfg( self, subset: Union[str, List[str]] = ["test", "val", "train"] - ) -> Union[None, Tuple[int, int]]: + ) -> Optional[Tuple[int, int]]: """Estimate image size using data pipeline. Args: @@ -920,22 +920,22 @@ def select_closest_size(input_size: Tuple[int, int], preset_sizes: List[Tuple[in if len(preset_sizes) == 0: return input_size - def scale_of(x): + def to_log_scale(x): return np.log(np.sqrt(x[0] * x[1])) - input_scale = scale_of(input_size) - preset_scales = np.array(list(map(scale_of, preset_sizes))) + input_scale = to_log_scale(input_size) + preset_scales = np.array(list(map(to_log_scale, preset_sizes))) abs_diff = np.abs(preset_scales - input_scale) return preset_sizes[np.argmin(abs_diff)] def adapt_input_size_to_dataset( - self, max_image_size: float, min_object_size: float = None, downscale_only: bool = True + self, max_image_size: int, min_object_size: Optional[int] = None, downscale_only: bool = True ) -> Tuple[int, int]: """Compute appropriate model input size w.r.t. dataset statistics. Args: max_image_size (int): Typical large image size of dataset in pixels. - min_object_size (int): Typical small object size of dataset in pixels. + min_object_size (int, optional): Typical small object size of dataset in pixels. None to consider only image size. Defaults to None. downscale_only (bool) : Whether to allow only smaller size than default setting. Defaults to True. @@ -947,7 +947,7 @@ def adapt_input_size_to_dataset( base_input_size = self.base_input_size if isinstance(base_input_size, Dict): - base_input_size = base_input_size.get("test", None) + base_input_size = base_input_size.get("train", base_input_size.get("test", None)) logger.info(f"-> Current base input size: {base_input_size}") if max_image_size <= 0: diff --git a/src/otx/algorithms/common/utils/data.py b/src/otx/algorithms/common/utils/data.py index 3c5a67f0f65..75fa6f2a201 100644 --- a/src/otx/algorithms/common/utils/data.py +++ b/src/otx/algorithms/common/utils/data.py @@ -284,13 +284,13 @@ def compute_robust_dataset_statistics(dataset: DatasetEntity, ann_stat=False, ma Returns: Dict[str, Any]: Robust avg, min, max values for images, and annotations optionally. - e.x) stat = { - "image": {"avg": ...}, - "annotation": { - "num_per_image": {"avg": ...}, - "size_of_shape": {"avg": ...}, - } - } + ex) stat = { + "image": {"avg": ...}, + "annotation": { + "num_per_image": {"avg": ...}, + "size_of_shape": {"avg": ...}, + } + } """ stat: Dict = {} if len(dataset) == 0 or max_samples <= 0: diff --git a/tests/integration/cli/detection/test_detection.py b/tests/integration/cli/detection/test_detection.py index 76d838543f9..b993355fe38 100644 --- a/tests/integration/cli/detection/test_detection.py +++ b/tests/integration/cli/detection/test_detection.py @@ -97,7 +97,7 @@ class TestDetectionCLI: def test_otx_train(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "detection" _args = args.copy() - # FIXME: remove this block once Issue#2054 resolved + # FIXME: remove this block once Issue#2504 resolved if "DINO" in template.name: _args["train_params"] = [ "params", @@ -116,7 +116,7 @@ def test_otx_resume(self, template, tmp_dir_path): tmp_dir_path = tmp_dir_path / "detection/test_resume" _args = args.copy() _resume_params = resume_params.copy() - # FIXME: remove this block once Issue#2054 resolved + # FIXME: remove this block once Issue#2504 resolved if "DINO" in template.name: _args["train_params"].extend(["--learning_parameters.input_size", "Default"]) _resume_params.extend(["--learning_parameters.input_size", "Default"]) From 44469359e4f111408a9e86968ab93ebbc334937e Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 20 Sep 2023 14:33:25 +0900 Subject: [PATCH 25/27] Fix pre-commit Signed-off-by: Songki Choi --- src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f8853eef1e7..b52a225ff9b 100644 --- a/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py +++ b/src/otx/algorithms/common/adapters/mmcv/utils/config_utils.py @@ -958,7 +958,7 @@ def adapt_input_size_to_dataset( # Refine using annotation shape size stat if min_object_size is not None and min_object_size > 0: - image_size = image_size * self.MIN_RECOGNIZABLE_OBJECT_SIZE / min_object_size + image_size = round(image_size * self.MIN_RECOGNIZABLE_OBJECT_SIZE / min_object_size) logger.info(f"-> Based on typical small object size {min_object_size}: {image_size}") input_size = (round(image_size), round(image_size)) From 43c8723379141963e33c1d58e6a381ba9dfa6e8c Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 20 Sep 2023 14:44:14 +0900 Subject: [PATCH 26/27] 'Default' as default mode of input size config Signed-off-by: Songki Choi --- .../classification/configs/configuration.yaml | 4 +- .../configs/detection/configuration.yaml | 4 +- .../instance_segmentation/configuration.yaml | 4 +- .../rotated_detection/configuration.yaml | 102 +++++++++++++++++- .../segmentation/configs/configuration.yaml | 4 +- 5 files changed, 109 insertions(+), 9 deletions(-) diff --git a/src/otx/algorithms/classification/configs/configuration.yaml b/src/otx/algorithms/classification/configs/configuration.yaml index 099c20af4f8..2ddc91fe803 100644 --- a/src/otx/algorithms/classification/configs/configuration.yaml +++ b/src/otx/algorithms/classification/configs/configuration.yaml @@ -277,11 +277,11 @@ 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. + 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..52ddff1b4de 100644 --- a/src/otx/algorithms/detection/configs/detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/detection/configuration.yaml @@ -245,11 +245,11 @@ 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. + 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..efb76d31d2e 100644 --- a/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml +++ b/src/otx/algorithms/detection/configs/instance_segmentation/configuration.yaml @@ -245,11 +245,11 @@ 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. + 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 04005843a9e..ecc3e920721 100644 --- a/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml +++ b/src/otx/algorithms/detection/configs/rotated_detection/configuration.yaml @@ -24,6 +24,28 @@ learning_parameters: Increasing this value may cause the system to use more memory than available, potentially causing out of memory errors, please update with caution. auto_hpo_state: NOT_POSSIBLE + inference_batch_size: + affects_outcome_of: TRAINING + default_value: 1 + description: The number of samples seen in each iteration of inference. + Increasing this value improves inference time and may make the inference more + stable. A larger batch size has higher memory requirements. + editable: true + header: Inference batch size + max_value: 512 + min_value: 1 + type: INTEGER + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1 + visible_in_ui: true + warning: + Increasing this value may cause the system to use more memory than available, + potentially causing out of memory errors, please update with caution. + auto_hpo_state: NOT_POSSIBLE description: Learning Parameters header: Learning Parameters learning_rate: @@ -227,7 +249,7 @@ learning_parameters: 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. + Defaults to per-model default resolution. editable: true enum_name: InputSizePreset header: Configure model input size. @@ -248,6 +270,8 @@ learning_parameters: value: Default visible_in_ui: false warning: Modifying input size may decrease model performance. + type: PARAMETER_GROUP + visible_in_ui: true postprocessing: confidence_threshold: affects_outcome_of: INFERENCE @@ -326,6 +350,28 @@ algo_backend: 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 @@ -546,5 +592,59 @@ tiling_parameters: visible_in_ui: true warning: null + tile_ir_scale_factor: + header: OpenVINO IR Scale Factor + description: The purpose of the scale parameter is to optimize the performance and efficiency of tiling in OpenVINO IR during inference. By controlling the increase in tile size and input size, the scale parameter allows for more efficient parallelization of the workload and improve the overall performance and efficiency of the inference process on OpenVINO. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 1.0 + max_value: 4.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + tile_sampling_ratio: + header: Sampling Ratio for entire tiling + description: Since tiling train and validation to all tile from large image, usually it takes lots of time than normal training. The tile_sampling_ratio is ratio for sampling entire tile dataset. Sampling tile dataset would save lots of time for training and validation time. Note that sampling will be applied to training and validation dataset, not test dataset. + affects_outcome_of: TRAINING + default_value: 1.0 + min_value: 0.000001 + max_value: 1.0 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 1.0 + visible_in_ui: true + warning: null + + object_tile_ratio: + header: Object tile ratio + description: The desired ratio of min object size and tile size. + affects_outcome_of: TRAINING + default_value: 0.03 + min_value: 0.00 + max_value: 1.00 + type: FLOAT + editable: true + ui_rules: + action: DISABLE_EDITING + operator: AND + rules: [] + type: UI_RULES + value: 0.03 + visible_in_ui: false + warning: null + type: PARAMETER_GROUP visible_in_ui: true diff --git a/src/otx/algorithms/segmentation/configs/configuration.yaml b/src/otx/algorithms/segmentation/configs/configuration.yaml index 4e4d859ad5e..64a2dcedc9c 100644 --- a/src/otx/algorithms/segmentation/configs/configuration.yaml +++ b/src/otx/algorithms/segmentation/configs/configuration.yaml @@ -232,11 +232,11 @@ 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. + Defaults to per-model default resolution. editable: true enum_name: InputSizePreset header: Configure model input size. From 2cb5e159cb39aa01e233e4aef3ec75705bc8cdfa Mon Sep 17 00:00:00 2001 From: Songki Choi Date: Wed, 20 Sep 2023 15:23:07 +0900 Subject: [PATCH 27/27] Update document Signed-off-by: Songki Choi --- .../auto_configuration.rst | 7 ++++ .../additional_features/config_input_size.rst | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/docs/source/guide/explanation/additional_features/auto_configuration.rst b/docs/source/guide/explanation/additional_features/auto_configuration.rst index 9ad223d5813..5e91f239753 100644 --- a/docs/source/guide/explanation/additional_features/auto_configuration.rst +++ b/docs/source/guide/explanation/additional_features/auto_configuration.rst @@ -120,3 +120,10 @@ OpenVINO™ Training Extensions will automatically recognize these types of task .. note:: To use auto template configuration with Self-SL training type `--task` option is required since it is impossible to recognize task type by folder with only images. + +Auto-adapt input size +--------------------- + +"Auto" input size feature tries to automatically select the right model input size +based on given dataset statictics. +See :ref:`adaptive-input-size`. diff --git a/docs/source/guide/explanation/additional_features/config_input_size.rst b/docs/source/guide/explanation/additional_features/config_input_size.rst index 90c4305631b..715831b07e5 100644 --- a/docs/source/guide/explanation/additional_features/config_input_size.rst +++ b/docs/source/guide/explanation/additional_features/config_input_size.rst @@ -21,10 +21,50 @@ The available input sizes are currently as follows: - 64x64 (only for classification) - 128x128 (only for classification) +- 224x224 (only for classification) - 256x256 - 384x384 - 512x512 +- 768x768 - 1024x1024 +- Default (per-model default input size) +- Auto (adaptive to dataset statistics) + +.. _adaptive-input-size: + +Adaptive Input Size +------------------- + +"Auto" mode tries to automatically select the right size +based on given dataset statictics. + +1. OTX analyzes the input dataset to get robust statistics. + +2. Input size is initially set to typical large image size. + +.. code-block:: + + input_size = large_image_size + +3. (Optionally) Input size is adjusted by object sizes in the dataset, if any. + The input size from image size is rescaled accoridng to the ratio of + minimum recongnizable object size of models, which is typically 16x16 ~ 32x32, + and the typical small object size in the dataset. + In short, if objects are 64x64 in general in 512x512 image, + it will be down-scaled to 256x256 as 32x32 objects are enough to be detected. + +.. code-block:: + + input_size = input_size * MIN_RECOGNIZABLE_OBJECT_SIZE / small_object_size + +4. Select the closest size from standard preset sizes + +5. Restrict scale-up + +.. code-block:: + + input_size = min(input_size, default_model_input_size) + .. Note:: Using smaller input size with datasets having lower image resolutions or larger objects can yield a speed advantage with minimal impact on model performance.