diff --git a/docs/source/guide/reference/mpa/modules/models/backbones.rst b/docs/source/guide/reference/mpa/modules/models/backbones.rst deleted file mode 100644 index 249f934ebb5..00000000000 --- a/docs/source/guide/reference/mpa/modules/models/backbones.rst +++ /dev/null @@ -1,14 +0,0 @@ -Backbones -^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.mpa.modules.models.backbones - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.backbones.litehrnet - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/models/heads.rst b/docs/source/guide/reference/mpa/modules/models/heads.rst index 5b5a5de1c83..8fbf6601254 100644 --- a/docs/source/guide/reference/mpa/modules/models/heads.rst +++ b/docs/source/guide/reference/mpa/modules/models/heads.rst @@ -9,19 +9,10 @@ Heads :members: :undoc-members: -.. automodule:: otx.mpa.modules.models.heads.aggregator_mixin - :members: - :undoc-members: - - .. automodule:: otx.mpa.modules.models.heads.custom_cls_head :members: :undoc-members: -.. automodule:: otx.mpa.modules.models.heads.custom_fcn_head - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.models.heads.custom_hierarchical_linear_cls_head :members: :undoc-members: @@ -38,22 +29,10 @@ Heads :members: :undoc-members: -.. automodule:: otx.mpa.modules.models.heads.mix_loss_mixin - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.models.heads.non_linear_cls_head :members: :undoc-members: -.. automodule:: otx.mpa.modules.models.heads.pixel_weights_mixin - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.heads.segment_out_norm_mixin - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.models.heads.semisl_cls_head :members: :undoc-members: diff --git a/docs/source/guide/reference/mpa/modules/models/index.rst b/docs/source/guide/reference/mpa/modules/models/index.rst index 45d93070e47..65621b35cdc 100644 --- a/docs/source/guide/reference/mpa/modules/models/index.rst +++ b/docs/source/guide/reference/mpa/modules/models/index.rst @@ -4,10 +4,7 @@ Models .. toctree:: :maxdepth: 1 - backbones classifiers heads losses - scalar_schedulers - segmentors utils \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/models/losses.rst b/docs/source/guide/reference/mpa/modules/models/losses.rst index d084ee3a9ab..f4c9301cb99 100644 --- a/docs/source/guide/reference/mpa/modules/models/losses.rst +++ b/docs/source/guide/reference/mpa/modules/models/losses.rst @@ -21,18 +21,6 @@ Losses :members: :undoc-members: -.. automodule:: otx.mpa.modules.models.losses.base_pixel_loss - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.losses.base_weighted_loss - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.losses.cross_entropy_loss_with_ignore - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.models.losses.cross_entropy_loss :members: :undoc-members: @@ -41,11 +29,6 @@ Losses :members: :undoc-members: - -.. automodule:: otx.mpa.modules.models.losses.mpa_pixel_base - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.models.losses.utils :members: :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/models/scalar_schedulers.rst b/docs/source/guide/reference/mpa/modules/models/scalar_schedulers.rst deleted file mode 100644 index 869c52a169b..00000000000 --- a/docs/source/guide/reference/mpa/modules/models/scalar_schedulers.rst +++ /dev/null @@ -1,10 +0,0 @@ -Scalar Schedulers -^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.mpa.modules.models.scalar_schedulers - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/models/segmentors.rst b/docs/source/guide/reference/mpa/modules/models/segmentors.rst deleted file mode 100644 index 48184f2e04c..00000000000 --- a/docs/source/guide/reference/mpa/modules/models/segmentors.rst +++ /dev/null @@ -1,30 +0,0 @@ -Segmentors -^^^^^^^^^^ - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. automodule:: otx.mpa.modules.models.segmentors - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.segmentors.class_incr_encoder_decoder - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.segmentors.mean_teacher_segmentor - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.segmentors.mix_loss_mixin - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.segmentors.otx_encoder_decoder - :members: - :undoc-members: - -.. automodule:: otx.mpa.modules.models.segmentors.pixel_weights_mixin - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/ov/models.rst b/docs/source/guide/reference/mpa/modules/ov/models.rst index 362d788172d..15ed83acb06 100644 --- a/docs/source/guide/reference/mpa/modules/ov/models.rst +++ b/docs/source/guide/reference/mpa/modules/ov/models.rst @@ -24,7 +24,3 @@ Models .. automodule:: otx.mpa.modules.ov.models.mmcls :members: :undoc-members: - -.. automodule:: otx.mpa.modules.ov.models.mmseg - :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/guide/reference/mpa/modules/utils.rst b/docs/source/guide/reference/mpa/modules/utils.rst index a22e69d158c..340be22339b 100644 --- a/docs/source/guide/reference/mpa/modules/utils.rst +++ b/docs/source/guide/reference/mpa/modules/utils.rst @@ -13,10 +13,6 @@ Utils :members: :undoc-members: -.. automodule:: otx.mpa.modules.utils.seg_utils - :members: - :undoc-members: - .. automodule:: otx.mpa.modules.utils.task_adapt :members: :undoc-members: \ No newline at end of file diff --git a/otx/algorithms/segmentation/adapters/__init__.py b/otx/algorithms/segmentation/adapters/__init__.py index 8830b8e5239..53d34a44210 100644 --- a/otx/algorithms/segmentation/adapters/__init__.py +++ b/otx/algorithms/segmentation/adapters/__init__.py @@ -1,4 +1,16 @@ """Adapters for Segmentation.""" + # Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/otx/algorithms/segmentation/adapters/mmseg/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/__init__.py index d8754290882..4651aac0f2b 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/__init__.py @@ -1,10 +1,38 @@ """OTX Adapters - mmseg.""" -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -from .data import MPASegDataset -from .models import DetConB, DetConLoss, SelfSLMLP, SupConDetConB +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .datasets import MPASegDataset +from .models import ( + ClassIncrEncoderDecoder, + ConstantScalarScheduler, + CrossEntropyLossWithIgnore, + CustomFCNHead, + DetConB, + DetConLoss, + LiteHRNet, + MeanTeacherSegmentor, + MMOVBackbone, + MMOVDecodeHead, + PolyScalarScheduler, + SelfSLMLP, + StepScalarScheduler, + SupConDetConB, +) # fmt: off # isort: off @@ -16,4 +44,20 @@ # fmt: off # isort: on -__all__ = ["MPASegDataset", "DetConLoss", "SelfSLMLP", "DetConB", "SupConDetConB"] +__all__ = [ + "MPASegDataset", + "LiteHRNet", + "MMOVBackbone", + "CustomFCNHead", + "MMOVDecodeHead", + "DetConLoss", + "SelfSLMLP", + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", + "DetConB", + "CrossEntropyLossWithIgnore", + "SupConDetConB", + "ClassIncrEncoderDecoder", + "MeanTeacherSegmentor", +] diff --git a/otx/algorithms/segmentation/adapters/mmseg/data/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py similarity index 89% rename from otx/algorithms/segmentation/adapters/mmseg/data/__init__.py rename to otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py index f62eeed6289..6072fc61b45 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/data/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/__init__.py @@ -1,6 +1,6 @@ """OTX Algorithms - Segmentation Dataset.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,13 +18,17 @@ from .pipelines import ( LoadAnnotationFromOTXDataset, LoadImageFromOTXDataset, + MaskCompose, + ProbCompose, TwoCropTransform, ) __all__ = [ - "get_annotation_mmseg_format", - "LoadImageFromOTXDataset", "LoadAnnotationFromOTXDataset", - "MPASegDataset", + "LoadImageFromOTXDataset", + "MaskCompose", + "ProbCompose", "TwoCropTransform", + "get_annotation_mmseg_format", + "MPASegDataset", ] diff --git a/otx/algorithms/segmentation/adapters/mmseg/data/dataset.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py similarity index 99% rename from otx/algorithms/segmentation/adapters/mmseg/data/dataset.py rename to otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py index 9e506a04cc2..eb267d4fe12 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/data/dataset.py +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/dataset.py @@ -1,6 +1,6 @@ """Base MMDataset for Segmentation Task.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py new file mode 100644 index 00000000000..ec2878f7abc --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py @@ -0,0 +1,21 @@ +"""OTX Algorithms - Segmentation pipelines.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .compose import MaskCompose, ProbCompose +from .loads import LoadAnnotationFromOTXDataset, LoadImageFromOTXDataset +from .transforms import TwoCropTransform + +__all__ = ["MaskCompose", "ProbCompose", "LoadImageFromOTXDataset", "LoadAnnotationFromOTXDataset", "TwoCropTransform"] diff --git a/otx/mpa/modules/datasets/pipelines/compose.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py similarity index 87% rename from otx/mpa/modules/datasets/pipelines/compose.py rename to otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py index f8ea7953a62..99ecfc4bf36 100644 --- a/otx/mpa/modules/datasets/pipelines/compose.py +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/compose.py @@ -1,4 +1,6 @@ -# Copyright (C) 2022 Intel Corporation +"""Collection of compose pipelines for segmentation task.""" + +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -11,8 +13,11 @@ from scipy.ndimage import gaussian_filter +# pylint: disable=consider-using-f-string @PIPELINES.register_module() -class ProbCompose(object): +class ProbCompose: + """Compose pipelines in a list and enable or disable them with the probability.""" + def __init__(self, transforms, probs): assert isinstance(transforms, Sequence) assert isinstance(probs, Sequence) @@ -35,6 +40,7 @@ def __init__(self, transforms, probs): raise TypeError(f"transform must be callable or a dict, but got {type(transform)}") def __call__(self, data): + """Callback function of ProbCompose.""" rand_value = np.random.rand() transform_id = np.max(np.where(rand_value > self.limits)[0]) @@ -44,17 +50,20 @@ def __call__(self, data): return data def __repr__(self): + """Repr.""" format_string = self.__class__.__name__ + "(" for t in self.transforms: format_string += "\n" - format_string += " {0}".format(t) + format_string += f" {t}" format_string += "\n)" return format_string @PIPELINES.register_module() -class MaskCompose(object): +class MaskCompose: + """Compose mask-related pipelines in a list and enable or disable them with the probability.""" + def __init__(self, transforms, prob, lambda_limits=(4, 16), keep_original=False): self.keep_original = keep_original self.prob = prob @@ -102,6 +111,7 @@ def _mix_img(main_img, aux_img, mask): return np.where(np.expand_dims(mask, axis=2), main_img, aux_img) def __call__(self, data): + """Callback function of MaskCompose.""" main_data = self._apply_transforms(deepcopy(data), self.transforms) assert main_data is not None if not self.keep_original and np.random.rand() > self.prob: @@ -123,10 +133,11 @@ def __call__(self, data): return main_data def __repr__(self): + """Repr.""" format_string = self.__class__.__name__ + "(" for t in self.transforms: format_string += "\n" - format_string += " {0}".format(t) + format_string += f" {t}" format_string += "\n)" return format_string diff --git a/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py new file mode 100644 index 00000000000..e35c589f492 --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/loads.py @@ -0,0 +1,57 @@ +"""Collection of load pipelines for segmentation task.""" + +# Copyright (C) 2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +from typing import Any, Dict + +from mmseg.datasets.builder import PIPELINES + +import otx.core.data.pipelines.load_image_from_otx_dataset as load_image_base +from otx.algorithms.segmentation.adapters.mmseg.datasets.dataset import ( + get_annotation_mmseg_format, +) +from otx.api.utils.argument_checks import check_input_parameters_type + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +@PIPELINES.register_module() +class LoadImageFromOTXDataset(load_image_base.LoadImageFromOTXDataset): + """Pipeline element that loads an image from a OTX Dataset on the fly.""" + + +@PIPELINES.register_module() +class LoadAnnotationFromOTXDataset: + """Pipeline element that loads an annotation from a OTX Dataset on the fly. + + Expected entries in the 'results' dict that should be passed to this pipeline element are: + results['dataset_item']: dataset_item from which to load the annotation + results['ann_info']['label_list']: list of all labels in the project + + """ + + def __init__(self): + pass + + @check_input_parameters_type() + def __call__(self, results: Dict[str, Any]): + """Callback function of LoadAnnotationFromOTXDataset.""" + dataset_item = results["dataset_item"] + labels = results["ann_info"]["labels"] + + ann_info = get_annotation_mmseg_format(dataset_item, labels) + + results["gt_semantic_seg"] = ann_info["gt_semantic_seg"] + results["seg_fields"].append("gt_semantic_seg") + + return results diff --git a/otx/algorithms/segmentation/adapters/mmseg/data/pipelines.py b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py similarity index 65% rename from otx/algorithms/segmentation/adapters/mmseg/data/pipelines.py rename to otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py index 9d0f0278954..2ba3ada9e27 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/data/pipelines.py +++ b/otx/algorithms/segmentation/adapters/mmseg/datasets/pipelines/transforms.py @@ -1,66 +1,156 @@ -"""Collection Pipeline for segmentation task.""" -# Copyright (C) 2021 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 +"""Collection of transfrom pipelines for segmentation task.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. + from copy import deepcopy from typing import Any, Dict, List +import mmcv import numpy as np +from mmcv.parallel import DataContainer as DC from mmcv.utils import build_from_cfg from mmseg.datasets.builder import PIPELINES from mmseg.datasets.pipelines import Compose +from mmseg.datasets.pipelines.formatting import to_tensor from PIL import Image from torchvision import transforms as T from torchvision.transforms import functional as F -import otx.core.data.pipelines.load_image_from_otx_dataset as load_image_base from otx.api.utils.argument_checks import check_input_parameters_type -from .dataset import get_annotation_mmseg_format +@PIPELINES.register_module(force=True) +class Normalize: + """Normalize the image. -# pylint: disable=too-many-instance-attributes, too-many-arguments -@PIPELINES.register_module() -class LoadImageFromOTXDataset(load_image_base.LoadImageFromOTXDataset): - """Pipeline element that loads an image from a OTX Dataset on the fly.""" + Added key is "img_norm_cfg". + + Args: + mean (sequence): Mean values of 3 channels. + std (sequence): Std values of 3 channels. + to_rgb (bool): Whether to convert the image from BGR to RGB, + default is true. + """ + def __init__(self, mean, std, to_rgb=True): + self.mean = np.array(mean, dtype=np.float32) + self.std = np.array(std, dtype=np.float32) + self.to_rgb = to_rgb -@PIPELINES.register_module() -class LoadAnnotationFromOTXDataset: - """Pipeline element that loads an annotation from a OTX Dataset on the fly. + def __call__(self, results): + """Call function to normalize images. + + Args: + results (dict): Result dict from loading pipeline. + + Returns: + dict: Normalized results, 'img_norm_cfg' key is added into + result dict. + """ - Expected entries in the 'results' dict that should be passed to this pipeline element are: - results['dataset_item']: dataset_item from which to load the annotation - results['ann_info']['label_list']: list of all labels in the project + for target in ["img", "ul_w_img", "aux_img"]: + if target in results: + results[target] = mmcv.imnormalize(results[target], self.mean, self.std, self.to_rgb) + results["img_norm_cfg"] = dict(mean=self.mean, std=self.std, to_rgb=self.to_rgb) + return results + + def __repr__(self): + """Repr.""" + repr_str = self.__class__.__name__ + repr_str += f"(mean={self.mean}, std={self.std}, to_rgb=" f"{self.to_rgb})" + return repr_str + + +@PIPELINES.register_module(force=True) +class DefaultFormatBundle: + """Default formatting bundle. + + It simplifies the pipeline of formatting common fields, including "img" + and "gt_semantic_seg". These fields are formatted as follows. + + - img: (1)transpose, (2)to tensor, (3)to DataContainer (stack=True) + - gt_semantic_seg: (1)unsqueeze dim-0 (2)to tensor, + (3)to DataContainer (stack=True) """ - def __init__(self): - pass + def __call__(self, results): + """Call function to transform and format common fields in results. - @check_input_parameters_type() - def __call__(self, results: Dict[str, Any]): - """Callback function of LoadAnnotationFromOTXDataset.""" - dataset_item = results["dataset_item"] - labels = results["ann_info"]["labels"] + Args: + results (dict): Result dict contains the data to convert. - ann_info = get_annotation_mmseg_format(dataset_item, labels) + Returns: + dict: The result dict contains the data that is formatted with + default bundle. + """ + for target in ["img", "ul_w_img", "aux_img"]: + if target not in results: + continue + + img = results[target] + if len(img.shape) < 3: + img = np.expand_dims(img, -1) + + if len(img.shape) == 3: + img = np.ascontiguousarray(img.transpose(2, 0, 1)).astype(np.float32) + elif len(img.shape) == 4: + # for selfsl or supcon + img = np.ascontiguousarray(img.transpose(0, 3, 1, 2)).astype(np.float32) + else: + raise ValueError(f"img.shape={img.shape} is not supported.") + + results[target] = DC(to_tensor(img), stack=True) + + for trg_name in ["gt_semantic_seg", "gt_class_borders", "pixel_weights"]: + if trg_name not in results: + continue + + out_type = np.float32 if trg_name == "pixel_weights" else np.int64 + results[trg_name] = DC(to_tensor(results[trg_name][None, ...].astype(out_type)), stack=True) + + return results + + def __repr__(self): + """Repr.""" + return self.__class__.__name__ + + +@PIPELINES.register_module() +class BranchImage: + """Branch images by copying with name of key. - results["gt_semantic_seg"] = ann_info["gt_semantic_seg"] - results["seg_fields"].append("gt_semantic_seg") + Args: + key_map (dict): keys to name each image. + """ + + def __init__(self, key_map): + self.key_map = key_map + + def __call__(self, results): + """Call function to branch images in img_fields in results. + Args: + results (dict): Result dict contains the image data to branch. + + Returns: + dict: The result dict contains the original image data and copied image data. + """ + for key1, key2 in self.key_map.items(): + if key1 in results: + results[key2] = results[key1] + if key1 in results["img_fields"]: + results["img_fields"].append(key2) return results + def __repr__(self): + """Repr.""" + + repr_str = self.__class__.__name__ + return repr_str + @PIPELINES.register_module() class TwoCropTransform: diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py index d4ef2e9c4ef..fa66af700d4 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/__init__.py @@ -1,6 +1,6 @@ """Adapters for OTX Common Algorithm. - mmseg.model.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,9 +14,35 @@ # See the License for the specific language governing permissions # and limitations under the License. - -from .losses import DetConLoss +from .backbones import LiteHRNet, MMOVBackbone +from .heads import CustomFCNHead, MMOVDecodeHead +from .losses import CrossEntropyLossWithIgnore, DetConLoss from .necks import SelfSLMLP -from .segmentors import DetConB, SupConDetConB +from .schedulers import ( + ConstantScalarScheduler, + PolyScalarScheduler, + StepScalarScheduler, +) +from .segmentors import ( + ClassIncrEncoderDecoder, + DetConB, + MeanTeacherSegmentor, + SupConDetConB, +) -__all__ = ["DetConLoss", "SelfSLMLP", "DetConB", "SupConDetConB"] +__all__ = [ + "LiteHRNet", + "MMOVBackbone", + "CustomFCNHead", + "MMOVDecodeHead", + "DetConLoss", + "SelfSLMLP", + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", + "DetConB", + "CrossEntropyLossWithIgnore", + "SupConDetConB", + "ClassIncrEncoderDecoder", + "MeanTeacherSegmentor", +] diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py new file mode 100644 index 00000000000..a241bbb48f8 --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py @@ -0,0 +1,24 @@ +"""Backbones for semantic segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .litehrnet import LiteHRNet +from .mmov_backbone import MMOVBackbone + +__all__ = [ + "LiteHRNet", + "MMOVBackbone", +] diff --git a/otx/mpa/modules/models/backbones/litehrnet.py b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py similarity index 96% rename from otx/mpa/modules/models/backbones/litehrnet.py rename to otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py index 85f7df0ac31..acbe46316af 100644 --- a/otx/mpa/modules/models/backbones/litehrnet.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/litehrnet.py @@ -1,19 +1,22 @@ +"""HRNet network modules for base backbone. + +Modified from: +- https://github.com/HRNet/Lite-HRNet +""" + # Copyright (c) 2018-2020 Open-MMLab. # SPDX-License-Identifier: Apache-2.0 # # Copyright (c) 2021 DeLightCMU # SPDX-License-Identifier: Apache-2.0 # -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # -"""Modified from: https://github.com/HRNet/Lite-HRNet""" - import mmcv import torch -import torch.nn as nn import torch.nn.functional as F import torch.utils.checkpoint as cp from mmcv.cnn import ( @@ -28,8 +31,9 @@ from mmseg.models.backbones.resnet import BasicBlock, Bottleneck from mmseg.models.builder import BACKBONES from mmseg.utils import get_root_logger +from torch import nn -from ..utils import ( +from otx.mpa.modules.models.utils import ( AsymmetricPositionAttentionModule, IterativeAggregator, LocalAttentionModule, @@ -37,7 +41,11 @@ ) +# pylint: disable=invalid-name, too-many-lines, too-many-instance-attributes, too-many-locals, too-many-arguments +# pylint: disable=unused-argument, consider-using-enumerate class NeighbourSupport(nn.Module): + """Neighbour support module.""" + def __init__(self, channels, kernel_size=3, key_ratio=8, value_ratio=8, conv_cfg=None, norm_cfg=None): super().__init__() @@ -100,6 +108,7 @@ def __init__(self, channels, kernel_size=3, key_ratio=8, value_ratio=8, conv_cfg ) def forward(self, x): + """Forward.""" h, w = [int(_) for _ in x.size()[-2:]] key = self.key(x).view(-1, 1, self.kernel_size**2, h, w) @@ -115,6 +124,8 @@ def forward(self, x): class CrossResolutionWeighting(nn.Module): + """Cross resolution weighting.""" + def __init__( self, channels, ratio=16, conv_cfg=None, norm_cfg=None, act_cfg=(dict(type="ReLU"), dict(type="Sigmoid")) ): @@ -148,6 +159,7 @@ def __init__( ) def forward(self, x): + """Forward.""" min_size = [int(_) for _ in x[-1].size()[-2:]] out = [F.adaptive_avg_pool2d(s, min_size) for s in x[:-1]] + [x[-1]] @@ -161,6 +173,8 @@ def forward(self, x): class SpatialWeighting(nn.Module): + """Spatial weighting.""" + def __init__(self, channels, ratio=16, conv_cfg=None, act_cfg=(dict(type="ReLU"), dict(type="Sigmoid")), **kwargs): super().__init__() @@ -188,6 +202,7 @@ def __init__(self, channels, ratio=16, conv_cfg=None, act_cfg=(dict(type="ReLU") ) def forward(self, x): + """Forward.""" out = self.global_avgpool(x) out = self.conv1(out) out = self.conv2(out) @@ -196,7 +211,7 @@ def forward(self, x): class SpatialWeightingV2(nn.Module): - """The original repo: https://github.com/DeLightCMU/PSA""" + """The original repo: https://github.com/DeLightCMU/PSA.""" def __init__(self, channels, ratio=16, conv_cfg=None, norm_cfg=None, enable_norm=False, **kwargs): super().__init__() @@ -294,6 +309,7 @@ def _spatial_weighting(self, x): return out def forward(self, x): + """Forward.""" y_channel = self._channel_weighting(x) y_spatial = self._spatial_weighting(x) out = y_channel + y_spatial @@ -302,13 +318,15 @@ def forward(self, x): class ConditionalChannelWeighting(nn.Module): + """Conditional channel weighting module.""" + def __init__( self, in_channels, stride, reduce_ratio, conv_cfg=None, - norm_cfg=dict(type="BN"), + norm_cfg=None, with_cp=False, dropout=None, weighting_module_version="v1", @@ -317,6 +335,9 @@ def __init__( ): super().__init__() + if norm_cfg is None: + norm_cfg = dict(type="BN") + self.with_cp = with_cp self.stride = stride assert stride in [1, 2] @@ -389,6 +410,7 @@ def _inner_forward(self, x): return out def forward(self, x): + """Forward.""" if self.with_cp and x.requires_grad: out = cp.checkpoint(self._inner_forward, x) else: @@ -398,6 +420,8 @@ def forward(self, x): class Stem(nn.Module): + """Stem.""" + def __init__( self, in_channels, @@ -405,7 +429,7 @@ def __init__( out_channels, expand_ratio, conv_cfg=None, - norm_cfg=dict(type="BN"), + norm_cfg=None, with_cp=False, strides=(2, 2), extra_stride=False, @@ -413,6 +437,9 @@ def __init__( ): super().__init__() + if norm_cfg is None: + norm_cfg = dict(type="BN") + assert isinstance(strides, (tuple, list)) assert len(strides) == 2 @@ -535,6 +562,7 @@ def _inner_forward(self, x): return out def forward(self, x): + """Forward.""" if self.with_cp and x.requires_grad: out = cp.checkpoint(self._inner_forward, x) else: @@ -544,6 +572,8 @@ def forward(self, x): class StemV2(nn.Module): + """StemV2.""" + def __init__( self, in_channels, @@ -551,7 +581,7 @@ def __init__( out_channels, expand_ratio, conv_cfg=None, - norm_cfg=dict(type="BN"), + norm_cfg=None, with_cp=False, num_stages=1, strides=(2, 2), @@ -560,6 +590,9 @@ def __init__( ): super().__init__() + if norm_cfg is None: + norm_cfg = dict(type="BN") + assert num_stages > 0 assert isinstance(strides, (tuple, list)) assert len(strides) == 1 + num_stages @@ -689,6 +722,7 @@ def _inner_forward(self, x): return out_list def forward(self, x): + """Forward.""" if self.with_cp and x.requires_grad: out = cp.checkpoint(self._inner_forward, x) else: @@ -720,11 +754,17 @@ def __init__( out_channels, stride=1, conv_cfg=None, - norm_cfg=dict(type="BN"), - act_cfg=dict(type="ReLU"), + norm_cfg=None, + act_cfg=None, with_cp=False, ): super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") + if act_cfg is None: + act_cfg = dict(type="ReLU") + self.stride = stride self.with_cp = with_cp @@ -812,6 +852,7 @@ def _inner_forward(self, x): return out def forward(self, x): + """Forward.""" if self.with_cp and x.requires_grad: out = cp.checkpoint(self._inner_forward, x) else: @@ -821,6 +862,8 @@ def forward(self, x): class LiteHRModule(nn.Module): + """LiteHR module.""" + def __init__( self, num_branches, @@ -831,13 +874,16 @@ def __init__( multiscale_output=False, with_fuse=True, conv_cfg=None, - norm_cfg=dict(type="BN"), + norm_cfg=None, with_cp=False, dropout=None, weighting_module_version="v1", neighbour_weighting=False, ): super().__init__() + + if norm_cfg is None: + norm_cfg = dict(type="BN") self._check_branches(num_branches, in_channels) self.in_channels = in_channels @@ -871,7 +917,7 @@ def _check_branches(num_branches, in_channels): def _make_weighting_blocks(self, num_blocks, reduce_ratio, stride=1, dropout=None): layers = [] - for i in range(num_blocks): + for _ in range(num_blocks): layers.append( ConditionalChannelWeighting( self.in_channels, @@ -902,7 +948,7 @@ def _make_one_branch(self, branch_index, num_blocks, stride=1): with_cp=self.with_cp, ) ] - for i in range(1, num_blocks): + for _ in range(1, num_blocks): layers.append( ShuffleUnit( self.in_channels[branch_index], @@ -1081,7 +1127,7 @@ def __init__( extra, in_channels=3, conv_cfg=None, - norm_cfg=dict(type="BN"), + norm_cfg=None, norm_eval=False, with_cp=False, zero_init_residual=False, @@ -1090,6 +1136,9 @@ def __init__( ): super().__init__(init_cfg=init_cfg) + if norm_cfg is None: + norm_cfg = dict(type="BN") + self.extra = extra self.conv_cfg = conv_cfg self.norm_cfg = norm_cfg @@ -1123,12 +1172,12 @@ def __init__( num_channels = self.stages_spec["num_channels"][i] num_channels = [num_channels[i] for i in range(len(num_channels))] - setattr(self, "transition{}".format(i), self._make_transition_layer(num_channels_last, num_channels)) + setattr(self, f"transition{i}", self._make_transition_layer(num_channels_last, num_channels)) stage, num_channels_last = self._make_stage( self.stages_spec, i, num_channels, multiscale_output=True, dropout=dropout ) - setattr(self, "stage{}".format(i), stage) + setattr(self, f"stage{i}", stage) self.out_modules = None if self.extra.get("out_modules") is not None: @@ -1356,7 +1405,7 @@ def forward(self, x): y_list = [y] for i in range(self.num_stages): - transition_modules = getattr(self, "transition{}".format(i)) + transition_modules = getattr(self, f"transition{i}") stage_inputs = [] for j in range(self.stages_spec["num_branches"][i]): @@ -1368,7 +1417,7 @@ def forward(self, x): else: stage_inputs.append(y_list[j]) - stage_module = getattr(self, "stage{}".format(i)) + stage_module = getattr(self, f"stage{i}") y_list = stage_module(stage_inputs) if self.out_modules is not None: diff --git a/otx/mpa/modules/ov/models/mmseg/backbones/mmov_backbone.py b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py similarity index 64% rename from otx/mpa/modules/ov/models/mmseg/backbones/mmov_backbone.py rename to otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py index 31e4aaaf218..0b19fc7eb12 100644 --- a/otx/mpa/modules/ov/models/mmseg/backbones/mmov_backbone.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/backbones/mmov_backbone.py @@ -1,18 +1,25 @@ -# Copyright (C) 2022 Intel Corporation +"""Backbone used for openvino export.""" + +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # from mmseg.models.builder import BACKBONES -from ...mmov_model import MMOVModel +from otx.mpa.modules.ov.models.mmov_model import MMOVModel + +# pylint: disable=unused-argument @BACKBONES.register_module() class MMOVBackbone(MMOVModel): + """MMOVBackbone.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def forward(self, *args, **kwargs): + """Forward.""" outputs = super().forward(*args, **kwargs) if not isinstance(outputs, tuple): outputs = (outputs,) @@ -20,5 +27,6 @@ def forward(self, *args, **kwargs): return outputs def init_weights(self, pretrained=None): + """Initialize the weights.""" # TODO - pass + return diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py new file mode 100644 index 00000000000..8ea771a0803 --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py @@ -0,0 +1,21 @@ +"""Semantic segmentation heads.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + + +from .custom_fcn_head import CustomFCNHead +from .mmov_decode_head import MMOVDecodeHead + +__all__ = ["MMOVDecodeHead", "CustomFCNHead"] diff --git a/otx/mpa/modules/models/heads/custom_fcn_head.py b/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_fcn_head.py similarity index 62% rename from otx/mpa/modules/models/heads/custom_fcn_head.py rename to otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_fcn_head.py index 00efd56cec0..20b3fb2039b 100644 --- a/otx/mpa/modules/models/heads/custom_fcn_head.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/heads/custom_fcn_head.py @@ -1,18 +1,24 @@ -# Copyright (C) 2022 Intel Corporation +"""Custom FCN head.""" + +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # from mmseg.models.builder import HEADS from mmseg.models.decode_heads.fcn_head import FCNHead -from .aggregator_mixin import AggregatorMixin -from .mix_loss_mixin import MixLossMixin -from .pixel_weights_mixin import PixelWeightsMixin2 -from .segment_out_norm_mixin import SegmentOutNormMixin +from .mixin import ( + AggregatorMixin, + MixLossMixin, + PixelWeightsMixin2, + SegmentOutNormMixin, +) @HEADS.register_module() -class CustomFCNHead(SegmentOutNormMixin, AggregatorMixin, MixLossMixin, PixelWeightsMixin2, FCNHead): +class CustomFCNHead( + SegmentOutNormMixin, AggregatorMixin, MixLossMixin, PixelWeightsMixin2, FCNHead +): # pylint: disable=too-many-ancestors """Custom Fully Convolution Networks for Semantic Segmentation.""" def __init__(self, *args, **kwargs): diff --git a/otx/mpa/modules/models/heads/pixel_weights_mixin.py b/otx/algorithms/segmentation/adapters/mmseg/models/heads/mixin.py similarity index 59% rename from otx/mpa/modules/models/heads/pixel_weights_mixin.py rename to otx/algorithms/segmentation/adapters/mmseg/models/heads/mixin.py index 6ca8821b3ac..ecb47213a8d 100644 --- a/otx/mpa/modules/models/heads/pixel_weights_mixin.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/heads/mixin.py @@ -1,26 +1,144 @@ -# Copyright (C) 2022 Intel Corporation +"""Modules for aggregator and loss mix.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # - -import torch.nn as nn +import torch +import torch.nn.functional as F from mmcv.runner import force_fp32 from mmseg.core import add_prefix from mmseg.models.losses import accuracy from mmseg.ops import resize +from torch import nn -from otx.mpa.modules.utils.seg_utils import get_valid_label_mask_per_batch +from otx.algorithms.segmentation.adapters.mmseg.utils import ( + get_valid_label_mask_per_batch, +) +from otx.mpa.modules.models.losses.utils import LossEqualizer +from otx.mpa.modules.models.utils import AngularPWConv, IterativeAggregator, normalize -from ..losses.utils import LossEqualizer +# pylint: disable=abstract-method, unused-argument, keyword-arg-before-vararg -class PixelWeightsMixin(nn.Module): +class SegmentOutNormMixin(nn.Module): + """SegmentOutNormMixin.""" + + def __init__(self, *args, enable_out_seg=True, enable_out_norm=False, **kwargs): + super().__init__(*args, **kwargs) + + self.enable_out_seg = enable_out_seg + self.enable_out_norm = enable_out_norm + + if enable_out_seg: + if enable_out_norm: + self.conv_seg = AngularPWConv(self.channels, self.out_channels, clip_output=True) + else: + self.conv_seg = None + + def cls_seg(self, feat): + """Classify each pixel.""" + if self.dropout is not None: + feat = self.dropout(feat) + if self.enable_out_norm: + feat = normalize(feat, dim=1, p=2) + if self.conv_seg is not None: + return self.conv_seg(feat) + return feat + + +class AggregatorMixin(nn.Module): + """A class for creating an aggregator.""" + def __init__( self, - enable_loss_equalizer=False, - loss_target="gt_semantic_seg", *args, + enable_aggregator=False, + aggregator_min_channels=None, + aggregator_merge_norm=None, + aggregator_use_concat=False, **kwargs, ): + + in_channels = kwargs.get("in_channels") + in_index = kwargs.get("in_index") + norm_cfg = kwargs.get("norm_cfg") + conv_cfg = kwargs.get("conv_cfg") + input_transform = kwargs.get("input_transform") + + aggregator = None + if enable_aggregator: + assert isinstance(in_channels, (tuple, list)) + assert len(in_channels) > 1 + + aggregator = IterativeAggregator( + in_channels=in_channels, + min_channels=aggregator_min_channels, + conv_cfg=conv_cfg, + norm_cfg=norm_cfg, + merge_norm=aggregator_merge_norm, + use_concat=aggregator_use_concat, + ) + + aggregator_min_channels = aggregator_min_channels if aggregator_min_channels is not None else 0 + # change arguments temporarily + kwargs["in_channels"] = max(in_channels[0], aggregator_min_channels) + kwargs["input_transform"] = None + if in_index is not None: + kwargs["in_index"] = in_index[0] + + super().__init__(*args, **kwargs) + + self.aggregator = aggregator + # re-define variables + self.in_channels = in_channels + self.input_transform = input_transform + self.in_index = in_index + + def _transform_inputs(self, inputs): + inputs = super()._transform_inputs(inputs) + if self.aggregator is not None: + inputs = self.aggregator(inputs)[0] + return inputs + + +class MixLossMixin(nn.Module): + """Loss mixing module.""" + + @staticmethod + def _mix_loss(logits, target, ignore_index=255): + num_samples = logits.size(0) + assert num_samples % 2 == 0 + + with torch.no_grad(): + probs = F.softmax(logits, dim=1) + probs_a, probs_b = torch.split(probs, num_samples // 2) + mean_probs = 0.5 * (probs_a + probs_b) + trg_probs = torch.cat([mean_probs, mean_probs], dim=0) + + log_probs = torch.log_softmax(logits, dim=1) + losses = torch.sum(trg_probs * log_probs, dim=1).neg() + + valid_mask = target != ignore_index + valid_losses = torch.where(valid_mask, losses, torch.zeros_like(losses)) + + return valid_losses.mean() + + @force_fp32(apply_to=("seg_logit",)) + def losses(self, seg_logit, seg_label, train_cfg, *args, **kwargs): + """Loss computing.""" + loss = super().losses(seg_logit, seg_label, train_cfg, *args, **kwargs) + if train_cfg.get("mix_loss", None) and train_cfg.mix_loss.get("enable", False): + mix_loss = self._mix_loss(seg_logit, seg_label, ignore_index=self.ignore_index) + + mix_loss_weight = train_cfg.mix_loss.get("weight", 1.0) + loss["loss_mix"] = mix_loss_weight * mix_loss + + return loss + + +class PixelWeightsMixin(nn.Module): + """PixelWeightsMixin.""" + + def __init__(self, enable_loss_equalizer=False, loss_target="gt_semantic_seg", *args, **kwargs): super().__init__(*args, **kwargs) self.enable_loss_equalizer = enable_loss_equalizer @@ -34,10 +152,12 @@ def __init__( @property def loss_target_name(self): + """Return loss target name.""" return self.loss_target @property def last_scale(self): + """Return the last scale.""" if not isinstance(self.loss_decode, nn.ModuleList): losses_decode = [self.loss_decode] else: @@ -54,6 +174,7 @@ def last_scale(self): return loss_module.last_scale def set_step_params(self, init_iter, epoch_size): + """Set step parameters.""" if not isinstance(self.loss_decode, nn.ModuleList): losses_decode = [self.loss_decode] else: @@ -73,6 +194,7 @@ def forward_train( return_logits=False, ): """Forward function for training. + Args: inputs (list[Tensor]): List of multi-level img features. img_metas (list[dict]): List of image info dict where each dict @@ -138,6 +260,8 @@ def losses(self, seg_logit, seg_label, train_cfg, pixel_weights=None): class PixelWeightsMixin2(PixelWeightsMixin): + """Pixel weight mixin class.""" + def forward_train( self, inputs, @@ -148,6 +272,7 @@ def forward_train( return_logits=False, ): """Forward function for training. + Args: inputs (list[Tensor]): List of multi-level img features. img_metas (list[dict]): List of image info dict where each dict @@ -176,7 +301,9 @@ def forward_train( return losses @force_fp32(apply_to=("seg_logit",)) - def losses(self, seg_logit, seg_label, train_cfg, valid_label_mask, pixel_weights=None): + def losses( + self, seg_logit, seg_label, train_cfg, valid_label_mask, pixel_weights=None + ): # pylint: disable=arguments-renamed """Compute segmentation loss.""" loss = dict() diff --git a/otx/mpa/modules/ov/models/mmseg/decode_heads/mmov_decode_head.py b/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py similarity index 78% rename from otx/mpa/modules/ov/models/mmseg/decode_heads/mmov_decode_head.py rename to otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py index 375fb51b17b..75fc3083919 100644 --- a/otx/mpa/modules/ov/models/mmseg/decode_heads/mmov_decode_head.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/heads/mmov_decode_head.py @@ -1,4 +1,6 @@ -# Copyright (C) 2022 Intel Corporation +"""Decode-head used for openvino export.""" + +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -6,25 +8,31 @@ from typing import Dict, List, Optional, Union import openvino.runtime as ov -from mmseg.models.builder import HEADS from mmseg.models.decode_heads.decode_head import BaseDecodeHead -from ...mmov_model import MMOVModel +from otx.mpa.modules.ov.models.mmov_model import MMOVModel + +# pylint: disable=too-many-instance-attributes, keyword-arg-before-vararg -@HEADS.register_module() class MMOVDecodeHead(BaseDecodeHead): + """MMOVDecodeHead.""" + def __init__( self, model_path_or_model: Union[str, ov.Model] = None, weight_path: Optional[str] = None, - inputs: Dict[str, Union[str, List[str]]] = {}, - outputs: Dict[str, Union[str, List[str]]] = {}, + inputs: Optional[Dict[str, Union[str, List[str]]]] = None, + outputs: Optional[Dict[str, Union[str, List[str]]]] = None, init_weight: bool = False, verify_shape: bool = True, *args, - **kwargs, + **kwargs ): + if inputs is None: + inputs = {} + if outputs is None: + outputs = {} self._model_path_or_model = model_path_or_model self._weight_path = weight_path self._inputs = deepcopy(inputs) @@ -68,10 +76,12 @@ def __init__( ) def init_weights(self): + """Init weights.""" # TODO - pass + return def forward(self, inputs): + """Forward.""" outputs = self._transform_inputs(inputs) if getattr(self, "extractor"): outputs = self.extractor(outputs) diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py index 7455c2529c3..f6ba9b2e32b 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/__init__.py @@ -1,6 +1,6 @@ """Segmentation losses.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions # and limitations under the License. +from .cross_entropy_loss_with_ignore import CrossEntropyLossWithIgnore from .detcon_loss import DetConLoss -__all__ = ["DetConLoss"] +__all__ = ["DetConLoss", "CrossEntropyLossWithIgnore"] diff --git a/otx/mpa/modules/models/losses/base_pixel_loss.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/base_pixel_loss.py similarity index 89% rename from otx/mpa/modules/models/losses/base_pixel_loss.py rename to otx/algorithms/segmentation/adapters/mmseg/models/losses/base_pixel_loss.py index 423cf744fea..1d0d69e27c8 100644 --- a/otx/mpa/modules/models/losses/base_pixel_loss.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/base_pixel_loss.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""Base pixel loss.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -8,18 +9,23 @@ import torch.nn.functional as F from mmseg.models.losses.utils import weight_reduce_loss -from otx.mpa.modules.models.builder import build_scalar_scheduler +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import ( + build_scalar_scheduler, +) from .base_weighted_loss import BaseWeightedLoss def entropy(p, dim=1, keepdim=False): + """Calculates the entropy.""" return -torch.where(p > 0.0, p * p.log(), torch.zeros_like(p)).sum(dim=dim, keepdim=keepdim) class BasePixelLoss(BaseWeightedLoss): + """Base pixel loss.""" + def __init__(self, scale_cfg=None, pr_product=False, conf_penalty_weight=None, border_reweighting=False, **kwargs): - super(BasePixelLoss, self).__init__(**kwargs) + super().__init__(**kwargs) self._enable_pr_product = pr_product self._border_reweighting = border_reweighting @@ -32,22 +38,27 @@ def __init__(self, scale_cfg=None, pr_product=False, conf_penalty_weight=None, b @property def last_scale(self): + """Return last_scale.""" return self._last_scale @property def last_reg_weight(self): + """Return last_reg_weight.""" return self._last_reg_weight @property def with_regularization(self): + """Check regularization use.""" return self._reg_weight_scheduler is not None @property def with_pr_product(self): + """Check pr_product.""" return self._enable_pr_product @property def with_border_reweighting(self): + """Check border reweighting.""" return self._border_reweighting @staticmethod @@ -99,7 +110,9 @@ def _pred_stat(output, labels, valid_mask, window_size=5, min_group_ratio=0.6): return out_ratio.item() - def _forward(self, output, labels, avg_factor=None, pixel_weights=None, reduction_override=None): + def _forward( + self, output, labels, avg_factor=None, pixel_weights=None, reduction_override=None + ): # pylint: disable=too-many-locals assert reduction_override in (None, "none", "mean", "sum") reduction = reduction_override if reduction_override else self.reduction diff --git a/otx/mpa/modules/models/losses/base_weighted_loss.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/base_weighted_loss.py similarity index 83% rename from otx/mpa/modules/models/losses/base_weighted_loss.py rename to otx/algorithms/segmentation/adapters/mmseg/models/losses/base_weighted_loss.py index e74b5233e97..2487eed40fe 100644 --- a/otx/mpa/modules/models/losses/base_weighted_loss.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/base_weighted_loss.py @@ -1,17 +1,21 @@ -# Copyright (C) 2022 Intel Corporation +"""Base weighted loss function for semantic segmentation.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # from abc import ABCMeta, abstractmethod import torch -import torch.nn as nn from mmseg.core import build_pixel_sampler -from scipy.special import erfinv +from scipy.special import erfinv # pylint: disable=no-name-in-module +from torch import nn -from otx.mpa.modules.models.builder import build_scalar_scheduler +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import ( + build_scalar_scheduler, +) +# pylint: disable=too-many-instance-attributes, unused-argument class BaseWeightedLoss(nn.Module, metaclass=ABCMeta): """Base class for loss. @@ -57,6 +61,7 @@ def __init__( self._epoch_size = 1 def set_step_params(self, init_iter, epoch_size): + """Set step parameters.""" assert init_iter >= 0 assert epoch_size > 0 @@ -65,18 +70,22 @@ def set_step_params(self, init_iter, epoch_size): @property def with_loss_jitter(self): + """Check loss jitter.""" return self._jitter_sigma_factor is not None @property def iter(self): + """Return iteration.""" return self._iter @property def epoch_size(self): + """Return epoch size.""" return self._epoch_size @property def last_loss_weight(self): + """Return last loss weight.""" return self._last_loss_weight @abstractmethod @@ -99,8 +108,8 @@ def forward(self, *args, **kwargs): loss, meta = self._forward(*args, **kwargs) # make sure meta data are tensor as well for aggregation # when parsing loss in sgementator - for k, v in meta.items(): - meta[k] = torch.tensor(v, dtype=loss.dtype, device=loss.device) + for key, val in meta.items(): + meta[key] = torch.tensor(val, dtype=loss.dtype, device=loss.device) if self.with_loss_jitter and loss.numel() == 1: if self._smooth_loss is None: diff --git a/otx/mpa/modules/models/losses/cross_entropy_loss_with_ignore.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py similarity index 88% rename from otx/mpa/modules/models/losses/cross_entropy_loss_with_ignore.py rename to otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py index cdabc189798..57e9c24c268 100644 --- a/otx/mpa/modules/models/losses/cross_entropy_loss_with_ignore.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/cross_entropy_loss_with_ignore.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""Cross entropy loss for ignored mode in class-incremental learning.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -7,11 +8,11 @@ from mmseg.models.builder import LOSSES from mmseg.models.losses.utils import get_class_weight -from .mpa_pixel_base import MPABasePixelLoss +from .otx_pixel_base import OTXBasePixelLoss @LOSSES.register_module() -class CrossEntropyLossWithIgnore(MPABasePixelLoss): +class CrossEntropyLossWithIgnore(OTXBasePixelLoss): """CrossEntropyLossWithIgnore with Ignore Mode Support for Class Incremental Learning. Args: @@ -24,13 +25,14 @@ class CrossEntropyLossWithIgnore(MPABasePixelLoss): """ def __init__(self, reduction="mean", loss_weight=None, **kwargs): - super(CrossEntropyLossWithIgnore, self).__init__(**kwargs) + super().__init__(**kwargs) self.reduction = reduction self.class_weight = get_class_weight(loss_weight) @property def name(self): + """name.""" return "ce_with_ignore" def _calculate(self, cls_score, label, valid_label_mask, scale): diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py index 8d93688cbc1..19140effac1 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/detcon_loss.py @@ -1,6 +1,6 @@ """DetCon loss.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=no-name-in-module, not-callable diff --git a/otx/mpa/modules/models/losses/mpa_pixel_base.py b/otx/algorithms/segmentation/adapters/mmseg/models/losses/otx_pixel_base.py similarity index 88% rename from otx/mpa/modules/models/losses/mpa_pixel_base.py rename to otx/algorithms/segmentation/adapters/mmseg/models/losses/otx_pixel_base.py index e7a7bacdd96..b9f68a5ffc4 100644 --- a/otx/mpa/modules/models/losses/mpa_pixel_base.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/losses/otx_pixel_base.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""OTX pixel loss.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -7,10 +8,14 @@ from .base_pixel_loss import BasePixelLoss +# pylint: disable=too-many-function-args, too-many-locals + + +class OTXBasePixelLoss(BasePixelLoss): # pylint: disable=abstract-method + """OTXBasePixelLoss.""" -class MPABasePixelLoss(BasePixelLoss): def __init__(self, **kwargs): - super(MPABasePixelLoss, self).__init__(**kwargs) + super().__init__(**kwargs) def _forward( self, @@ -20,7 +25,7 @@ def _forward( avg_factor=None, pixel_weights=None, reduction_override=None, - ): + ): # pylint: disable=arguments-renamed assert reduction_override in (None, "none", "mean", "sum") reduction = reduction_override if reduction_override else self.reduction diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py index 841d7bf50d4..cf76dc5c172 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/necks/__init__.py @@ -1,6 +1,6 @@ """OTX Algorithms - Segmentation Necks.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py b/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py index 563c89b4df4..e7656efd150 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/necks/selfsl_mlp.py @@ -3,7 +3,7 @@ This MLP consists of fc (conv) - norm - relu - fc (conv). """ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=dangerous-default-value diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py new file mode 100644 index 00000000000..47c10da0113 --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/__init__.py @@ -0,0 +1,25 @@ +"""Scaler schedulers for semantic segmentation.""" + +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .constant import ConstantScalarScheduler +from .poly import PolyScalarScheduler +from .step import StepScalarScheduler + +__all__ = [ + "ConstantScalarScheduler", + "PolyScalarScheduler", + "StepScalarScheduler", +] diff --git a/otx/mpa/modules/models/scalar_schedulers/base.py b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py similarity index 70% rename from otx/mpa/modules/models/scalar_schedulers/base.py rename to otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py index e3000f0a21b..600309d8f1d 100644 --- a/otx/mpa/modules/models/scalar_schedulers/base.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/base.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""Base scalar scheduler.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -6,10 +7,10 @@ class BaseScalarScheduler(metaclass=ABCMeta): - def __init__(self): - super(BaseScalarScheduler, self).__init__() + """Base scalar scheduler.""" def __call__(self, step, epoch_size) -> float: + """Callback function of BaseScalarScheduler.""" return self._get_value(step, epoch_size) @abstractmethod diff --git a/otx/mpa/modules/models/scalar_schedulers/constant.py b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py similarity index 73% rename from otx/mpa/modules/models/scalar_schedulers/constant.py rename to otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py index 96536dd8994..f7819d18ce5 100644 --- a/otx/mpa/modules/models/scalar_schedulers/constant.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/constant.py @@ -1,8 +1,10 @@ -# Copyright (C) 2022 Intel Corporation +"""Constant scheduler.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # -from ..builder import SCALAR_SCHEDULERS +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + from .base import BaseScalarScheduler @@ -11,12 +13,13 @@ class ConstantScalarScheduler(BaseScalarScheduler): """The learning rate remains constant over time. The learning rate equals the scale. + Args: scale (float): The learning rate scale. """ def __init__(self, scale: float = 30.0): - super(ConstantScalarScheduler, self).__init__() + super().__init__() self._end_s = scale assert self._end_s > 0.0 diff --git a/otx/mpa/modules/models/scalar_schedulers/poly.py b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py similarity index 90% rename from otx/mpa/modules/models/scalar_schedulers/poly.py rename to otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py index 6b70fe9423b..f173b62f374 100644 --- a/otx/mpa/modules/models/scalar_schedulers/poly.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/poly.py @@ -1,10 +1,13 @@ -# Copyright (C) 2022 Intel Corporation +"""Polynomial scheduler.""" + +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # import numpy as np -from ..builder import SCALAR_SCHEDULERS +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + from .base import BaseScalarScheduler @@ -23,7 +26,7 @@ class PolyScalarScheduler(BaseScalarScheduler): def __init__( self, start_scale: float, end_scale: float, num_iters: int, power: float = 1.2, by_epoch: bool = False ): - super(PolyScalarScheduler, self).__init__() + super().__init__() self._start_s = start_scale assert self._start_s >= 0.0 diff --git a/otx/mpa/modules/models/scalar_schedulers/step.py b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py similarity index 89% rename from otx/mpa/modules/models/scalar_schedulers/step.py rename to otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py index 3646f148960..19c4f81563d 100644 --- a/otx/mpa/modules/models/scalar_schedulers/step.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/schedulers/step.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""Step scheduler.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -6,7 +7,8 @@ import numpy as np -from ..builder import SCALAR_SCHEDULERS +from otx.algorithms.segmentation.adapters.mmseg.utils.builder import SCALAR_SCHEDULERS + from .base import BaseScalarScheduler @@ -26,7 +28,7 @@ class StepScalarScheduler(BaseScalarScheduler): """ def __init__(self, scales: List[float], num_iters: List[int], by_epoch: bool = False): - super(StepScalarScheduler, self).__init__() + super().__init__() self.by_epoch = by_epoch diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py index cf76332e0d2..d953b628f81 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/__init__.py @@ -1,6 +1,6 @@ """OTX Algorithms - Segmentation Segmentors.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions # and limitations under the License. +from .class_incr_encoder_decoder import ClassIncrEncoderDecoder from .detcon import DetConB, SupConDetConB +from .mean_teacher_segmentor import MeanTeacherSegmentor -__all__ = ["DetConB", "SupConDetConB"] +__all__ = ["DetConB", "SupConDetConB", "ClassIncrEncoderDecoder", "MeanTeacherSegmentor"] diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/class_incr_encoder_decoder.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/class_incr_encoder_decoder.py new file mode 100644 index 00000000000..f18de3a5193 --- /dev/null +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/class_incr_encoder_decoder.py @@ -0,0 +1,109 @@ +"""Encoder-decoder for incremental learning.""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import functools + +import torch +from mmseg.models import SEGMENTORS +from mmseg.utils import get_root_logger + +from otx.mpa.modules.utils.task_adapt import map_class_names + +from .mixin import PixelWeightsMixin +from .otx_encoder_decoder import OTXEncoderDecoder + + +@SEGMENTORS.register_module() +class ClassIncrEncoderDecoder(PixelWeightsMixin, OTXEncoderDecoder): + """Encoder-decoder for incremental learning.""" + + def __init__(self, *args, task_adapt=None, **kwargs): + super().__init__(*args, **kwargs) + + # Hook for class-sensitive weight loading + assert task_adapt is not None, "When using task_adapt, task_adapt must be set." + + self._register_load_state_dict_pre_hook( + functools.partial( + self.load_state_dict_pre_hook, + self, # model + task_adapt["dst_classes"], # model_classes + task_adapt["src_classes"], # chkpt_classes + ) + ) + + def forward_train( + self, + img, + img_metas, + gt_semantic_seg, + aux_img=None, + **kwargs, + ): # pylint: disable=arguments-renamed + """Forward function for training. + + Args: + img (Tensor): Input images. + img_metas (list[dict]): List of image info dict where each dict + has: 'img_shape', 'scale_factor', 'flip', and may also contain + 'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'. + For details on the values of these keys see + `mmseg/datasets/pipelines/formatting.py:Collect`. + gt_semantic_seg (Tensor): Semantic segmentation masks + used if the architecture supports semantic segmentation task. + aux_img (Tensor): Auxiliary images. + + Returns: + dict[str, Tensor]: a dictionary of loss components + """ + if aux_img is not None: + mix_loss_enabled = False + mix_loss_cfg = self.train_cfg.get("mix_loss", None) + if mix_loss_cfg is not None: + mix_loss_enabled = mix_loss_cfg.get("enable", False) + if mix_loss_enabled: + self.train_cfg.mix_loss.enable = mix_loss_enabled + + if self.train_cfg.mix_loss.enable: + img = torch.cat([img, aux_img], dim=0) + gt_semantic_seg = torch.cat([gt_semantic_seg, gt_semantic_seg], dim=0) + + return super().forward_train(img, img_metas, gt_semantic_seg, **kwargs) + + @staticmethod + def load_state_dict_pre_hook( + model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs + ): # pylint: disable=too-many-locals, unused-argument + """Modify input state_dict according to class name matching before weight loading.""" + logger = get_root_logger("INFO") + logger.info(f"----------------- ClassIncrEncoderDecoder.load_state_dict_pre_hook() called w/ prefix: {prefix}") + + # Dst to src mapping index + model_classes = list(model_classes) + chkpt_classes = list(chkpt_classes) + model2chkpt = map_class_names(model_classes, chkpt_classes) + logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") + + model_dict = model.state_dict() + param_names = [ + "decode_head.conv_seg.weight", + "decode_head.conv_seg.bias", + ] + for model_name in param_names: + chkpt_name = prefix + model_name + if model_name not in model_dict or chkpt_name not in chkpt_dict: + logger.info(f"Skipping weight copy: {chkpt_name}") + continue + + # Mix weights + model_param = model_dict[model_name].clone() + chkpt_param = chkpt_dict[chkpt_name] + for model_key, c in enumerate(model2chkpt): + if c >= 0: + model_param[model_key].copy_(chkpt_param[c]) + + # Replace checkpoint weight by mixed weights + chkpt_dict[chkpt_name] = model_param diff --git a/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py index 399b2384290..af4ca52d56d 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/detcon.py @@ -4,7 +4,7 @@ - 'Efficient Visual Pretraining with Contrastive Detection', https://arxiv.org/abs/2103.10957 """ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=unused-argument, invalid-name, unnecessary-pass, not-callable @@ -24,11 +24,10 @@ from mmseg.ops import resize from torch import nn -from otx.mpa.modules.models.segmentors.class_incr_encoder_decoder import ( - ClassIncrEncoderDecoder, -) from otx.mpa.utils.logger import get_logger +from .class_incr_encoder_decoder import ClassIncrEncoderDecoder + logger = get_logger() @@ -517,10 +516,10 @@ def __init__( # pylint: disable=arguments-renamed def forward_train( self, - img: torch.Tensor, - img_metas: List[Dict], - gt_semantic_seg: torch.Tensor, - pixel_weights: Optional[torch.Tensor] = None, + img, + img_metas, + gt_semantic_seg, + pixel_weights=None, **kwargs, ): """Forward function for training. diff --git a/otx/mpa/modules/models/segmentors/mean_teacher_segmentor.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py similarity index 66% rename from otx/mpa/modules/models/segmentors/mean_teacher_segmentor.py rename to otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py index cf3eb65c8d3..d12418a9670 100644 --- a/otx/mpa/modules/models/segmentors/mean_teacher_segmentor.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mean_teacher_segmentor.py @@ -1,5 +1,6 @@ +"""Mean teacher segmentor for semi-supervised learning.""" + import functools -from collections import OrderedDict import torch from mmseg.models import SEGMENTORS, build_segmentor @@ -10,11 +11,18 @@ logger = get_logger() +# pylint: disable=too-many-locals, protected-access + @SEGMENTORS.register_module() class MeanTeacherSegmentor(BaseSegmentor): + """Mean teacher segmentor for semi-supervised learning. + + It creates two models and ema from one to the other for consistency loss. + """ + def __init__(self, orig_type=None, unsup_weight=0.1, semisl_start_iter=30, **kwargs): - super(MeanTeacherSegmentor, self).__init__() + super().__init__() self.test_cfg = kwargs["test_cfg"] self.semisl_start_iter = semisl_start_iter self.count_iter = 0 @@ -31,21 +39,27 @@ def __init__(self, orig_type=None, unsup_weight=0.1, semisl_start_iter=30, **kwa self._register_load_state_dict_pre_hook(functools.partial(self.load_state_dict_pre_hook, self)) def encode_decode(self, img, img_metas): + """Encode and decode images.""" return self.model_s.encode_decode(img, img_metas) def extract_feat(self, imgs): + """Extract feature.""" return self.model_s.extract_feat(imgs) - def simple_test(self, img, img_metas, **kwargs): - return self.model_s.simple_test(img, img_metas, **kwargs) + def simple_test(self, img, img_meta, **kwargs): + """Simple test.""" + return self.model_s.simple_test(img, img_meta, **kwargs) def aug_test(self, imgs, img_metas, **kwargs): + """Aug test.""" return self.model_s.aug_test(imgs, img_metas, **kwargs) def forward_dummy(self, img, **kwargs): + """Forward dummy.""" return self.model_s.forward_dummy(img, **kwargs) def forward_train(self, img, img_metas, gt_semantic_seg, **kwargs): + """Forward train.""" self.count_iter += 1 if self.semisl_start_iter > self.count_iter or "extra_0" not in kwargs: x = self.model_s.extract_feat(img) @@ -63,7 +77,7 @@ def forward_train(self, img, img_metas, gt_semantic_seg, **kwargs): teacher_logit = resize( input=teacher_logit, size=ul_w_img.shape[2:], mode="bilinear", align_corners=self.align_corners ) - conf_from_teacher, pl_from_teacher = torch.max(torch.softmax(teacher_logit, axis=1), axis=1, keepdim=True) + _, pl_from_teacher = torch.max(torch.softmax(teacher_logit, axis=1), axis=1, keepdim=True) losses = dict() @@ -72,34 +86,34 @@ def forward_train(self, img, img_metas, gt_semantic_seg, **kwargs): loss_decode = self.model_s._decode_head_forward_train(x, img_metas, gt_semantic_seg=gt_semantic_seg) loss_decode_u = self.model_s._decode_head_forward_train(x_u, ul_img_metas, gt_semantic_seg=pl_from_teacher) - for (k, v) in loss_decode_u.items(): - if v is None: + for (key, value) in loss_decode_u.items(): + if value is None: continue - losses[k] = loss_decode[k] + loss_decode_u[k] * self.unsup_weight + losses[key] = loss_decode[key] + loss_decode_u[key] * self.unsup_weight return losses @staticmethod - def state_dict_hook(module, state_dict, prefix, *args, **kwargs): - """Redirect student model as output state_dict (teacher as auxilliary)""" + def state_dict_hook(module, state_dict, prefix, *args, **kwargs): # pylint: disable=unused-argument + """Redirect student model as output state_dict (teacher as auxilliary).""" logger.info("----------------- MeanTeacherSegmentor.state_dict_hook() called") - for k in list(state_dict.keys()): - v = state_dict.pop(k) - if not prefix or k.startswith(prefix): - k = k.replace(prefix, "", 1) - if k.startswith("model_s."): - k = k.replace("model_s.", "", 1) - elif k.startswith("model_t."): + for key in list(state_dict.keys()): + value = state_dict.pop(key) + if not prefix or key.startswith(prefix): + key = key.replace(prefix, "", 1) + if key.startswith("model_s."): + key = key.replace("model_s.", "", 1) + elif key.startswith("model_t."): continue - k = prefix + k - state_dict[k] = v + key = prefix + key + state_dict[key] = value return state_dict @staticmethod - def load_state_dict_pre_hook(module, state_dict, *args, **kwargs): - """Redirect input state_dict to teacher model""" + def load_state_dict_pre_hook(module, state_dict, *args, **kwargs): # pylint: disable=unused-argument + """Redirect input state_dict to teacher model.""" logger.info("----------------- MeanTeacherSegmentor.load_state_dict_pre_hook() called") - for k in list(state_dict.keys()): - v = state_dict.pop(k) - state_dict["model_s." + k] = v - state_dict["model_t." + k] = v + for key in list(state_dict.keys()): + value = state_dict.pop(key) + state_dict["model_s." + key] = value + state_dict["model_t." + key] = value diff --git a/otx/mpa/modules/models/segmentors/pixel_weights_mixin.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mixin.py similarity index 94% rename from otx/mpa/modules/models/segmentors/pixel_weights_mixin.py rename to otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mixin.py index b5ed556856f..cd075524755 100644 --- a/otx/mpa/modules/models/segmentors/pixel_weights_mixin.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/mixin.py @@ -1,16 +1,20 @@ -# Copyright (C) 2022 Intel Corporation +"""Modules for decode and loss reweighting/mix.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # - -import torch.nn as nn from mmseg.core import add_prefix from mmseg.models.builder import build_loss from mmseg.ops import resize +from torch import nn + +from otx.mpa.modules.models.losses.utils import LossEqualizer -from ..losses.utils import LossEqualizer +# pylint: disable=too-many-locals -class PixelWeightsMixin(object): +class PixelWeightsMixin: + """PixelWeightsMixin.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._init_train_components(self.train_cfg) @@ -45,6 +49,7 @@ def _get_argument_by_name(trg_name, arguments): return arguments[trg_name] def set_step_params(self, init_iter, epoch_size): + """Sets the step params for the current object's decode head.""" self.decode_head.set_step_params(init_iter, epoch_size) if getattr(self, "auxiliary_head", None) is not None: @@ -55,7 +60,7 @@ def set_step_params(self, init_iter, epoch_size): self.auxiliary_head.set_step_params(init_iter, epoch_size) def _decode_head_forward_train(self, x, img_metas, pixel_weights=None, **kwargs): - + """Run forward train in decode head.""" trg_map = self._get_argument_by_name(self.decode_head.loss_target_name, kwargs) loss_decode, logits_decode = self.decode_head.forward_train( x, diff --git a/otx/mpa/modules/models/segmentors/otx_encoder_decoder.py b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py similarity index 72% rename from otx/mpa/modules/models/segmentors/otx_encoder_decoder.py rename to otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py index a5812775259..6bacc0d59cb 100644 --- a/otx/mpa/modules/models/segmentors/otx_encoder_decoder.py +++ b/otx/algorithms/segmentation/adapters/mmseg/models/segmentors/otx_encoder_decoder.py @@ -1,4 +1,5 @@ -# Copyright (C) 2022 Intel Corporation +"""OTX encoder decoder for semantic segmentation.""" +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -9,8 +10,11 @@ from otx.algorithms.common.adapters.mmdeploy.utils import is_mmdeploy_enabled +# pylint: disable=unused-argument, line-too-long @SEGMENTORS.register_module() class OTXEncoderDecoder(EncoderDecoder): + """OTX encoder decoder.""" + def simple_test(self, img, img_meta, rescale=True, output_logits=False): """Simple test with single image.""" seg_logit = self.inference(img, img_meta, rescale) @@ -34,26 +38,27 @@ def simple_test(self, img, img_meta, rescale=True, output_logits=False): if is_mmdeploy_enabled(): from mmdeploy.core import FUNCTION_REWRITER - from otx.mpa.modules.hooks.recording_forward_hooks import FeatureVectorHook - - @FUNCTION_REWRITER.register_rewriter( - "otx.mpa.modules.models.segmentors.otx_encoder_decoder.OTXEncoderDecoder.extract_feat" + from otx.mpa.modules.hooks.recording_forward_hooks import ( # pylint: disable=ungrouped-imports + FeatureVectorHook, ) + + BASE_CLASS = "otx.algorithms.segmentation.adapters.mmseg.models.segmentors.otx_encoder_decoder.OTXEncoderDecoder" + + @FUNCTION_REWRITER.register_rewriter(f"{BASE_CLASS}.extract_feat") def single_stage_detector__extract_feat(ctx, self, img): + """Extract feature.""" feat = self.backbone(img) self.feature_map = feat if self.with_neck: feat = self.neck(feat) return feat - @FUNCTION_REWRITER.register_rewriter( - "otx.mpa.modules.models.segmentors.otx_encoder_decoder.OTXEncoderDecoder.simple_test" - ) + @FUNCTION_REWRITER.register_rewriter(f"{BASE_CLASS}.simple_test") def single_stage_detector__simple_test(ctx, self, img, img_metas, **kwargs): + """Test.""" # with output activation seg_logit = self.inference(img, img_metas, True) if ctx.cfg["dump_features"]: feature_vector = FeatureVectorHook.func(self.feature_map) return seg_logit, feature_vector - else: - return seg_logit + return seg_logit diff --git a/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py index 23b312c33a5..1463ba35711 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/nncf/__init__.py @@ -1,9 +1,18 @@ """NNCF utils for mmseg.""" -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# -# flake8: noqa +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. from .builder import build_nncf_segmentor from .hooks import CustomstepLrUpdaterHook diff --git a/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py b/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py index cb8f9c54cb4..d09e1c8b157 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py +++ b/otx/algorithms/segmentation/adapters/mmseg/nncf/builder.py @@ -1,5 +1,5 @@ """NNCF wrapped mmcls models builder.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # diff --git a/otx/algorithms/segmentation/adapters/mmseg/nncf/hooks.py b/otx/algorithms/segmentation/adapters/mmseg/nncf/hooks.py index 7c572870719..b7270e60354 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/nncf/hooks.py +++ b/otx/algorithms/segmentation/adapters/mmseg/nncf/hooks.py @@ -1,5 +1,5 @@ """NNCF task related hooks.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # diff --git a/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py b/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py index 4acf96f333a..6955d15c02f 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py +++ b/otx/algorithms/segmentation/adapters/mmseg/nncf/patches.py @@ -1,5 +1,5 @@ """Patch mmseg library.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # diff --git a/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py b/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py index 8d8bd75c07e..690936587bd 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py +++ b/otx/algorithms/segmentation/adapters/mmseg/utils/__init__.py @@ -1,9 +1,20 @@ """OTX Adapters - mmseg.utils.""" -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. -from .builder import build_segmentor +from .builder import build_scalar_scheduler, build_segmentor from .config_utils import ( patch_config, patch_datasets, @@ -11,7 +22,7 @@ prepare_for_training, set_hyperparams, ) -from .data_utils import load_dataset_items +from .data_utils import get_valid_label_mask_per_batch, load_dataset_items __all__ = [ "patch_config", @@ -20,5 +31,7 @@ "prepare_for_training", "set_hyperparams", "load_dataset_items", + "build_scalar_scheduler", "build_segmentor", + "get_valid_label_mask_per_batch", ] diff --git a/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py b/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py index df0c8777b80..6c8d5512851 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py +++ b/otx/algorithms/segmentation/adapters/mmseg/utils/builder.py @@ -1,5 +1,5 @@ """MMseg model builder.""" -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # @@ -9,6 +9,23 @@ import torch from mmcv.runner import load_checkpoint from mmcv.utils import Config, ConfigDict +from mmseg.models.builder import MODELS + +SCALAR_SCHEDULERS = MODELS + + +def build_scalar_scheduler(cfg, default_value=None): + """Build scalar scheduler.""" + if cfg is None: + if default_value is not None: + assert isinstance(default_value, (int, float)) + cfg = dict(type="ConstantScalarScheduler", scale=float(default_value)) + else: + return None + elif isinstance(cfg, (int, float)): + cfg = dict(type="ConstantScalarScheduler", scale=float(cfg)) + + return SCALAR_SCHEDULERS.build(cfg) def build_segmentor( diff --git a/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py b/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py index bfcddf65e77..eaa38e49f13 100644 --- a/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py +++ b/otx/algorithms/segmentation/adapters/mmseg/utils/data_utils.py @@ -20,6 +20,7 @@ import cv2 import numpy as np +import torch import tqdm from mmseg.datasets.custom import CustomDataset from skimage.segmentation import felzenszwalb @@ -156,6 +157,17 @@ def get_extended_label_names(labels: List[LabelEntity]): return all_labels +def get_valid_label_mask_per_batch(img_metas, num_classes): + """Get valid label mask removing ignored classes to zero mask in a batch.""" + valid_label_mask_per_batch = [] + for _, meta in enumerate(img_metas): + valid_label_mask = torch.Tensor([1 for _ in range(num_classes)]) + if "ignored_labels" in meta and meta["ignored_labels"]: + valid_label_mask[meta["ignored_labels"]] = 0 + valid_label_mask_per_batch.append(valid_label_mask) + return valid_label_mask_per_batch + + @check_input_parameters_type() def create_pseudo_masks(ann_file_path: str, data_root_dir: str, mode="FH"): """Create pseudo masks for Self-SL using DetCon.""" diff --git a/otx/mpa/modules/datasets/pipelines/transforms/seg_custom_pipelines.py b/otx/mpa/modules/datasets/pipelines/transforms/seg_custom_pipelines.py deleted file mode 100644 index 51829425725..00000000000 --- a/otx/mpa/modules/datasets/pipelines/transforms/seg_custom_pipelines.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import mmcv -import numpy as np -from mmcv.parallel import DataContainer as DC -from mmseg.datasets import PIPELINES -from mmseg.datasets.pipelines.formatting import to_tensor - - -@PIPELINES.register_module(force=True) -class Normalize(object): - """Normalize the image. - - Added key is "img_norm_cfg". - - Args: - mean (sequence): Mean values of 3 channels. - std (sequence): Std values of 3 channels. - to_rgb (bool): Whether to convert the image from BGR to RGB, - default is true. - """ - - def __init__(self, mean, std, to_rgb=True): - self.mean = np.array(mean, dtype=np.float32) - self.std = np.array(std, dtype=np.float32) - self.to_rgb = to_rgb - - def __call__(self, results): - """Call function to normalize images. - - Args: - results (dict): Result dict from loading pipeline. - - Returns: - dict: Normalized results, 'img_norm_cfg' key is added into - result dict. - """ - - for target in ["img", "ul_w_img", "aux_img"]: - if target in results: - results[target] = mmcv.imnormalize(results[target], self.mean, self.std, self.to_rgb) - results["img_norm_cfg"] = dict(mean=self.mean, std=self.std, to_rgb=self.to_rgb) - - return results - - def __repr__(self): - repr_str = self.__class__.__name__ - repr_str += f"(mean={self.mean}, std={self.std}, to_rgb=" f"{self.to_rgb})" - return repr_str - - -@PIPELINES.register_module(force=True) -class DefaultFormatBundle(object): - """Default formatting bundle. - - It simplifies the pipeline of formatting common fields, including "img" - and "gt_semantic_seg". These fields are formatted as follows. - - - img: (1)transpose, (2)to tensor, (3)to DataContainer (stack=True) - - gt_semantic_seg: (1)unsqueeze dim-0 (2)to tensor, - (3)to DataContainer (stack=True) - """ - - def __call__(self, results): - """Call function to transform and format common fields in results. - - Args: - results (dict): Result dict contains the data to convert. - - Returns: - dict: The result dict contains the data that is formatted with - default bundle. - """ - for target in ["img", "ul_w_img", "aux_img"]: - if target not in results: - continue - - img = results[target] - if len(img.shape) < 3: - img = np.expand_dims(img, -1) - - if len(img.shape) == 3: - img = np.ascontiguousarray(img.transpose(2, 0, 1)).astype(np.float32) - elif len(img.shape) == 4: - # for selfsl or supcon - img = np.ascontiguousarray(img.transpose(0, 3, 1, 2)).astype(np.float32) - else: - raise ValueError(f"img.shape={img.shape} is not supported.") - - results[target] = DC(to_tensor(img), stack=True) - - for trg_name in ["gt_semantic_seg", "gt_class_borders", "pixel_weights"]: - if trg_name not in results: - continue - - out_type = np.float32 if trg_name == "pixel_weights" else np.int64 - results[trg_name] = DC(to_tensor(results[trg_name][None, ...].astype(out_type)), stack=True) - - return results - - def __repr__(self): - return self.__class__.__name__ - - -@PIPELINES.register_module() -class BranchImage(object): - def __init__(self, key_map={}): - self.key_map = key_map - - def __call__(self, results): - for k1, k2 in self.key_map.items(): - if k1 in results: - results[k2] = results[k1] - if k1 in results["img_fields"]: - results["img_fields"].append(k2) - return results - - def __repr__(self): - repr_str = self.__class__.__name__ - return repr_str diff --git a/otx/mpa/modules/models/builder.py b/otx/mpa/modules/models/builder.py deleted file mode 100644 index f064c6321d1..00000000000 --- a/otx/mpa/modules/models/builder.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from mmseg.models.builder import MODELS - -SCALAR_SCHEDULERS = MODELS - - -def build_scalar_scheduler(cfg, default_value=None): - if cfg is None: - if default_value is not None: - assert isinstance(default_value, (int, float)) - cfg = dict(type="ConstantScalarScheduler", scale=float(default_value)) - else: - return None - elif isinstance(cfg, (int, float)): - cfg = dict(type="ConstantScalarScheduler", scale=float(cfg)) - - return SCALAR_SCHEDULERS.build(cfg) diff --git a/otx/mpa/modules/models/heads/aggregator_mixin.py b/otx/mpa/modules/models/heads/aggregator_mixin.py deleted file mode 100644 index 4f320c7bb9d..00000000000 --- a/otx/mpa/modules/models/heads/aggregator_mixin.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2020-2021 The MMSegmentation Authors -# SPDX-License-Identifier: Apache-2.0 -# -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import torch.nn as nn - -from ..utils import IterativeAggregator, IterativeConcatAggregator - - -class AggregatorMixin(nn.Module): - def __init__( - self, - *args, - enable_aggregator=False, - aggregator_min_channels=None, - aggregator_merge_norm=None, - aggregator_use_concat=False, - **kwargs - ): - - in_channels = kwargs.get("in_channels") - in_index = kwargs.get("in_index") - norm_cfg = kwargs.get("norm_cfg") - conv_cfg = kwargs.get("conv_cfg") - input_transform = kwargs.get("input_transform") - - aggregator = None - if enable_aggregator: - assert isinstance(in_channels, (tuple, list)) - assert len(in_channels) > 1 - - aggregator = IterativeAggregator( - in_channels=in_channels, - min_channels=aggregator_min_channels, - conv_cfg=conv_cfg, - norm_cfg=norm_cfg, - merge_norm=aggregator_merge_norm, - use_concat=aggregator_use_concat, - ) - - aggregator_min_channels = aggregator_min_channels if aggregator_min_channels is not None else 0 - # change arguments temporarily - kwargs["in_channels"] = max(in_channels[0], aggregator_min_channels) - kwargs["input_transform"] = None - if in_index is not None: - kwargs["in_index"] = in_index[0] - - super(AggregatorMixin, self).__init__(*args, **kwargs) - - self.aggregator = aggregator - # re-define variables - self.in_channels = in_channels - self.input_transform = input_transform - self.in_index = in_index - - def _transform_inputs(self, inputs): - inputs = super()._transform_inputs(inputs) - if self.aggregator is not None: - inputs = self.aggregator(inputs)[0] - return inputs diff --git a/otx/mpa/modules/models/heads/mix_loss_mixin.py b/otx/mpa/modules/models/heads/mix_loss_mixin.py deleted file mode 100644 index 7dccb520748..00000000000 --- a/otx/mpa/modules/models/heads/mix_loss_mixin.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import torch -import torch.nn as nn -import torch.nn.functional as F -from mmcv.runner import force_fp32 - - -class MixLossMixin(nn.Module): - @staticmethod - def _mix_loss(logits, target, ignore_index=255): - num_samples = logits.size(0) - assert num_samples % 2 == 0 - - with torch.no_grad(): - probs = F.softmax(logits, dim=1) - probs_a, probs_b = torch.split(probs, num_samples // 2) - mean_probs = 0.5 * (probs_a + probs_b) - trg_probs = torch.cat([mean_probs, mean_probs], dim=0) - - log_probs = torch.log_softmax(logits, dim=1) - losses = torch.sum(trg_probs * log_probs, dim=1).neg() - - valid_mask = target != ignore_index - valid_losses = torch.where(valid_mask, losses, torch.zeros_like(losses)) - - return valid_losses.mean() - - @force_fp32(apply_to=("seg_logit",)) - def losses(self, seg_logit, seg_label, train_cfg, *args, **kwargs): - loss = super().losses(seg_logit, seg_label, train_cfg, *args, **kwargs) - if train_cfg.get("mix_loss", None) and train_cfg.mix_loss.get("enable", False): - mix_loss = self._mix_loss(seg_logit, seg_label, ignore_index=self.ignore_index) - - mix_loss_weight = train_cfg.mix_loss.get("weight", 1.0) - loss["loss_mix"] = mix_loss_weight * mix_loss - - return loss diff --git a/otx/mpa/modules/models/heads/segment_out_norm_mixin.py b/otx/mpa/modules/models/heads/segment_out_norm_mixin.py deleted file mode 100644 index c82af88c817..00000000000 --- a/otx/mpa/modules/models/heads/segment_out_norm_mixin.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import torch.nn as nn - -from ..utils import AngularPWConv, normalize - - -class SegmentOutNormMixin(nn.Module): - def __init__(self, *args, enable_out_seg=True, enable_out_norm=False, **kwargs): - super().__init__(*args, **kwargs) - - self.enable_out_seg = enable_out_seg - self.enable_out_norm = enable_out_norm - - if enable_out_seg: - if enable_out_norm: - self.conv_seg = AngularPWConv(self.channels, self.out_channels, clip_output=True) - else: - self.conv_seg = None - - def cls_seg(self, feat): - """Classify each pixel.""" - if self.dropout is not None: - feat = self.dropout(feat) - if self.enable_out_norm: - feat = normalize(feat, dim=1, p=2) - if self.conv_seg is not None: - return self.conv_seg(feat) - else: - return feat diff --git a/otx/mpa/modules/models/scalar_schedulers/__init__.py b/otx/mpa/modules/models/scalar_schedulers/__init__.py deleted file mode 100644 index f79e183f50f..00000000000 --- a/otx/mpa/modules/models/scalar_schedulers/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -from .constant import ConstantScalarScheduler -from .poly import PolyScalarScheduler -from .step import StepScalarScheduler - -__all__ = [ - "ConstantScalarScheduler", - "PolyScalarScheduler", - "StepScalarScheduler", -] diff --git a/otx/mpa/modules/models/segmentors/__init__.py b/otx/mpa/modules/models/segmentors/__init__.py deleted file mode 100644 index d7b6a1934c7..00000000000 --- a/otx/mpa/modules/models/segmentors/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -# flake8: noqa -from . import class_incr_encoder_decoder, mean_teacher_segmentor, otx_encoder_decoder diff --git a/otx/mpa/modules/models/segmentors/class_incr_encoder_decoder.py b/otx/mpa/modules/models/segmentors/class_incr_encoder_decoder.py deleted file mode 100644 index 6cb17955106..00000000000 --- a/otx/mpa/modules/models/segmentors/class_incr_encoder_decoder.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import functools - -from mmseg.models import SEGMENTORS -from mmseg.utils import get_root_logger - -from otx.mpa.modules.utils.task_adapt import map_class_names - -from .mix_loss_mixin import MixLossMixin -from .otx_encoder_decoder import OTXEncoderDecoder -from .pixel_weights_mixin import PixelWeightsMixin - - -@SEGMENTORS.register_module() -class ClassIncrEncoderDecoder(MixLossMixin, PixelWeightsMixin, OTXEncoderDecoder): - """ """ - - def __init__(self, *args, task_adapt=None, **kwargs): - super().__init__(*args, **kwargs) - - # Hook for class-sensitive weight loading - assert task_adapt is not None, "When using task_adapt, task_adapt must be set." - - self._register_load_state_dict_pre_hook( - functools.partial( - self.load_state_dict_pre_hook, - self, # model - task_adapt["dst_classes"], # model_classes - task_adapt["src_classes"], # chkpt_classes - ) - ) - - @staticmethod - def load_state_dict_pre_hook(model, model_classes, chkpt_classes, chkpt_dict, prefix, *args, **kwargs): - """Modify input state_dict according to class name matching before weight loading""" - logger = get_root_logger("INFO") - logger.info(f"----------------- ClassIncrEncoderDecoder.load_state_dict_pre_hook() called w/ prefix: {prefix}") - - # Dst to src mapping index - model_classes = list(model_classes) - chkpt_classes = list(chkpt_classes) - model2chkpt = map_class_names(model_classes, chkpt_classes) - logger.info(f"{chkpt_classes} -> {model_classes} ({model2chkpt})") - - model_dict = model.state_dict() - param_names = [ - "decode_head.conv_seg.weight", - "decode_head.conv_seg.bias", - ] - for model_name in param_names: - chkpt_name = prefix + model_name - if model_name not in model_dict or chkpt_name not in chkpt_dict: - logger.info(f"Skipping weight copy: {chkpt_name}") - continue - - # Mix weights - model_param = model_dict[model_name].clone() - chkpt_param = chkpt_dict[chkpt_name] - for m, c in enumerate(model2chkpt): - if c >= 0: - model_param[m].copy_(chkpt_param[c]) - - # Replace checkpoint weight by mixed weights - chkpt_dict[chkpt_name] = model_param diff --git a/otx/mpa/modules/models/segmentors/mix_loss_mixin.py b/otx/mpa/modules/models/segmentors/mix_loss_mixin.py deleted file mode 100644 index 63a820d612a..00000000000 --- a/otx/mpa/modules/models/segmentors/mix_loss_mixin.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import torch -import torch.nn as nn - - -class MixLossMixin(object): - def forward_train(self, img, img_metas, gt_semantic_seg, aux_img=None, **kwargs): - """Forward function for training. - - Args: - img (Tensor): Input images. - img_metas (list[dict]): List of image info dict where each dict - has: 'img_shape', 'scale_factor', 'flip', and may also contain - 'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'. - For details on the values of these keys see - `mmseg/datasets/pipelines/formatting.py:Collect`. - gt_semantic_seg (Tensor): Semantic segmentation masks - used if the architecture supports semantic segmentation task. - aux_img (Tensor): Auxiliary images. - - Returns: - dict[str, Tensor]: a dictionary of loss components - """ - - if aux_img is not None: - mix_loss_enabled = False - mix_loss_cfg = self.train_cfg.get("mix_loss", None) - if mix_loss_cfg is not None: - mix_loss_enabled = mix_loss_cfg.get("enable", False) - if mix_loss_enabled: - self.train_cfg.mix_loss.enable = mix_loss_enabled - - if self.train_cfg.mix_loss.enable: - img = torch.cat([img, aux_img], dim=0) - gt_semantic_seg = torch.cat([gt_semantic_seg, gt_semantic_seg], dim=0) - - return super().forward_train(img, img_metas, gt_semantic_seg, **kwargs) diff --git a/otx/mpa/modules/ov/models/mmseg/decode_heads/__init__.py b/otx/mpa/modules/ov/models/mmseg/decode_heads/__init__.py deleted file mode 100644 index 26515f29058..00000000000 --- a/otx/mpa/modules/ov/models/mmseg/decode_heads/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -# flake8: noqa -from .mmov_decode_head import MMOVDecodeHead diff --git a/otx/mpa/modules/utils/seg_utils.py b/otx/mpa/modules/utils/seg_utils.py deleted file mode 100644 index a293c4fd378..00000000000 --- a/otx/mpa/modules/utils/seg_utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import torch - - -def get_valid_label_mask_per_batch(img_metas, num_classes): - valid_label_mask_per_batch = [] - for _, meta in enumerate(img_metas): - valid_label_mask = torch.Tensor([1 for _ in range(num_classes)]) - if "ignored_labels" in meta and meta["ignored_labels"]: - valid_label_mask[meta["ignored_labels"]] = 0 - valid_label_mask_per_batch.append(valid_label_mask) - return valid_label_mask_per_batch diff --git a/otx/mpa/seg/__init__.py b/otx/mpa/seg/__init__.py index 8c091f1f20c..b7b2c4ffb47 100644 --- a/otx/mpa/seg/__init__.py +++ b/otx/mpa/seg/__init__.py @@ -2,16 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 # -import otx.mpa.modules.datasets.pipelines.compose -import otx.mpa.modules.datasets.pipelines.transforms.seg_custom_pipelines +import otx.algorithms.segmentation.adapters.mmseg +import otx.algorithms.segmentation.adapters.mmseg.models +import otx.algorithms.segmentation.adapters.mmseg.models.schedulers import otx.mpa.modules.hooks -import otx.mpa.modules.models.backbones.litehrnet -import otx.mpa.modules.models.heads.custom_fcn_head -import otx.mpa.modules.models.losses.cross_entropy_loss_with_ignore -import otx.mpa.modules.models.scalar_schedulers.constant -import otx.mpa.modules.models.scalar_schedulers.poly -import otx.mpa.modules.models.scalar_schedulers.step -import otx.mpa.modules.models.segmentors from otx.mpa.seg.incremental import IncrSegInferrer, IncrSegTrainer from otx.mpa.seg.semisl import SemiSLSegExporter, SemiSLSegInferrer, SemiSLSegTrainer diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/data/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/data/__init__.py deleted file mode 100644 index 3bdbe22ef68..00000000000 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/data/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Test for otx.algorithms.segmentation.adapters.mmseg.data""" -# Copyright (C) 2022 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/data/test_pipelines.py b/tests/unit/algorithms/segmentation/adapters/mmseg/data/test_pipelines.py deleted file mode 100644 index 2ff48d5c195..00000000000 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/data/test_pipelines.py +++ /dev/null @@ -1,151 +0,0 @@ -from typing import Any, Dict - -import numpy as np -import pytest -from PIL import Image - -from otx.algorithms.segmentation.adapters.mmseg.data.pipelines import ( - NDArrayToPILImage, - PILImageToNDArray, - RandomColorJitter, - RandomGaussianBlur, - RandomGrayscale, - RandomResizedCrop, - RandomSolarization, - TwoCropTransform, -) -from tests.test_suite.e2e_test_system import e2e_pytest_unit - - -@pytest.fixture(scope="module") -def inputs_np(): - return { - "img": np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8), - "gt_semantic_seg": np.random.rand(16, 16), - "flip": True, - } - - -@pytest.fixture(scope="module") -def inputs_PIL(): - return { - "img": Image.fromarray(np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8)), - "gt_semantic_seg": np.random.randint(0, 5, (16, 16), dtype=np.uint8), - "seg_fields": ["gt_semantic_seg"], - "ori_shape": (16, 16, 3), - } - - -class TestTwoCropTransform: - @pytest.fixture(autouse=True) - def setup(self, mocker) -> None: - mocker.patch( - "otx.algorithms.segmentation.adapters.mmseg.data.pipelines.build_from_cfg", return_value=lambda x: x - ) - self.two_crop_transform = TwoCropTransform(view0=[], view1=[]) - - @e2e_pytest_unit - def test_call(self, mocker, inputs_np: Dict[str, Any]) -> None: - """Test __call__.""" - results = self.two_crop_transform(inputs_np) - - assert isinstance(results, dict) - assert "img" in results and results["img"].ndim == 4 - assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 3 - assert "flip" in results and isinstance(results["flip"], list) - - @e2e_pytest_unit - def test_call_with_single_pipeline(self, mocker, inputs_np: Dict[str, Any]) -> None: - """Test __call__ with single pipeline.""" - self.two_crop_transform.is_both = False - - results = self.two_crop_transform(inputs_np) - - assert isinstance(results, dict) - assert "img" in results and results["img"].ndim == 3 - assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 2 - assert "flip" in results and isinstance(results["flip"], bool) - - -@e2e_pytest_unit -def test_random_resized_crop(inputs_PIL: Dict[str, Any]) -> None: - """Test RandomResizedCrop.""" - random_resized_crop = RandomResizedCrop(size=(8, 8)) - - results = random_resized_crop(inputs_PIL) - - assert isinstance(results, dict) - assert "img" in results and results["img"].size == (8, 8) - assert "gt_semantic_seg" in results and results["gt_semantic_seg"].shape == (8, 8) - assert "img_shape" in results - assert "ori_shape" in results - assert "scale_factor" in results - - -@e2e_pytest_unit -def test_random_color_jitter(inputs_PIL: Dict[str, Any]) -> None: - """Test RandomColorJitter.""" - random_color_jitter = RandomColorJitter(p=1.0) - - results = random_color_jitter(inputs_PIL) - - assert isinstance(results, dict) - assert "img" in results - - -@e2e_pytest_unit -def test_random_grayscale(inputs_PIL: Dict[str, Any]) -> None: - """Test RandomGrayscale.""" - random_grayscale = RandomGrayscale() - - results = random_grayscale(inputs_PIL) - - assert isinstance(results, dict) - assert "img" in results - - -@e2e_pytest_unit -def test_random_gaussian_blur(inputs_PIL: Dict[str, Any]) -> None: - """Test RandomGaussianBlur.""" - random_gaussian_blur = RandomGaussianBlur(p=1.0, kernel_size=3) - - results = random_gaussian_blur(inputs_PIL) - - assert isinstance(results, dict) - assert "img" in results - - -@e2e_pytest_unit -def test_random_solarization(inputs_np: Dict[str, Any]) -> None: - """Test RandomSolarization.""" - random_solarization = RandomSolarization(p=1.0) - - results = random_solarization(inputs_np) - - assert isinstance(results, dict) - assert "img" in results - assert repr(random_solarization) == "RandomSolarization" - - -@e2e_pytest_unit -def test_nd_array_to_pil_image(inputs_np: Dict[str, Any]) -> None: - """Test NDArrayToPILImage.""" - nd_array_to_pil_image = NDArrayToPILImage(keys=["img"]) - - results = nd_array_to_pil_image(inputs_np) - - assert "img" in results - assert isinstance(results["img"], Image.Image) - assert repr(nd_array_to_pil_image) == "NDArrayToPILImage" - - -@e2e_pytest_unit -def test_pil_image_to_nd_array(inputs_PIL: Dict[str, Any]) -> None: - """Test PILImageToNDArray.""" - pil_image_to_nd_array = PILImageToNDArray(keys=["img"]) - - results = pil_image_to_nd_array(inputs_PIL) - - assert "img" in results - assert isinstance(results["img"], np.ndarray) - assert repr(pil_image_to_nd_array) == "PILImageToNDArray" diff --git a/otx/mpa/modules/models/backbones/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py similarity index 54% rename from otx/mpa/modules/models/backbones/__init__.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py index 4e1701262e2..d671e6bb59c 100644 --- a/otx/mpa/modules/models/backbones/__init__.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/__init__.py @@ -1,5 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.datasets""" # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # - -# flake8: noqa diff --git a/otx/mpa/modules/ov/models/mmseg/backbones/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py similarity index 50% rename from otx/mpa/modules/ov/models/mmseg/backbones/__init__.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py index 1ad81562177..e2b1bd6ce7b 100644 --- a/otx/mpa/modules/ov/models/mmseg/backbones/__init__.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/__init__.py @@ -1,6 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines""" # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # - -# flake8: noqa -from .mmov_backbone import MMOVBackbone diff --git a/tests/unit/mpa/modules/datasets/pipelines/test_compose.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py similarity index 98% rename from tests/unit/mpa/modules/datasets/pipelines/test_compose.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py index 96b9602fd8b..4c777d83a3f 100644 --- a/tests/unit/mpa/modules/datasets/pipelines/test_compose.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_compose.py @@ -12,7 +12,7 @@ from mmseg.datasets.builder import PIPELINES from mmseg.datasets.pipelines import RandomCrop -from otx.mpa.modules.datasets.pipelines.compose import MaskCompose, ProbCompose +from otx.algorithms.segmentation.adapters.mmseg.datasets import MaskCompose, ProbCompose class TestProbCompose: diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py new file mode 100644 index 00000000000..37d5275fb2e --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_loads.py @@ -0,0 +1,53 @@ +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import numpy as np +import pytest + +from otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines.loads import ( + LoadAnnotationFromOTXDataset, +) +from otx.api.entities.annotation import ( + Annotation, + AnnotationSceneEntity, + AnnotationSceneKind, +) +from otx.api.entities.dataset_item import DatasetItemEntity +from otx.api.entities.image import Image +from otx.api.entities.label import Domain, LabelEntity +from otx.api.entities.scored_label import ScoredLabel +from otx.api.entities.shapes.rectangle import Rectangle +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +def label_entity(name="test label") -> LabelEntity: + return LabelEntity(name=name, domain=Domain.SEGMENTATION) + + +def dataset_item() -> DatasetItemEntity: + image: Image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) + annotation: Annotation = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(label_entity())]) + annotation_scene: AnnotationSceneEntity = AnnotationSceneEntity( + annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION + ) + return DatasetItemEntity(media=image, annotation_scene=annotation_scene) + + +class TestLoadAnnotationFromOTXDataset: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + + self.dataset_item: DatasetItemEntity = dataset_item() + self.results: dict = { + "dataset_item": self.dataset_item, + "ann_info": {"labels": [label_entity("class_1")]}, + "seg_fields": [], + } + self.pipeline: LoadAnnotationFromOTXDataset = LoadAnnotationFromOTXDataset() + + @e2e_pytest_unit + def test_call(self) -> None: + loaded_annotations: dict = self.pipeline(self.results) + assert "gt_semantic_seg" in loaded_annotations + assert loaded_annotations["dataset_item"] == self.dataset_item diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines_params_validation.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_pipelines_params_validation.py similarity index 97% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines_params_validation.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_pipelines_params_validation.py index 41373df066c..db88ce2a8ad 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines_params_validation.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_pipelines_params_validation.py @@ -4,7 +4,7 @@ import pytest -from otx.algorithms.segmentation.adapters.mmseg.data.pipelines import ( +from otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines import ( LoadAnnotationFromOTXDataset, LoadImageFromOTXDataset, ) diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py new file mode 100644 index 00000000000..facded59996 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/pipelines/test_transforms.py @@ -0,0 +1,313 @@ +from typing import Any, Dict + +import numpy as np +import pytest +import torch +from mmcv.parallel import DataContainer +from PIL import Image + +from otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines.transforms import ( + BranchImage, + DefaultFormatBundle, + NDArrayToPILImage, + Normalize, + PILImageToNDArray, + RandomColorJitter, + RandomGaussianBlur, + RandomGrayscale, + RandomResizedCrop, + RandomSolarization, + TwoCropTransform, +) +from tests.test_suite.e2e_test_system import e2e_pytest_unit + + +@pytest.fixture(scope="module") +def inputs_np(): + return { + "img": np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8), + "gt_semantic_seg": np.random.rand(16, 16), + "flip": True, + } + + +@pytest.fixture(scope="module") +def inputs_PIL(): + return { + "img": Image.fromarray(np.random.randint(0, 10, (16, 16, 3), dtype=np.uint8)), + "gt_semantic_seg": np.random.randint(0, 5, (16, 16), dtype=np.uint8), + "seg_fields": ["gt_semantic_seg"], + "ori_shape": (16, 16, 3), + } + + +class TestNDArrayToPILImage: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} + self.nd_array_to_pil_image: NDArrayToPILImage = NDArrayToPILImage(keys=["img"]) + + @e2e_pytest_unit + def test_call(self) -> None: + converted_img: dict = self.nd_array_to_pil_image(self.results) + assert "img" in converted_img + assert isinstance(converted_img["img"], Image.Image) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.nd_array_to_pil_image) == "NDArrayToPILImage" + + +class TestPILImageToNDArray: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": Image.new("RGB", (3, 3))} + self.pil_image_to_nd_array: PILImageToNDArray = PILImageToNDArray(keys=["img"]) + + @e2e_pytest_unit + def test_call(self) -> None: + converted_array: dict = self.pil_image_to_nd_array(self.results) + assert "img" in converted_array + assert isinstance(converted_array["img"], np.ndarray) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.pil_image_to_nd_array) == "PILImageToNDArray" + + +class TestRandomResizedCrop: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": Image.new("RGB", (10, 16)), "img_shape": (10, 16), "ori_shape": (10, 16)} + self.random_resized_crop: RandomResizedCrop = RandomResizedCrop((5, 5), (0.5, 1.0)) + + @e2e_pytest_unit + def test_call(self) -> None: + cropped_img: dict = self.random_resized_crop(self.results) + assert cropped_img["img_shape"] == (5, 5) + assert cropped_img["ori_shape"] == (10, 16) + + +class TestRandomSolarization: + @pytest.fixture(autouse=True) + def setUp(self) -> None: + self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} + self.random_solarization: RandomSolarization = RandomSolarization(p=1.0) + + @e2e_pytest_unit + def test_call(self) -> None: + solarized: dict = self.random_solarization(self.results) + assert "img" in solarized + assert isinstance(solarized["img"], np.ndarray) + + @e2e_pytest_unit + def test_repr(self) -> None: + assert str(self.random_solarization) == "RandomSolarization" + + +class TestNormalize: + @e2e_pytest_unit + @pytest.mark.parametrize( + "mean,std,to_rgb,expected", + [ + (1.0, 1.0, True, np.array([[[1.0, 0.0, 0.0]]], dtype=np.float32)), + (1.0, 1.0, False, np.array([[[-1.0, 0.0, 0.0]]], dtype=np.float32)), + ], + ) + def test_call(self, mean: float, std: float, to_rgb: bool, expected: np.array) -> None: + """Test __call__.""" + normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) + inputs = dict(img=np.arange(3).reshape(1, 1, 3)) + + results = normalize(inputs.copy()) + + assert "img" in results + assert "img_norm_cfg" in results + assert np.all(results["img"] == expected) + + @e2e_pytest_unit + @pytest.mark.parametrize("mean,std,to_rgb", [(1.0, 1.0, True)]) + def test_repr(self, mean: float, std: float, to_rgb: bool) -> None: + """Test __repr__.""" + normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) + + assert repr(normalize) == normalize.__class__.__name__ + f"(mean={mean}, std={std}, to_rgb=" f"{to_rgb})" + + +class TestDefaultFormatBundle: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.default_format_bundle = DefaultFormatBundle() + + @e2e_pytest_unit + @pytest.mark.parametrize("img", [np.ones((1, 1)), np.ones((1, 1, 1)), np.ones((1, 1, 1, 1))]) + @pytest.mark.parametrize("gt_semantic_seg,pixel_weights", [(np.ones((1, 1)), np.ones((1, 1)))]) + def test_call(self, img: np.array, gt_semantic_seg: np.array, pixel_weights: np.array) -> None: + """Test __call__.""" + inputs = dict(img=img, gt_semantic_seg=gt_semantic_seg, pixel_weights=pixel_weights) + + results = self.default_format_bundle(inputs.copy()) + + assert isinstance(results, dict) + assert "img" in results + assert isinstance(results["img"], DataContainer) + assert len(results["img"].data.shape) >= 3 + assert results["img"].data.dtype == torch.float32 + assert "gt_semantic_seg" in results + assert len(results["gt_semantic_seg"].data.shape) == len(inputs["gt_semantic_seg"].shape) + 1 + assert results["gt_semantic_seg"].data.dtype == torch.int64 + assert "pixel_weights" in results + assert len(results["pixel_weights"].data.shape) == len(inputs["pixel_weights"].shape) + 1 + assert results["pixel_weights"].data.dtype == torch.float32 + + @e2e_pytest_unit + @pytest.mark.parametrize("img", [np.ones((1,))]) + def test_call_invalid_shape(self, img: np.array): + inputs = dict(img=img) + + with pytest.raises(ValueError): + self.default_format_bundle(inputs.copy()) + + @e2e_pytest_unit + def test_repr(self) -> None: + """Test __repr__.""" + assert repr(self.default_format_bundle) == self.default_format_bundle.__class__.__name__ + + +class TestBranchImage: + @pytest.fixture(autouse=True) + def setup(self) -> None: + self.branch_image = BranchImage(key_map={"key1": "key2"}) + + @e2e_pytest_unit + def test_call(self) -> None: + """Test __call__.""" + inputs = dict(key1="key1", img_fields=["key1"]) + + results = self.branch_image(inputs.copy()) + + assert isinstance(results, dict) + assert "key2" in results + assert results["key1"] == results["key2"] + assert "key2" in results["img_fields"] + + @e2e_pytest_unit + def test_repr(self) -> None: + """Test __repr__.""" + assert repr(self.branch_image) == self.branch_image.__class__.__name__ + + +class TestTwoCropTransform: + @pytest.fixture(autouse=True) + def setup(self, mocker) -> None: + mocker.patch( + "otx.algorithms.segmentation.adapters.mmseg.datasets.pipelines.transforms.build_from_cfg", + return_value=lambda x: x, + ) + self.two_crop_transform = TwoCropTransform(view0=[], view1=[]) + + @e2e_pytest_unit + def test_call(self, mocker, inputs_np: Dict[str, Any]) -> None: + """Test __call__.""" + results = self.two_crop_transform(inputs_np) + + assert isinstance(results, dict) + assert "img" in results and results["img"].ndim == 4 + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 3 + assert "flip" in results and isinstance(results["flip"], list) + + @e2e_pytest_unit + def test_call_with_single_pipeline(self, mocker, inputs_np: Dict[str, Any]) -> None: + """Test __call__ with single pipeline.""" + self.two_crop_transform.is_both = False + + results = self.two_crop_transform(inputs_np) + + assert isinstance(results, dict) + assert "img" in results and results["img"].ndim == 3 + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].ndim == 2 + assert "flip" in results and isinstance(results["flip"], bool) + + +@e2e_pytest_unit +def test_random_resized_crop(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomResizedCrop.""" + random_resized_crop = RandomResizedCrop(size=(8, 8)) + + results = random_resized_crop(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results and results["img"].size == (8, 8) + assert "gt_semantic_seg" in results and results["gt_semantic_seg"].shape == (8, 8) + assert "img_shape" in results + assert "ori_shape" in results + assert "scale_factor" in results + + +@e2e_pytest_unit +def test_random_color_jitter(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomColorJitter.""" + random_color_jitter = RandomColorJitter(p=1.0) + + results = random_color_jitter(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_grayscale(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomGrayscale.""" + random_grayscale = RandomGrayscale() + + results = random_grayscale(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_gaussian_blur(inputs_PIL: Dict[str, Any]) -> None: + """Test RandomGaussianBlur.""" + random_gaussian_blur = RandomGaussianBlur(p=1.0, kernel_size=3) + + results = random_gaussian_blur(inputs_PIL) + + assert isinstance(results, dict) + assert "img" in results + + +@e2e_pytest_unit +def test_random_solarization(inputs_np: Dict[str, Any]) -> None: + """Test RandomSolarization.""" + random_solarization = RandomSolarization(p=1.0) + + results = random_solarization(inputs_np) + + assert isinstance(results, dict) + assert "img" in results + assert repr(random_solarization) == "RandomSolarization" + + +@e2e_pytest_unit +def test_nd_array_to_pil_image(inputs_np: Dict[str, Any]) -> None: + """Test NDArrayToPILImage.""" + nd_array_to_pil_image = NDArrayToPILImage(keys=["img"]) + + results = nd_array_to_pil_image(inputs_np) + + assert "img" in results + assert isinstance(results["img"], Image.Image) + assert repr(nd_array_to_pil_image) == "NDArrayToPILImage" + + +@e2e_pytest_unit +def test_pil_image_to_nd_array(inputs_PIL: Dict[str, Any]) -> None: + """Test PILImageToNDArray.""" + pil_image_to_nd_array = PILImageToNDArray(keys=["img"]) + + results = pil_image_to_nd_array(inputs_PIL) + + assert "img" in results + assert isinstance(results["img"], np.ndarray) + assert repr(pil_image_to_nd_array) == "PILImageToNDArray" diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py similarity index 97% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py index 5b797baed65..7327c1f3b35 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from otx.algorithms.segmentation.adapters.mmseg.data.dataset import MPASegDataset +from otx.algorithms.segmentation.adapters.mmseg.datasets import MPASegDataset from otx.api.entities.annotation import ( Annotation, AnnotationSceneEntity, diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset_params_validation.py b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset_params_validation.py similarity index 99% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset_params_validation.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset_params_validation.py index ca629d38734..7ad55560b65 100644 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_dataset_params_validation.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/datasets/test_dataset_params_validation.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from otx.algorithms.segmentation.adapters.mmseg.data.dataset import ( +from otx.algorithms.segmentation.adapters.mmseg.datasets.dataset import ( OTXSegDataset, get_annotation_mmseg_format, ) diff --git a/tests/unit/mpa/modules/models/scalar_schedulers/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py similarity index 50% rename from tests/unit/mpa/modules/models/scalar_schedulers/__init__.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py index 57e225a8be4..dc3be0d0648 100644 --- a/tests/unit/mpa/modules/models/scalar_schedulers/__init__.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/__init__.py @@ -1,4 +1,4 @@ -"""Test for otx.mpa.modules.models.scheculers""" +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.backbones.""" # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/mpa/modules/models/backbones/test_litehrnet.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py similarity index 97% rename from tests/unit/mpa/modules/models/backbones/test_litehrnet.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py index f59878fb309..361e1527f1d 100644 --- a/tests/unit/mpa/modules/models/backbones/test_litehrnet.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_litehrnet.py @@ -6,7 +6,7 @@ from otx.algorithms.common.adapters.mmcv.configs.backbones.lite_hrnet_18 import ( model as model_cfg, ) -from otx.mpa.modules.models.backbones.litehrnet import ( +from otx.algorithms.segmentation.adapters.mmseg.models.backbones.litehrnet import ( LiteHRNet, NeighbourSupport, SpatialWeightingV2, diff --git a/tests/unit/mpa/modules/ov/models/mmseg/backbones/test_ov_mmseg_mmov_backbone.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py similarity index 94% rename from tests/unit/mpa/modules/ov/models/mmseg/backbones/test_ov_mmseg_mmov_backbone.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py index 8796f89ef2a..24aebfcbe81 100644 --- a/tests/unit/mpa/modules/ov/models/mmseg/backbones/test_ov_mmseg_mmov_backbone.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/backbones/test_mmseg_mmov_backbone.py @@ -7,7 +7,7 @@ import pytest import torch -from otx.mpa.modules.ov.models.mmseg.backbones.mmov_backbone import MMOVBackbone +from otx.algorithms.segmentation.adapters.mmseg.models.backbones import MMOVBackbone from tests.test_suite.e2e_test_system import e2e_pytest_unit diff --git a/tests/unit/mpa/modules/models/backbones/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py similarity index 52% rename from tests/unit/mpa/modules/models/backbones/__init__.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py index 24b1785922f..60b6f78ebef 100644 --- a/tests/unit/mpa/modules/models/backbones/__init__.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/__init__.py @@ -1,4 +1,4 @@ -"""Test for otx.mpa.modules.models.backbones.""" +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.heads.""" # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/mpa/modules/ov/models/mmseg/decode_heads/test_ov_mmseg_mmov_decode_head.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py similarity index 95% rename from tests/unit/mpa/modules/ov/models/mmseg/decode_heads/test_ov_mmseg_mmov_decode_head.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py index c4fccf7ab9b..07f46d0e8ae 100644 --- a/tests/unit/mpa/modules/ov/models/mmseg/decode_heads/test_ov_mmseg_mmov_decode_head.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/heads/test_mmseg_mmov_decode_head.py @@ -7,7 +7,7 @@ import pytest import torch -from otx.mpa.modules.ov.models.mmseg.decode_heads.mmov_decode_head import MMOVDecodeHead +from otx.algorithms.segmentation.adapters.mmseg.models import MMOVDecodeHead from tests.test_suite.e2e_test_system import e2e_pytest_unit diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py new file mode 100644 index 00000000000..c1b25fc5a37 --- /dev/null +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/__init__.py @@ -0,0 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.models.scheculers""" + +# Copyright (C) 2023 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/unit/mpa/modules/models/scalar_schedulers/test_schedulers.py b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py similarity index 98% rename from tests/unit/mpa/modules/models/scalar_schedulers/test_schedulers.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py index a79cbdc67e6..1295b8d37eb 100644 --- a/tests/unit/mpa/modules/models/scalar_schedulers/test_schedulers.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/models/scalar_schedulers/test_schedulers.py @@ -6,7 +6,7 @@ import pytest -from otx.mpa.modules.models.scalar_schedulers import ( +from otx.algorithms.segmentation.adapters.mmseg.models.schedulers import ( ConstantScalarScheduler, PolyScalarScheduler, StepScalarScheduler, diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines.py b/tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines.py deleted file mode 100644 index d59987c3820..00000000000 --- a/tests/unit/algorithms/segmentation/adapters/mmseg/test_pipelines.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2023 Intel Corporation -# SPDX-License-Identifier: Apache-2.0 -# - -import numpy as np -import PIL.Image -import pytest - -from otx.algorithms.segmentation.adapters.mmseg.data.pipelines import ( - LoadAnnotationFromOTXDataset, - NDArrayToPILImage, - PILImageToNDArray, - RandomResizedCrop, - RandomSolarization, -) -from otx.api.entities.annotation import ( - Annotation, - AnnotationSceneEntity, - AnnotationSceneKind, -) -from otx.api.entities.dataset_item import DatasetItemEntity -from otx.api.entities.image import Image -from otx.api.entities.label import Domain, LabelEntity -from otx.api.entities.scored_label import ScoredLabel -from otx.api.entities.shapes.rectangle import Rectangle -from tests.test_suite.e2e_test_system import e2e_pytest_unit - - -def label_entity(name="test label") -> LabelEntity: - return LabelEntity(name=name, domain=Domain.SEGMENTATION) - - -def dataset_item() -> DatasetItemEntity: - image: Image = Image(data=np.random.randint(low=0, high=255, size=(10, 16, 3))) - annotation: Annotation = Annotation(shape=Rectangle.generate_full_box(), labels=[ScoredLabel(label_entity())]) - annotation_scene: AnnotationSceneEntity = AnnotationSceneEntity( - annotations=[annotation], kind=AnnotationSceneKind.ANNOTATION - ) - return DatasetItemEntity(media=image, annotation_scene=annotation_scene) - - -class TestLoadAnnotationFromOTXDataset: - @pytest.fixture(autouse=True) - def setUp(self) -> None: - - self.dataset_item: DatasetItemEntity = dataset_item() - self.results: dict = { - "dataset_item": self.dataset_item, - "ann_info": {"labels": [label_entity("class_1")]}, - "seg_fields": [], - } - self.pipeline: LoadAnnotationFromOTXDataset = LoadAnnotationFromOTXDataset() - - @e2e_pytest_unit - def test_call(self) -> None: - loaded_annotations: dict = self.pipeline(self.results) - assert "gt_semantic_seg" in loaded_annotations - assert loaded_annotations["dataset_item"] == self.dataset_item - - -class TestNDArrayToPILImage: - @pytest.fixture(autouse=True) - def setUp(self) -> None: - self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} - self.nd_array_to_pil_image: NDArrayToPILImage = NDArrayToPILImage(keys=["img"]) - - @e2e_pytest_unit - def test_call(self) -> None: - converted_img: dict = self.nd_array_to_pil_image(self.results) - assert "img" in converted_img - assert isinstance(converted_img["img"], PIL.Image.Image) - - @e2e_pytest_unit - def test_repr(self) -> None: - assert str(self.nd_array_to_pil_image) == "NDArrayToPILImage" - - -class TestPILImageToNDArray: - @pytest.fixture(autouse=True) - def setUp(self) -> None: - self.results: dict = {"img": PIL.Image.new("RGB", (3, 3))} - self.pil_image_to_nd_array: PILImageToNDArray = PILImageToNDArray(keys=["img"]) - - @e2e_pytest_unit - def test_call(self) -> None: - converted_array: dict = self.pil_image_to_nd_array(self.results) - assert "img" in converted_array - assert isinstance(converted_array["img"], np.ndarray) - - @e2e_pytest_unit - def test_repr(self) -> None: - assert str(self.pil_image_to_nd_array) == "PILImageToNDArray" - - -class TestRandomResizedCrop: - @pytest.fixture(autouse=True) - def setUp(self) -> None: - self.results: dict = {"img": PIL.Image.new("RGB", (10, 16)), "img_shape": (10, 16), "ori_shape": (10, 16)} - self.random_resized_crop: RandomResizedCrop = RandomResizedCrop((5, 5), (0.5, 1.0)) - - @e2e_pytest_unit - def test_call(self) -> None: - cropped_img: dict = self.random_resized_crop(self.results) - assert cropped_img["img_shape"] == (5, 5) - assert cropped_img["ori_shape"] == (10, 16) - - -class TestRandomSolarization: - @pytest.fixture(autouse=True) - def setUp(self) -> None: - self.results: dict = {"img": np.random.randint(0, 255, (3, 3, 3), dtype=np.uint8)} - self.random_solarization: RandomSolarization = RandomSolarization(p=1.0) - - @e2e_pytest_unit - def test_call(self) -> None: - solarized: dict = self.random_solarization(self.results) - assert "img" in solarized - assert isinstance(solarized["img"], np.ndarray) - - @e2e_pytest_unit - def test_repr(self) -> None: - assert str(self.random_solarization) == "RandomSolarization" diff --git a/otx/mpa/modules/ov/models/mmseg/__init__.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py similarity index 55% rename from otx/mpa/modules/ov/models/mmseg/__init__.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py index f5601bf53ac..2e7d4985d06 100644 --- a/otx/mpa/modules/ov/models/mmseg/__init__.py +++ b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/__init__.py @@ -1,6 +1,4 @@ +"""Test for otx.algorithms.segmentation.adapters.mmseg.utils.""" # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # - -# flake8: noqa -from . import backbones, decode_heads diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_config_utils.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_config_utils.py similarity index 100% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_config_utils.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_config_utils.py diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_config_utils_params_validation.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_config_utils_params_validation.py similarity index 100% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_config_utils_params_validation.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_config_utils_params_validation.py diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_data_utils.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py similarity index 100% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_data_utils.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils.py diff --git a/tests/unit/algorithms/segmentation/adapters/mmseg/test_data_utils_params_validation.py b/tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils_params_validation.py similarity index 100% rename from tests/unit/algorithms/segmentation/adapters/mmseg/test_data_utils_params_validation.py rename to tests/unit/algorithms/segmentation/adapters/mmseg/utils/test_data_utils_params_validation.py diff --git a/tests/unit/mpa/modules/datasets/pipelines/transforms/test_seg_custom_pipelines.py b/tests/unit/mpa/modules/datasets/pipelines/transforms/test_seg_custom_pipelines.py deleted file mode 100644 index c966164a992..00000000000 --- a/tests/unit/mpa/modules/datasets/pipelines/transforms/test_seg_custom_pipelines.py +++ /dev/null @@ -1,103 +0,0 @@ -import numpy as np -import pytest -import torch -from mmcv.parallel import DataContainer - -from otx.mpa.modules.datasets.pipelines.transforms.seg_custom_pipelines import ( - BranchImage, - DefaultFormatBundle, - Normalize, -) -from tests.test_suite.e2e_test_system import e2e_pytest_unit - - -class TestNormalize: - @e2e_pytest_unit - @pytest.mark.parametrize( - "mean,std,to_rgb,expected", - [ - (1.0, 1.0, True, np.array([[[1.0, 0.0, 0.0]]], dtype=np.float32)), - (1.0, 1.0, False, np.array([[[-1.0, 0.0, 0.0]]], dtype=np.float32)), - ], - ) - def test_call(self, mean: float, std: float, to_rgb: bool, expected: np.array) -> None: - """Test __call__.""" - normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) - inputs = dict(img=np.arange(3).reshape(1, 1, 3)) - - results = normalize(inputs.copy()) - - assert "img" in results - assert "img_norm_cfg" in results - assert np.all(results["img"] == expected) - - @e2e_pytest_unit - @pytest.mark.parametrize("mean,std,to_rgb", [(1.0, 1.0, True)]) - def test_repr(self, mean: float, std: float, to_rgb: bool) -> None: - """Test __repr__.""" - normalize = Normalize(mean=mean, std=std, to_rgb=to_rgb) - - assert repr(normalize) == normalize.__class__.__name__ + f"(mean={mean}, std={std}, to_rgb=" f"{to_rgb})" - - -class TestDefaultFormatBundle: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.default_format_bundle = DefaultFormatBundle() - - @e2e_pytest_unit - @pytest.mark.parametrize("img", [np.ones((1, 1)), np.ones((1, 1, 1)), np.ones((1, 1, 1, 1))]) - @pytest.mark.parametrize("gt_semantic_seg,pixel_weights", [(np.ones((1, 1)), np.ones((1, 1)))]) - def test_call(self, img: np.array, gt_semantic_seg: np.array, pixel_weights: np.array) -> None: - """Test __call__.""" - inputs = dict(img=img, gt_semantic_seg=gt_semantic_seg, pixel_weights=pixel_weights) - - results = self.default_format_bundle(inputs.copy()) - - assert isinstance(results, dict) - assert "img" in results - assert isinstance(results["img"], DataContainer) - assert len(results["img"].data.shape) >= 3 - assert results["img"].data.dtype == torch.float32 - assert "gt_semantic_seg" in results - assert len(results["gt_semantic_seg"].data.shape) == len(inputs["gt_semantic_seg"].shape) + 1 - assert results["gt_semantic_seg"].data.dtype == torch.int64 - assert "pixel_weights" in results - assert len(results["pixel_weights"].data.shape) == len(inputs["pixel_weights"].shape) + 1 - assert results["pixel_weights"].data.dtype == torch.float32 - - @e2e_pytest_unit - @pytest.mark.parametrize("img", [np.ones((1,))]) - def test_call_invalid_shape(self, img: np.array): - inputs = dict(img=img) - - with pytest.raises(ValueError): - self.default_format_bundle(inputs.copy()) - - @e2e_pytest_unit - def test_repr(self) -> None: - """Test __repr__.""" - assert repr(self.default_format_bundle) == self.default_format_bundle.__class__.__name__ - - -class TestBranchImage: - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.branch_image = BranchImage(key_map={"key1": "key2"}) - - @e2e_pytest_unit - def test_call(self) -> None: - """Test __call__.""" - inputs = dict(key1="key1", img_fields=["key1"]) - - results = self.branch_image(inputs.copy()) - - assert isinstance(results, dict) - assert "key2" in results - assert results["key1"] == results["key2"] - assert "key2" in results["img_fields"] - - @e2e_pytest_unit - def test_repr(self) -> None: - """Test __repr__.""" - assert repr(self.branch_image) == self.branch_image.__class__.__name__