diff --git a/.gitignore b/.gitignore index 33052c0bd7..ba0b6d5a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,10 @@ instance/ # Scrapy stuff: .scrapy +# Sphinx documentation +docs/build/ +docs/source/_build/ + # PyBuilder .pybuilder/ target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 039f89777c..90a2cc69d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,4 +61,4 @@ You accept that your contributions will be licensed under the [Apache-2.0 Licens ## References -This document was adapted from [here](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62). \ No newline at end of file +This document was adapted from [here](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62). diff --git a/anomalib/config/__init__.py b/anomalib/config/__init__.py index 628ad792b6..2becba50cf 100644 --- a/anomalib/config/__init__.py +++ b/anomalib/config/__init__.py @@ -1,4 +1,4 @@ -"""Utilities to get configurable parameters.""" +"""Utilities for parsing model configuration.""" # Copyright (C) 2020 Intel Corporation # @@ -14,10 +14,6 @@ # See the License for the specific language governing permissions # and limitations under the License. -from .config import ( - get_configurable_parameters, - update_input_size_config, - update_nncf_config, -) +from .config import get_configurable_parameters, update_nncf_config -__all__ = ["get_configurable_parameters", "update_input_size_config", "update_nncf_config"] +__all__ = ["get_configurable_parameters", "update_nncf_config"] diff --git a/anomalib/data/inference.py b/anomalib/data/inference.py index 094d4d72bc..b775a17fff 100644 --- a/anomalib/data/inference.py +++ b/anomalib/data/inference.py @@ -20,8 +20,8 @@ import albumentations as A from torch.utils.data.dataset import Dataset -from anomalib.data.transforms import PreProcessor from anomalib.data.utils import get_image_filenames, read_image +from anomalib.pre_processing import PreProcessor class InferenceDataset(Dataset): diff --git a/anomalib/data/mvtec.py b/anomalib/data/mvtec.py index ccf8ca9d2f..e29e02b8b7 100644 --- a/anomalib/data/mvtec.py +++ b/anomalib/data/mvtec.py @@ -41,9 +41,8 @@ from torchvision.datasets.folder import VisionDataset from anomalib.data.inference import InferenceDataset -from anomalib.data.transforms import PreProcessor -from anomalib.data.utils import read_image -from anomalib.utils.download_progress_bar import DownloadProgressBar +from anomalib.data.utils import DownloadProgressBar, read_image +from anomalib.pre_processing import PreProcessor logger = logging.getLogger(name="Dataset: MVTec") logger.setLevel(logging.DEBUG) diff --git a/anomalib/data/utils/__init__.py b/anomalib/data/utils/__init__.py new file mode 100644 index 0000000000..01c8f98459 --- /dev/null +++ b/anomalib/data/utils/__init__.py @@ -0,0 +1,20 @@ +"""Helper utilities for data.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .download_progress_bar import DownloadProgressBar +from .image import get_image_filenames, read_image + +__all__ = ["get_image_filenames", "read_image", "DownloadProgressBar"] diff --git a/anomalib/utils/download_progress_bar.py b/anomalib/data/utils/download_progress_bar.py similarity index 100% rename from anomalib/utils/download_progress_bar.py rename to anomalib/data/utils/download_progress_bar.py diff --git a/anomalib/data/utils.py b/anomalib/data/utils/image.py similarity index 98% rename from anomalib/data/utils.py rename to anomalib/data/utils/image.py index e403d787ae..892ee1d3bc 100644 --- a/anomalib/data/utils.py +++ b/anomalib/data/utils/image.py @@ -1,4 +1,4 @@ -"""Dataset Utils.""" +"""Image Utils.""" # Copyright (C) 2020 Intel Corporation # diff --git a/anomalib/deploy/__init__.py b/anomalib/deploy/__init__.py index ac5b879bc4..27298410e5 100644 --- a/anomalib/deploy/__init__.py +++ b/anomalib/deploy/__init__.py @@ -1,4 +1,4 @@ -"""Utilities for inference and deployment.""" +"""Functions for Inference and model deployment.""" # Copyright (C) 2020 Intel Corporation # @@ -13,3 +13,8 @@ # WITHOUT 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 .inferencers import OpenVINOInferencer, TorchInferencer +from .optimize import export_convert, get_model_metadata + +__all__ = ["OpenVINOInferencer", "TorchInferencer", "export_convert", "get_model_metadata"] diff --git a/anomalib/deploy/inferencers/base.py b/anomalib/deploy/inferencers/base.py index b7fc0082bb..c9d0270e2d 100644 --- a/anomalib/deploy/inferencers/base.py +++ b/anomalib/deploy/inferencers/base.py @@ -23,10 +23,12 @@ from torch import Tensor from anomalib.data.utils import read_image -from anomalib.utils.normalization.cdf import normalize as normalize_cdf -from anomalib.utils.normalization.cdf import standardize -from anomalib.utils.normalization.min_max import normalize as normalize_min_max -from anomalib.utils.post_process import superimpose_anomaly_map +from anomalib.post_processing import superimpose_anomaly_map +from anomalib.post_processing.normalization.cdf import normalize as normalize_cdf +from anomalib.post_processing.normalization.cdf import standardize +from anomalib.post_processing.normalization.min_max import ( + normalize as normalize_min_max, +) class Inferencer(ABC): diff --git a/anomalib/deploy/inferencers/openvino.py b/anomalib/deploy/inferencers/openvino.py index 999caf1ed7..52f50e98a5 100644 --- a/anomalib/deploy/inferencers/openvino.py +++ b/anomalib/deploy/inferencers/openvino.py @@ -22,7 +22,7 @@ from omegaconf import DictConfig, ListConfig from openvino.inference_engine import IECore # pylint: disable=no-name-in-module -from anomalib.data.transforms.pre_process import PreProcessor +from anomalib.pre_processing import PreProcessor from .base import Inferencer diff --git a/anomalib/deploy/inferencers/torch.py b/anomalib/deploy/inferencers/torch.py index eb6efc1f46..a6c4c04e07 100644 --- a/anomalib/deploy/inferencers/torch.py +++ b/anomalib/deploy/inferencers/torch.py @@ -23,10 +23,10 @@ from omegaconf import DictConfig, ListConfig from torch import Tensor -from anomalib.core.model import AnomalyModule -from anomalib.data.transforms.pre_process import PreProcessor from anomalib.deploy.optimize import get_model_metadata from anomalib.models import get_model +from anomalib.models.components import AnomalyModule +from anomalib.pre_processing import PreProcessor from .base import Inferencer diff --git a/anomalib/deploy/optimize.py b/anomalib/deploy/optimize.py index 7353420323..f38c8a21b4 100644 --- a/anomalib/deploy/optimize.py +++ b/anomalib/deploy/optimize.py @@ -24,7 +24,7 @@ import torch from torch import Tensor -from anomalib.core.model.anomaly_module import AnomalyModule +from anomalib.models.components import AnomalyModule def get_model_metadata(model: AnomalyModule) -> Dict[str, Tensor]: diff --git a/anomalib/models/__init__.py b/anomalib/models/__init__.py index 71fd89a040..1a8c72eee8 100644 --- a/anomalib/models/__init__.py +++ b/anomalib/models/__init__.py @@ -21,7 +21,7 @@ from omegaconf import DictConfig, ListConfig from torch import load -from anomalib.core.model import AnomalyModule +from anomalib.models.components import AnomalyModule def get_model(config: Union[DictConfig, ListConfig]) -> AnomalyModule: diff --git a/anomalib/models/cflow/model.py b/anomalib/models/cflow/model.py index d9385eaaf8..d7f90207e3 100644 --- a/anomalib/models/cflow/model.py +++ b/anomalib/models/cflow/model.py @@ -28,9 +28,8 @@ from pytorch_lightning.callbacks import EarlyStopping from torch import Tensor, nn, optim -from anomalib.core.model import AnomalyModule -from anomalib.core.model.feature_extractor import FeatureExtractor from anomalib.models.cflow.backbone import cflow_head, positional_encoding_2d +from anomalib.models.components import AnomalyModule, FeatureExtractor __all__ = ["AnomalyMapGenerator", "CflowModel", "CflowLightning"] diff --git a/anomalib/models/components/__init__.py b/anomalib/models/components/__init__.py new file mode 100644 index 0000000000..65d4bc6342 --- /dev/null +++ b/anomalib/models/components/__init__.py @@ -0,0 +1,32 @@ +"""Components used within the models.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +from .base import AnomalyModule, DynamicBufferModule +from .dimensionality_reduction import PCA, SparseRandomProjection +from .feature_extractors import FeatureExtractor +from .sampling import KCenterGreedy +from .stats import GaussianKDE, MultiVariateGaussian + +__all__ = [ + "AnomalyModule", + "DynamicBufferModule", + "PCA", + "SparseRandomProjection", + "FeatureExtractor", + "KCenterGreedy", + "GaussianKDE", + "MultiVariateGaussian", +] diff --git a/anomalib/models/components/base/__init__.py b/anomalib/models/components/base/__init__.py new file mode 100644 index 0000000000..fdcf2bee5a --- /dev/null +++ b/anomalib/models/components/base/__init__.py @@ -0,0 +1,20 @@ +"""Base classes for all anomaly components.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .anomaly_module import AnomalyModule +from .dynamic_module import DynamicBufferModule + +__all__ = ["AnomalyModule", "DynamicBufferModule"] diff --git a/anomalib/core/model/anomaly_module.py b/anomalib/models/components/base/anomaly_module.py similarity index 99% rename from anomalib/core/model/anomaly_module.py rename to anomalib/models/components/base/anomaly_module.py index 8cea7d2ec7..4a20a53ddc 100644 --- a/anomalib/core/model/anomaly_module.py +++ b/anomalib/models/components/base/anomaly_module.py @@ -23,7 +23,7 @@ from torch import Tensor, nn from torchmetrics import F1, MetricCollection -from anomalib.core.metrics import ( +from anomalib.utils.metrics import ( AUROC, AdaptiveThreshold, AnomalyScoreDistribution, diff --git a/anomalib/core/model/dynamic_module.py b/anomalib/models/components/base/dynamic_module.py similarity index 100% rename from anomalib/core/model/dynamic_module.py rename to anomalib/models/components/base/dynamic_module.py diff --git a/anomalib/models/components/dimensionality_reduction/__init__.py b/anomalib/models/components/dimensionality_reduction/__init__.py new file mode 100644 index 0000000000..b31a5ea825 --- /dev/null +++ b/anomalib/models/components/dimensionality_reduction/__init__.py @@ -0,0 +1,20 @@ +"""Algorithms for decomposition and dimensionality reduction.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .pca import PCA +from .random_projection import SparseRandomProjection + +__all__ = ["PCA", "SparseRandomProjection"] diff --git a/anomalib/core/model/pca.py b/anomalib/models/components/dimensionality_reduction/pca.py similarity index 98% rename from anomalib/core/model/pca.py rename to anomalib/models/components/dimensionality_reduction/pca.py index 9edc82be03..ef7a22f3c0 100644 --- a/anomalib/core/model/pca.py +++ b/anomalib/models/components/dimensionality_reduction/pca.py @@ -19,7 +19,7 @@ import torch from torch import Tensor -from anomalib.core.model.dynamic_module import DynamicBufferModule +from anomalib.models.components.base import DynamicBufferModule class PCA(DynamicBufferModule): diff --git a/anomalib/core/model/random_projection.py b/anomalib/models/components/dimensionality_reduction/random_projection.py similarity index 100% rename from anomalib/core/model/random_projection.py rename to anomalib/models/components/dimensionality_reduction/random_projection.py diff --git a/anomalib/core/model/__init__.py b/anomalib/models/components/feature_extractors/__init__.py similarity index 84% rename from anomalib/core/model/__init__.py rename to anomalib/models/components/feature_extractors/__init__.py index cf27494ddb..984b5eb3a2 100644 --- a/anomalib/core/model/__init__.py +++ b/anomalib/models/components/feature_extractors/__init__.py @@ -1,4 +1,4 @@ -"""Anomalib Core Model Entities.""" +"""Feature extractors.""" # Copyright (C) 2020 Intel Corporation # @@ -14,6 +14,6 @@ # See the License for the specific language governing permissions # and limitations under the License. -from .anomaly_module import AnomalyModule +from .feature_extractor import FeatureExtractor -__all__ = ["AnomalyModule"] +__all__ = ["FeatureExtractor"] diff --git a/anomalib/core/model/feature_extractor.py b/anomalib/models/components/feature_extractors/feature_extractor.py similarity index 100% rename from anomalib/core/model/feature_extractor.py rename to anomalib/models/components/feature_extractors/feature_extractor.py diff --git a/anomalib/models/components/sampling/__init__.py b/anomalib/models/components/sampling/__init__.py new file mode 100644 index 0000000000..26dac37289 --- /dev/null +++ b/anomalib/models/components/sampling/__init__.py @@ -0,0 +1,19 @@ +"""Sampling methods.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .k_center_greedy import KCenterGreedy + +__all__ = ["KCenterGreedy"] diff --git a/anomalib/core/model/k_center_greedy.py b/anomalib/models/components/sampling/k_center_greedy.py similarity index 98% rename from anomalib/core/model/k_center_greedy.py rename to anomalib/models/components/sampling/k_center_greedy.py index 49e6e8c76f..1703c74149 100644 --- a/anomalib/core/model/k_center_greedy.py +++ b/anomalib/models/components/sampling/k_center_greedy.py @@ -11,7 +11,7 @@ import torch.nn.functional as F from torch import Tensor -from anomalib.core.model.random_projection import SparseRandomProjection +from anomalib.models.components.dimensionality_reduction import SparseRandomProjection class KCenterGreedy: diff --git a/anomalib/models/components/stats/__init__.py b/anomalib/models/components/stats/__init__.py new file mode 100644 index 0000000000..829c0f75b2 --- /dev/null +++ b/anomalib/models/components/stats/__init__.py @@ -0,0 +1,20 @@ +"""Statistical functions.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .kde import GaussianKDE +from .multi_variate_gaussian import MultiVariateGaussian + +__all__ = ["GaussianKDE", "MultiVariateGaussian"] diff --git a/anomalib/core/model/kde.py b/anomalib/models/components/stats/kde.py similarity index 97% rename from anomalib/core/model/kde.py rename to anomalib/models/components/stats/kde.py index 132dbd4b1e..aa327c26f2 100644 --- a/anomalib/core/model/kde.py +++ b/anomalib/models/components/stats/kde.py @@ -20,7 +20,7 @@ import torch from torch import Tensor -from anomalib.core.model.dynamic_module import DynamicBufferModule +from anomalib.models.components.base import DynamicBufferModule class GaussianKDE(DynamicBufferModule): diff --git a/anomalib/core/model/multi_variate_gaussian.py b/anomalib/models/components/stats/multi_variate_gaussian.py similarity index 100% rename from anomalib/core/model/multi_variate_gaussian.py rename to anomalib/models/components/stats/multi_variate_gaussian.py diff --git a/anomalib/models/dfkde/model.py b/anomalib/models/dfkde/model.py index e14b33c800..8edb3a3c19 100644 --- a/anomalib/models/dfkde/model.py +++ b/anomalib/models/dfkde/model.py @@ -22,9 +22,9 @@ from omegaconf.listconfig import ListConfig from torch import Tensor -from anomalib.core.model import AnomalyModule -from anomalib.core.model.feature_extractor import FeatureExtractor -from anomalib.models.dfkde.normality_model import NormalityModel +from anomalib.models.components import AnomalyModule, FeatureExtractor + +from .normality_model import NormalityModel class DfkdeLightning(AnomalyModule): diff --git a/anomalib/models/dfkde/normality_model.py b/anomalib/models/dfkde/normality_model.py index 89f6be4eb5..b9302805a4 100644 --- a/anomalib/models/dfkde/normality_model.py +++ b/anomalib/models/dfkde/normality_model.py @@ -20,8 +20,7 @@ import torch from torch import Tensor, nn -from anomalib.core.model.kde import GaussianKDE -from anomalib.core.model.pca import PCA +from anomalib.models.components import PCA, GaussianKDE class NormalityModel(nn.Module): diff --git a/anomalib/models/dfm/dfm_model.py b/anomalib/models/dfm/dfm_model.py index cd005fbb3f..90aea4fb8b 100644 --- a/anomalib/models/dfm/dfm_model.py +++ b/anomalib/models/dfm/dfm_model.py @@ -19,8 +19,7 @@ import torch from torch import Tensor, nn -from anomalib.core.model.dynamic_module import DynamicBufferModule -from anomalib.core.model.pca import PCA +from anomalib.models.components import PCA, DynamicBufferModule class SingleClassGaussian(DynamicBufferModule): diff --git a/anomalib/models/dfm/model.py b/anomalib/models/dfm/model.py index 7a4a9f1683..3592fb08a5 100644 --- a/anomalib/models/dfm/model.py +++ b/anomalib/models/dfm/model.py @@ -21,9 +21,9 @@ from omegaconf import DictConfig, ListConfig from torch import Tensor -from anomalib.core.model import AnomalyModule -from anomalib.core.model.feature_extractor import FeatureExtractor -from anomalib.models.dfm.dfm_model import DFMModel +from anomalib.models.components import AnomalyModule, FeatureExtractor + +from .dfm_model import DFMModel class DfmLightning(AnomalyModule): diff --git a/anomalib/models/ganomaly/model.py b/anomalib/models/ganomaly/model.py index 121179e9a9..163bf2fef3 100644 --- a/anomalib/models/ganomaly/model.py +++ b/anomalib/models/ganomaly/model.py @@ -24,7 +24,7 @@ from pytorch_lightning.callbacks import EarlyStopping from torch import Tensor, nn, optim -from anomalib.core.model import AnomalyModule +from anomalib.models.components import AnomalyModule from .torch_model import Discriminator, Generator diff --git a/anomalib/models/padim/model.py b/anomalib/models/padim/model.py index ca0343a5c2..8597f31d54 100644 --- a/anomalib/models/padim/model.py +++ b/anomalib/models/padim/model.py @@ -27,10 +27,12 @@ from omegaconf import DictConfig, ListConfig from torch import Tensor, nn -from anomalib.core.model import AnomalyModule -from anomalib.core.model.feature_extractor import FeatureExtractor -from anomalib.core.model.multi_variate_gaussian import MultiVariateGaussian -from anomalib.data.tiler import Tiler +from anomalib.models.components import ( + AnomalyModule, + FeatureExtractor, + MultiVariateGaussian, +) +from anomalib.pre_processing import Tiler __all__ = ["PadimLightning"] diff --git a/anomalib/models/patchcore/model.py b/anomalib/models/patchcore/model.py index b497b831ec..d98e062a25 100644 --- a/anomalib/models/patchcore/model.py +++ b/anomalib/models/patchcore/model.py @@ -26,11 +26,13 @@ from omegaconf import ListConfig from torch import Tensor, nn -from anomalib.core.model import AnomalyModule -from anomalib.core.model.dynamic_module import DynamicBufferModule -from anomalib.core.model.feature_extractor import FeatureExtractor -from anomalib.core.model.k_center_greedy import KCenterGreedy -from anomalib.data.tiler import Tiler +from anomalib.models.components import ( + AnomalyModule, + DynamicBufferModule, + FeatureExtractor, + KCenterGreedy, +) +from anomalib.pre_processing import Tiler class AnomalyMapGenerator: diff --git a/anomalib/models/stfpm/model.py b/anomalib/models/stfpm/model.py index 37aacba0b4..ed5e7f2b2f 100644 --- a/anomalib/models/stfpm/model.py +++ b/anomalib/models/stfpm/model.py @@ -26,9 +26,8 @@ from pytorch_lightning.callbacks import EarlyStopping from torch import Tensor, nn, optim -from anomalib.core.model import AnomalyModule -from anomalib.core.model.feature_extractor import FeatureExtractor -from anomalib.data.tiler import Tiler +from anomalib.models.components import AnomalyModule, FeatureExtractor +from anomalib.pre_processing import Tiler __all__ = ["Loss", "AnomalyMapGenerator", "STFPMModel", "StfpmLightning"] diff --git a/anomalib/post_processing/__init__.py b/anomalib/post_processing/__init__.py new file mode 100644 index 0000000000..11b0f0092c --- /dev/null +++ b/anomalib/post_processing/__init__.py @@ -0,0 +1,24 @@ +"""Methods to help post-process raw model outputs.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .post_process import ( + anomaly_map_to_color_map, + compute_mask, + superimpose_anomaly_map, +) +from .visualizer import Visualizer + +__all__ = ["anomaly_map_to_color_map", "superimpose_anomaly_map", "compute_mask", "Visualizer"] diff --git a/anomalib/utils/normalization/__init__.py b/anomalib/post_processing/normalization/__init__.py similarity index 100% rename from anomalib/utils/normalization/__init__.py rename to anomalib/post_processing/normalization/__init__.py diff --git a/anomalib/utils/normalization/cdf.py b/anomalib/post_processing/normalization/cdf.py similarity index 100% rename from anomalib/utils/normalization/cdf.py rename to anomalib/post_processing/normalization/cdf.py diff --git a/anomalib/utils/normalization/min_max.py b/anomalib/post_processing/normalization/min_max.py similarity index 100% rename from anomalib/utils/normalization/min_max.py rename to anomalib/post_processing/normalization/min_max.py diff --git a/anomalib/utils/post_process.py b/anomalib/post_processing/post_process.py similarity index 100% rename from anomalib/utils/post_process.py rename to anomalib/post_processing/post_process.py diff --git a/anomalib/utils/visualizer.py b/anomalib/post_processing/visualizer.py similarity index 100% rename from anomalib/utils/visualizer.py rename to anomalib/post_processing/visualizer.py diff --git a/anomalib/pre_processing/__init__.py b/anomalib/pre_processing/__init__.py new file mode 100644 index 0000000000..6313d91ef1 --- /dev/null +++ b/anomalib/pre_processing/__init__.py @@ -0,0 +1,20 @@ +"""Utilities for pre-processing the input before passing to the model.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 .pre_process import PreProcessor +from .tiler import Tiler + +__all__ = ["PreProcessor", "Tiler"] diff --git a/anomalib/data/transforms/pre_process.py b/anomalib/pre_processing/pre_process.py similarity index 100% rename from anomalib/data/transforms/pre_process.py rename to anomalib/pre_processing/pre_process.py diff --git a/anomalib/data/tiler.py b/anomalib/pre_processing/tiler.py similarity index 100% rename from anomalib/data/tiler.py rename to anomalib/pre_processing/tiler.py diff --git a/anomalib/data/transforms/__init__.py b/anomalib/pre_processing/transforms/__init__.py similarity index 87% rename from anomalib/data/transforms/__init__.py rename to anomalib/pre_processing/transforms/__init__.py index 5d5db735ac..66cedf5d45 100644 --- a/anomalib/data/transforms/__init__.py +++ b/anomalib/pre_processing/transforms/__init__.py @@ -15,6 +15,5 @@ # and limitations under the License. from .custom import Denormalize, ToNumpy -from .pre_process import PreProcessor -__all__ = ["Denormalize", "PreProcessor", "ToNumpy"] +__all__ = ["Denormalize", "ToNumpy"] diff --git a/anomalib/data/transforms/custom.py b/anomalib/pre_processing/transforms/custom.py similarity index 100% rename from anomalib/data/transforms/custom.py rename to anomalib/pre_processing/transforms/custom.py diff --git a/anomalib/core/callbacks/__init__.py b/anomalib/utils/callbacks/__init__.py similarity index 84% rename from anomalib/core/callbacks/__init__.py rename to anomalib/utils/callbacks/__init__.py index aaef376b45..a179930859 100644 --- a/anomalib/core/callbacks/__init__.py +++ b/anomalib/utils/callbacks/__init__.py @@ -1,5 +1,19 @@ """Callbacks for Anomalib models.""" +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + import os from importlib import import_module from typing import List, Union @@ -73,7 +87,7 @@ def get_callbacks(config: Union[ListConfig, DictConfig]) -> List[Callback]: if config.optimization.nncf.apply: # NNCF wraps torch's jit which conflicts with kornia's jit calls. # Hence, nncf is imported only when required - nncf_module = import_module("anomalib.core.callbacks.nncf_callback") + nncf_module = import_module("anomalib.utils.callbacks.nncf_callback") nncf_callback = getattr(nncf_module, "NNCFCallback") callbacks.append( nncf_callback( diff --git a/anomalib/core/callbacks/cdf_normalization.py b/anomalib/utils/callbacks/cdf_normalization.py similarity index 87% rename from anomalib/core/callbacks/cdf_normalization.py rename to anomalib/utils/callbacks/cdf_normalization.py index 1786591139..be072be236 100644 --- a/anomalib/core/callbacks/cdf_normalization.py +++ b/anomalib/utils/callbacks/cdf_normalization.py @@ -1,4 +1,19 @@ """Anomaly Score Normalization Callback.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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, Optional import pytorch_lightning as pl @@ -7,7 +22,7 @@ from torch.distributions import LogNormal from anomalib.models import get_model -from anomalib.utils.normalization.cdf import normalize, standardize +from anomalib.post_processing.normalization.cdf import normalize, standardize class CdfNormalizationCallback(Callback): diff --git a/anomalib/core/callbacks/compress.py b/anomalib/utils/callbacks/compress.py similarity index 67% rename from anomalib/core/callbacks/compress.py rename to anomalib/utils/callbacks/compress.py index e712ae3e54..48d9042582 100644 --- a/anomalib/core/callbacks/compress.py +++ b/anomalib/utils/callbacks/compress.py @@ -1,11 +1,26 @@ """Callback that compresses a trained model by first exporting to .onnx format, and then converting to OpenVINO IR.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + import os from typing import Tuple, cast from pytorch_lightning import Callback, LightningModule -from anomalib.core.model.anomaly_module import AnomalyModule -from anomalib.deploy.optimize import export_convert +from anomalib.deploy import export_convert +from anomalib.models.components import AnomalyModule class CompressModelCallback(Callback): diff --git a/anomalib/core/callbacks/min_max_normalization.py b/anomalib/utils/callbacks/min_max_normalization.py similarity index 97% rename from anomalib/core/callbacks/min_max_normalization.py rename to anomalib/utils/callbacks/min_max_normalization.py index e3406d3be9..acdbb03cb7 100644 --- a/anomalib/core/callbacks/min_max_normalization.py +++ b/anomalib/utils/callbacks/min_max_normalization.py @@ -20,7 +20,7 @@ from pytorch_lightning import Callback from pytorch_lightning.utilities.types import STEP_OUTPUT -from anomalib.utils.normalization.min_max import normalize +from anomalib.post_processing.normalization.min_max import normalize class MinMaxNormalizationCallback(Callback): diff --git a/anomalib/core/callbacks/model_loader.py b/anomalib/utils/callbacks/model_loader.py similarity index 100% rename from anomalib/core/callbacks/model_loader.py rename to anomalib/utils/callbacks/model_loader.py diff --git a/anomalib/core/callbacks/nncf_callback.py b/anomalib/utils/callbacks/nncf_callback.py similarity index 100% rename from anomalib/core/callbacks/nncf_callback.py rename to anomalib/utils/callbacks/nncf_callback.py diff --git a/anomalib/core/callbacks/save_to_csv.py b/anomalib/utils/callbacks/save_to_csv.py similarity index 95% rename from anomalib/core/callbacks/save_to_csv.py rename to anomalib/utils/callbacks/save_to_csv.py index ad7166a929..327788eb1d 100644 --- a/anomalib/core/callbacks/save_to_csv.py +++ b/anomalib/utils/callbacks/save_to_csv.py @@ -5,7 +5,7 @@ import pandas as pd from pytorch_lightning import Callback, Trainer -from anomalib.core.model import AnomalyModule +from anomalib.models.components import AnomalyModule class SaveToCSVCallback(Callback): diff --git a/anomalib/core/callbacks/timer.py b/anomalib/utils/callbacks/timer.py similarity index 100% rename from anomalib/core/callbacks/timer.py rename to anomalib/utils/callbacks/timer.py diff --git a/anomalib/core/callbacks/visualizer_callback.py b/anomalib/utils/callbacks/visualizer_callback.py similarity index 86% rename from anomalib/core/callbacks/visualizer_callback.py rename to anomalib/utils/callbacks/visualizer_callback.py index c15d4e1180..360dee0781 100644 --- a/anomalib/core/callbacks/visualizer_callback.py +++ b/anomalib/utils/callbacks/visualizer_callback.py @@ -1,4 +1,19 @@ """Visualizer Callback.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + from pathlib import Path from typing import Any, Optional from warnings import warn @@ -8,12 +23,11 @@ from pytorch_lightning.utilities.types import STEP_OUTPUT from skimage.segmentation import mark_boundaries -from anomalib import loggers -from anomalib.core.model import AnomalyModule -from anomalib.data.transforms import Denormalize -from anomalib.loggers.wandb import AnomalibWandbLogger -from anomalib.utils.post_process import compute_mask, superimpose_anomaly_map -from anomalib.utils.visualizer import Visualizer +from anomalib.models.components import AnomalyModule +from anomalib.post_processing import Visualizer, compute_mask, superimpose_anomaly_map +from anomalib.pre_processing.transforms import Denormalize +from anomalib.utils import loggers +from anomalib.utils.loggers import AnomalibWandbLogger class VisualizerCallback(Callback): diff --git a/anomalib/loggers/__init__.py b/anomalib/utils/loggers/__init__.py similarity index 100% rename from anomalib/loggers/__init__.py rename to anomalib/utils/loggers/__init__.py diff --git a/anomalib/loggers/base.py b/anomalib/utils/loggers/base.py similarity index 100% rename from anomalib/loggers/base.py rename to anomalib/utils/loggers/base.py diff --git a/anomalib/loggers/tensorboard.py b/anomalib/utils/loggers/tensorboard.py similarity index 98% rename from anomalib/loggers/tensorboard.py rename to anomalib/utils/loggers/tensorboard.py index 2d7f637855..56695babd6 100644 --- a/anomalib/loggers/tensorboard.py +++ b/anomalib/utils/loggers/tensorboard.py @@ -38,7 +38,7 @@ class AnomalibTensorBoardLogger(ImageLoggerBase, TensorBoardLogger): Example: >>> from pytorch_lightning import Trainer - >>> from anomalib.loggers.tensorboard import AnomalibTensorBoardLogger + >>> from anomalib.utils.loggers import AnomalibTensorBoardLogger >>> logger = AnomalibTensorBoardLogger("tb_logs", name="my_model") >>> trainer = Trainer(logger=logger) diff --git a/anomalib/loggers/wandb.py b/anomalib/utils/loggers/wandb.py similarity index 98% rename from anomalib/loggers/wandb.py rename to anomalib/utils/loggers/wandb.py index e037e27785..b5694ed861 100644 --- a/anomalib/loggers/wandb.py +++ b/anomalib/utils/loggers/wandb.py @@ -62,7 +62,7 @@ class AnomalibWandbLogger(ImageLoggerBase, WandbLogger): If both ``log_model`` and ``offline``is set to ``True``. Example: - >>> from anomalib.loggers.wandb import AnomalibWandbLogger + >>> from anomalib.utils.loggers import AnomalibWandbLogger >>> from pytorch_lightning import Trainer >>> wandb_logger = AnomalibWandbLogger() >>> trainer = Trainer(logger=wandb_logger) diff --git a/anomalib/core/metrics/__init__.py b/anomalib/utils/metrics/__init__.py similarity index 100% rename from anomalib/core/metrics/__init__.py rename to anomalib/utils/metrics/__init__.py diff --git a/anomalib/core/metrics/adaptive_threshold.py b/anomalib/utils/metrics/adaptive_threshold.py similarity index 100% rename from anomalib/core/metrics/adaptive_threshold.py rename to anomalib/utils/metrics/adaptive_threshold.py diff --git a/anomalib/core/metrics/anomaly_score_distribution.py b/anomalib/utils/metrics/anomaly_score_distribution.py similarity index 100% rename from anomalib/core/metrics/anomaly_score_distribution.py rename to anomalib/utils/metrics/anomaly_score_distribution.py diff --git a/anomalib/core/metrics/auroc.py b/anomalib/utils/metrics/auroc.py similarity index 100% rename from anomalib/core/metrics/auroc.py rename to anomalib/utils/metrics/auroc.py diff --git a/anomalib/core/metrics/min_max.py b/anomalib/utils/metrics/min_max.py similarity index 100% rename from anomalib/core/metrics/min_max.py rename to anomalib/utils/metrics/min_max.py diff --git a/anomalib/core/metrics/optimal_f1.py b/anomalib/utils/metrics/optimal_f1.py similarity index 100% rename from anomalib/core/metrics/optimal_f1.py rename to anomalib/utils/metrics/optimal_f1.py diff --git a/tests/helpers/inference.py b/tests/helpers/inference.py index 149cfdd7ba..90701f0576 100644 --- a/tests/helpers/inference.py +++ b/tests/helpers/inference.py @@ -19,7 +19,7 @@ import numpy as np -from anomalib.core.model import AnomalyModule +from anomalib.models.components import AnomalyModule class MockImageLoader: diff --git a/tests/helpers/model.py b/tests/helpers/model.py index 0a9536ac5a..937b8cfcb2 100644 --- a/tests/helpers/model.py +++ b/tests/helpers/model.py @@ -23,11 +23,10 @@ from pytorch_lightning.callbacks import ModelCheckpoint from anomalib.config import get_configurable_parameters, update_nncf_config -from anomalib.core.callbacks import get_callbacks -from anomalib.core.callbacks.visualizer_callback import VisualizerCallback -from anomalib.core.model.anomaly_module import AnomalyModule from anomalib.data import get_datamodule from anomalib.models import get_model +from anomalib.models.components import AnomalyModule +from anomalib.utils.callbacks import VisualizerCallback, get_callbacks def setup_model_train( diff --git a/tests/models/test_model.py b/tests/models/test_model.py new file mode 100644 index 0000000000..9b9611be35 --- /dev/null +++ b/tests/models/test_model.py @@ -0,0 +1,182 @@ +"""Test Models.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import random +import tempfile +from functools import wraps + +import numpy as np +import pytest +from pytorch_lightning import Trainer + +from anomalib.config import get_configurable_parameters, update_nncf_config +from anomalib.data import get_datamodule +from anomalib.models import get_model +from anomalib.utils.callbacks import VisualizerCallback, get_callbacks +from tests.helpers.dataset import TestDataset, get_dataset_path + + +@pytest.fixture(autouse=True) +def category() -> str: + """PyTest fixture to randomly return an MVTec category. + + Returns: + str: Random MVTec category to train/test. + """ + categories = [ + "bottle", + "cable", + "capsule", + "carpet", + "grid", + "hazelnut", + "leather", + "metal_nut", + "pill", + "screw", + "tile", + "toothbrush", + "transistor", + "wood", + "zipper", + ] + + category = random.choice(categories) # nosec + return category + + +class AddDFMScores: + """Function wrapper for checking both scores of DFM.""" + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + if kwds["model_name"] == "dfm": + for score in ["fre", "nll"]: + func(*args, score_type=score, **kwds) + else: + func(*args, **kwds) + + return inner + + +class TestModel: + """Test model.""" + + def _setup(self, model_name, use_mvtec, dataset_path, project_path, nncf, category, score_type=None): + config = get_configurable_parameters(model_name=model_name) + if score_type is not None: + config.model.score_type = score_type + config.project.seed = 1234 + config.dataset.category = category + config.dataset.path = dataset_path + config.model.weight_file = "weights/model.ckpt" # add model weights to the config + config.project.log_images_to = [] + + if not use_mvtec: + config.dataset.category = "shapes" + + if nncf: + config.optimization.nncf.apply = True + config = update_nncf_config(config) + config.init_weights = None + + # reassign project path as config is updated in `update_config_for_nncf` + config.project.path = project_path + + datamodule = get_datamodule(config) + model = get_model(config) + + callbacks = get_callbacks(config) + + for index, callback in enumerate(callbacks): + if isinstance(callback, VisualizerCallback): + callbacks.pop(index) + break + + # Train the model. + trainer = Trainer(callbacks=callbacks, **config.trainer) + trainer.fit(model=model, datamodule=datamodule) + return model, config, datamodule, trainer + + def _test_metrics(self, trainer, config, model, datamodule): + """Tests the model metrics but also acts as a setup.""" + + results = trainer.test(model=model, datamodule=datamodule)[0] + + assert results["image_AUROC"] >= 0.6 + + if config.dataset.task == "segmentation": + assert results["pixel_AUROC"] >= 0.6 + return results + + def _test_model_load(self, config, datamodule, results): + loaded_model = get_model(config) # get new model + + callbacks = get_callbacks(config) + + for index, callback in enumerate(callbacks): + # Remove visualizer callback as saving results takes time + if isinstance(callback, VisualizerCallback): + callbacks.pop(index) + break + + # create new trainer object with LoadModel callback (assumes it is present) + trainer = Trainer(callbacks=callbacks, **config.trainer) + # Assumes the new model has LoadModel callback and the old one had ModelCheckpoint callback + new_results = trainer.test(model=loaded_model, datamodule=datamodule)[0] + assert np.isclose( + results["image_AUROC"], new_results["image_AUROC"] + ), "Loaded model does not yield close performance results" + if config.dataset.task == "segmentation": + assert np.isclose( + results["pixel_AUROC"], new_results["pixel_AUROC"] + ), "Loaded model does not yield close performance results" + + @pytest.mark.parametrize( + ["model_name", "nncf"], + [ + ("padim", False), + ("dfkde", False), + # ("dfm", False), # skip dfm test + ("stfpm", False), + ("stfpm", True), + ("patchcore", False), + ("cflow", False), + ("ganomaly", False), + ], + ) + @pytest.mark.flaky(max_runs=3) + @TestDataset(num_train=200, num_test=10, path=get_dataset_path(), use_mvtec=True) + @AddDFMScores() + def test_model(self, category, model_name, nncf, use_mvtec=True, path="./datasets/MVTec", score_type=None): + """Driver for all the tests in the class.""" + with tempfile.TemporaryDirectory() as project_path: + model, config, datamodule, trainer = self._setup( + model_name=model_name, + use_mvtec=use_mvtec, + dataset_path=path, + nncf=nncf, + project_path=project_path, + category=category, + score_type=score_type, + ) + + # test model metrics + results = self._test_metrics(trainer=trainer, config=config, model=model, datamodule=datamodule) + + # test model load + self._test_model_load(config=config, datamodule=datamodule, results=results) diff --git a/tests/nightly/deploy/test_inferencer.py b/tests/nightly/deploy/test_inferencer.py index 23d11ab64e..f3cff8ae28 100644 --- a/tests/nightly/deploy/test_inferencer.py +++ b/tests/nightly/deploy/test_inferencer.py @@ -26,8 +26,7 @@ from anomalib.config import get_configurable_parameters from anomalib.data import get_datamodule -from anomalib.deploy.inferencers import OpenVINOInferencer, TorchInferencer -from anomalib.deploy.optimize import export_convert +from anomalib.deploy import OpenVINOInferencer, TorchInferencer, export_convert from anomalib.models import get_model from tests.helpers.dataset import TestDataset, get_dataset_path from tests.helpers.inference import MockImageLoader, get_meta_data diff --git a/tests/pre_merge/config/__init__.py b/tests/pre_merge/config/__init__.py index e3d7183a41..8d05a0bcff 100644 --- a/tests/pre_merge/config/__init__.py +++ b/tests/pre_merge/config/__init__.py @@ -1,4 +1,4 @@ -"""Tests for configuration getters/setters.""" +"""Test callbacks.""" # Copyright (C) 2020 Intel Corporation # diff --git a/tests/pre_merge/datasets/test_dataset.py b/tests/pre_merge/datasets/test_dataset.py index 9c31d5f7d4..608c12f3af 100644 --- a/tests/pre_merge/datasets/test_dataset.py +++ b/tests/pre_merge/datasets/test_dataset.py @@ -4,7 +4,7 @@ import pytest from anomalib.data.mvtec import MVTecDataModule -from anomalib.data.transforms import Denormalize, ToNumpy +from anomalib.pre_processing.transforms import Denormalize, ToNumpy from tests.helpers.dataset import get_dataset_path diff --git a/tests/pre_merge/datasets/test_tiler.py b/tests/pre_merge/datasets/test_tiler.py index ce64a28fa2..4555ce1490 100644 --- a/tests/pre_merge/datasets/test_tiler.py +++ b/tests/pre_merge/datasets/test_tiler.py @@ -4,7 +4,7 @@ import torch from omegaconf import ListConfig -from anomalib.data.tiler import StrideSizeError, Tiler +from anomalib.pre_processing.tiler import StrideSizeError, Tiler tile_data = [ ([3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512]), False), diff --git a/tests/pre_merge/datasets/test_transforms.py b/tests/pre_merge/datasets/test_transforms.py index 76b1c163c0..ea15788ec0 100644 --- a/tests/pre_merge/datasets/test_transforms.py +++ b/tests/pre_merge/datasets/test_transforms.py @@ -16,7 +16,7 @@ import skimage from torch import Tensor -from anomalib.data.transforms import PreProcessor +from anomalib.pre_processing import PreProcessor def test_transforms_and_image_size_cannot_be_none(): diff --git a/tests/pre_merge/loggers/test_get_logger.py b/tests/pre_merge/loggers/test_get_logger.py index 911d9c0e67..e48798d7a2 100644 --- a/tests/pre_merge/loggers/test_get_logger.py +++ b/tests/pre_merge/loggers/test_get_logger.py @@ -16,9 +16,13 @@ import pytest from omegaconf import OmegaConf -from pytorch_lightning.loggers.wandb import WandbLogger -from anomalib.loggers import AnomalibTensorBoardLogger, UnknownLogger, get_logger +from anomalib.utils.loggers import ( + AnomalibTensorBoardLogger, + AnomalibWandbLogger, + UnknownLogger, + get_logger, +) def test_get_logger(): @@ -47,7 +51,7 @@ def test_get_logger(): # get wandb logger config.project.logger = "wandb" logger = get_logger(config=config) - assert isinstance(logger, WandbLogger) + assert isinstance(logger, AnomalibWandbLogger) # raise unknown with pytest.raises(UnknownLogger): diff --git a/tests/pre_merge/utils/callbacks/compress_callback/dummy_lightning_model.py b/tests/pre_merge/utils/callbacks/compress_callback/dummy_lightning_model.py index 0f5c8206c5..a0bef14356 100644 --- a/tests/pre_merge/utils/callbacks/compress_callback/dummy_lightning_model.py +++ b/tests/pre_merge/utils/callbacks/compress_callback/dummy_lightning_model.py @@ -8,8 +8,8 @@ from torchvision import transforms from torchvision.datasets import FakeData -from anomalib.core.callbacks.visualizer_callback import VisualizerCallback -from anomalib.core.metrics import AdaptiveThreshold, AnomalyScoreDistribution, MinMax +from anomalib.utils.callbacks.visualizer_callback import VisualizerCallback +from anomalib.utils.metrics import AdaptiveThreshold, AnomalyScoreDistribution, MinMax class FakeDataModule(pl.LightningDataModule): diff --git a/tests/pre_merge/utils/callbacks/compress_callback/test_compress.py b/tests/pre_merge/utils/callbacks/compress_callback/test_compress.py index d0cb7c001d..a18d61b440 100644 --- a/tests/pre_merge/utils/callbacks/compress_callback/test_compress.py +++ b/tests/pre_merge/utils/callbacks/compress_callback/test_compress.py @@ -5,7 +5,7 @@ from pytorch_lightning.callbacks.early_stopping import EarlyStopping from anomalib.config import get_configurable_parameters -from anomalib.core.callbacks.compress import CompressModelCallback +from anomalib.utils.callbacks import CompressModelCallback from tests.pre_merge.utils.callbacks.compress_callback.dummy_lightning_model import ( DummyLightningModule, FakeDataModule, diff --git a/tests/pre_merge/utils/callbacks/normalization_callback/test_normalization_callback.py b/tests/pre_merge/utils/callbacks/normalization_callback/test_normalization_callback.py index 399d3d7bd4..97f0afa6aa 100644 --- a/tests/pre_merge/utils/callbacks/normalization_callback/test_normalization_callback.py +++ b/tests/pre_merge/utils/callbacks/normalization_callback/test_normalization_callback.py @@ -1,9 +1,9 @@ from pytorch_lightning import Trainer, seed_everything from anomalib.config import get_configurable_parameters -from anomalib.core.callbacks import get_callbacks from anomalib.data import get_datamodule from anomalib.models import get_model +from anomalib.utils.callbacks import get_callbacks from tests.helpers.dataset import TestDataset, get_dataset_path diff --git a/tests/pre_merge/utils/callbacks/visualizer_callback/dummy_lightning_model.py b/tests/pre_merge/utils/callbacks/visualizer_callback/dummy_lightning_model.py index f016dc0825..9ce70f207d 100644 --- a/tests/pre_merge/utils/callbacks/visualizer_callback/dummy_lightning_model.py +++ b/tests/pre_merge/utils/callbacks/visualizer_callback/dummy_lightning_model.py @@ -8,8 +8,8 @@ from torch import nn from torch.utils.data import DataLoader, Dataset -from anomalib.core.callbacks.visualizer_callback import VisualizerCallback -from anomalib.core.model import AnomalyModule +from anomalib.models.components import AnomalyModule +from anomalib.utils.callbacks.visualizer_callback import VisualizerCallback class DummyDataset(Dataset): diff --git a/tests/pre_merge/utils/callbacks/visualizer_callback/test_visualizer.py b/tests/pre_merge/utils/callbacks/visualizer_callback/test_visualizer.py index a1f37a57ba..6d5d12bbf7 100644 --- a/tests/pre_merge/utils/callbacks/visualizer_callback/test_visualizer.py +++ b/tests/pre_merge/utils/callbacks/visualizer_callback/test_visualizer.py @@ -7,7 +7,7 @@ import pytorch_lightning as pl from omegaconf.omegaconf import OmegaConf -from anomalib.loggers import AnomalibTensorBoardLogger +from anomalib.utils.loggers import AnomalibTensorBoardLogger from .dummy_lightning_model import DummyDataModule, DummyModule diff --git a/tests/pre_merge/utils/test_download_progress_bar.py b/tests/pre_merge/utils/test_download_progress_bar.py index 85ee207462..6fd5d67442 100644 --- a/tests/pre_merge/utils/test_download_progress_bar.py +++ b/tests/pre_merge/utils/test_download_progress_bar.py @@ -18,7 +18,7 @@ import tempfile from urllib.request import urlretrieve -from anomalib.utils.download_progress_bar import DownloadProgressBar +from anomalib.data.utils.download_progress_bar import DownloadProgressBar def test_output_on_download(capfd): diff --git a/tests/pre_processing/__init__.py b/tests/pre_processing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pre_processing/test_tiler.py b/tests/pre_processing/test_tiler.py new file mode 100644 index 0000000000..4555ce1490 --- /dev/null +++ b/tests/pre_processing/test_tiler.py @@ -0,0 +1,135 @@ +"""Image Tiling Tests.""" + +import pytest +import torch +from omegaconf import ListConfig + +from anomalib.pre_processing.tiler import StrideSizeError, Tiler + +tile_data = [ + ([3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512]), False), + ([1, 3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512]), False), + ([3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512]), True), + ([1, 3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512]), True), +] + +untile_data = [ + ([3, 1024, 1024], 512, 256, torch.Size([4, 3, 512, 512])), + ([1, 3, 1024, 1024], 512, 512, torch.Size([4, 3, 512, 512])), +] + +overlapping_data = [ + ( + torch.Size([1, 3, 1024, 1024]), + 512, + 256, + torch.Size([16, 3, 512, 512]), + "padding", + ), + ( + torch.Size([1, 3, 1024, 1024]), + 512, + 256, + torch.Size([16, 3, 512, 512]), + "interpolation", + ), +] + + +@pytest.mark.parametrize( + "tile_size, stride", + [(512, 256), ([512, 512], [256, 256]), (ListConfig([512, 512]), 256)], +) +def test_size_types_should_be_int_tuple_or_list_config(tile_size, stride): + """Size type could only be integer, tuple or ListConfig type.""" + tiler = Tiler(tile_size=tile_size, stride=stride) + assert isinstance(tiler.tile_size_h, int) + assert isinstance(tiler.stride_w, int) + + +@pytest.mark.parametrize("image_size, tile_size, stride, shape, use_random_tiling", tile_data) +def test_tiler_handles_single_image_without_batch_dimension(image_size, tile_size, stride, shape, use_random_tiling): + """Tiler should add batch dimension if image is 3D (CxHxW).""" + tiler = Tiler(tile_size=tile_size, stride=stride) + image = torch.rand(image_size) + patches = tiler.tile(image, use_random_tiling=use_random_tiling) + assert patches.shape == shape + + +def test_stride_size_cannot_be_larger_than_tile_size(): + """Larger stride size than tile size is not desired, and causes issues.""" + kernel_size = (128, 128) + stride = 256 + with pytest.raises(StrideSizeError): + tiler = Tiler(tile_size=kernel_size, stride=stride) + + +def test_tile_size_cannot_be_larger_than_image_size(): + """Larger tile size than image size is not desired, and causes issues.""" + with pytest.raises(ValueError): + tiler = Tiler(tile_size=1024, stride=512) + image = torch.rand(1, 3, 512, 512) + tiler.tile(image) + + +@pytest.mark.parametrize("tile_size, kernel_size, stride, image_size", untile_data) +def test_untile_non_overlapping_patches(tile_size, kernel_size, stride, image_size): + """Non-Overlapping Tiling/Untiling should return the same image size.""" + tiler = Tiler(tile_size=kernel_size, stride=stride) + image = torch.rand(image_size) + tiles = tiler.tile(image) + + untiled_image = tiler.untile(tiles) + assert untiled_image.shape == torch.Size(image_size) + + +@pytest.mark.parametrize("mode", ["pad", "padded", "interpolate", "interplation"]) +def test_upscale_downscale_mode(mode): + with pytest.raises(ValueError): + tiler = Tiler(tile_size=(512, 512), stride=(256, 256), mode=mode) + + +@pytest.mark.parametrize("image_size, kernel_size, stride, tile_size, mode", overlapping_data) +@pytest.mark.parametrize("remove_border_count", [0, 5]) +def test_untile_overlapping_patches(image_size, kernel_size, stride, remove_border_count, tile_size, mode): + """Overlapping Tiling/Untiling should return the same image size.""" + tiler = Tiler( + tile_size=kernel_size, + stride=stride, + remove_border_count=remove_border_count, + mode=mode, + ) + + image = torch.rand(image_size) + tiles = tiler.tile(image) + reconstructed_image = tiler.untile(tiles) + image = image[ + :, + :, + remove_border_count:-remove_border_count, + remove_border_count:-remove_border_count, + ] + reconstructed_image = reconstructed_image[ + :, + :, + remove_border_count:-remove_border_count, + remove_border_count:-remove_border_count, + ] + assert torch.equal(image, reconstructed_image) + + +@pytest.mark.parametrize("image_size", [(1, 3, 512, 512)]) +@pytest.mark.parametrize("tile_size", [(256, 256), (200, 200), (211, 213), (312, 333), (511, 511)]) +@pytest.mark.parametrize("stride", [(64, 64), (111, 111), (128, 111), (128, 128)]) +@pytest.mark.parametrize("mode", ["padding", "interpolation"]) +def test_divisible_tile_size_and_stride(image_size, tile_size, stride, mode): + """When the image is not divisible by tile size and stride, Tiler should up + samples the image before tiling, and downscales before untiling.""" + tiler = Tiler(tile_size, stride, mode=mode) + image = torch.rand(image_size) + tiles = tiler.tile(image) + reconstructed_image = tiler.untile(tiles) + assert image.shape == reconstructed_image.shape + + if mode == "padding": + assert torch.allclose(image, reconstructed_image) diff --git a/tests/pre_processing/transforms/__init__.py b/tests/pre_processing/transforms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pre_processing/transforms/test_transforms.py b/tests/pre_processing/transforms/test_transforms.py new file mode 100644 index 0000000000..ea15788ec0 --- /dev/null +++ b/tests/pre_processing/transforms/test_transforms.py @@ -0,0 +1,81 @@ +"""Data transformation test. + +This test contains the following test: + - Transformations could be ``None``, ``yaml``, ``json`` or ``dict``. + - When it is ``None``, the script loads the default transforms + - When it is ``yaml``, ``json`` or ``dict``, `albumentations` package + deserializes the transformations. +""" + +import tempfile +from pathlib import Path + +import albumentations as A +import numpy as np +import pytest +import skimage +from torch import Tensor + +from anomalib.pre_processing import PreProcessor + + +def test_transforms_and_image_size_cannot_be_none(): + """When transformations ``config`` and ``image_size`` are ``None`` + ``PreProcessor`` class should raise a ``ValueError``.""" + + with pytest.raises(ValueError): + PreProcessor(config=None, image_size=None) + + +def test_image_size_could_be_int_or_tuple(): + """When ``config`` is None, ``image_size`` could be either ``int`` or + ``Tuple[int, int]``.""" + + PreProcessor(config=None, image_size=256) + PreProcessor(config=None, image_size=(256, 512)) + with pytest.raises(ValueError): + PreProcessor(config=None, image_size=0.0) + + +def test_load_transforms_from_string(): + """When the pre-processor is instantiated via a transform config file, it + should work with either string or A.Compose and return a ValueError + otherwise.""" + + config_path = tempfile.NamedTemporaryFile(suffix=".yaml").name + + # Create a dummy transformation. + transforms = A.Compose( + [ + A.Resize(1024, 1024, always_apply=True), + A.CenterCrop(256, 256, always_apply=True), + A.Resize(224, 224, always_apply=True), + ] + ) + A.save(transform=transforms, filepath=config_path, data_format="yaml") + + # Pass a path to config + pre_processor = PreProcessor(config=config_path) + assert isinstance(pre_processor.transforms, A.Compose) + + # Pass a config of type A.Compose + pre_processor = PreProcessor(config=transforms) + assert isinstance(pre_processor.transforms, A.Compose) + + # Anything else should raise an error + with pytest.raises(ValueError): + PreProcessor(config=0) + + +def test_to_tensor_returns_correct_type(): + """`to_tensor` flag should ensure that pre-processor returns the expected + type.""" + image = skimage.data.astronaut() + + pre_processor = PreProcessor(config=None, image_size=256, to_tensor=True) + transformed = pre_processor(image=image)["image"] + assert isinstance(transformed, Tensor) + + pre_processor = PreProcessor(config=None, image_size=256, to_tensor=False) + transformed = pre_processor(image=image)["image"] + assert isinstance(transformed, np.ndarray) diff --git a/anomalib/core/__init__.py b/tests/utils/callbacks/__init__.py similarity index 86% rename from anomalib/core/__init__.py rename to tests/utils/callbacks/__init__.py index 7adddf907b..8d05a0bcff 100644 --- a/anomalib/core/__init__.py +++ b/tests/utils/callbacks/__init__.py @@ -1,4 +1,4 @@ -"""This module holds common components such as callbacks, custom modules and utils.""" +"""Test callbacks.""" # Copyright (C) 2020 Intel Corporation # diff --git a/tests/utils/callbacks/compress_callback/__init__.py b/tests/utils/callbacks/compress_callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/utils/callbacks/compress_callback/dummy_config.yml b/tests/utils/callbacks/compress_callback/dummy_config.yml new file mode 100644 index 0000000000..8939e6cb4b --- /dev/null +++ b/tests/utils/callbacks/compress_callback/dummy_config.yml @@ -0,0 +1,26 @@ +dataset: + name: FakeData + category: fakedata + image_size: 32 + +model: + dropout: 0 + lr: 1e-3 + metric: loss + momentum: 0.9 + name: DummyModel + weight_decay: 1e-4 + threshold: + image_default: 0.0 + pixel_default: 0.0 + +project: + path: ./results + +optimization: + compression: + apply: true + +trainer: + accelerator: null + gpus: 1 diff --git a/tests/utils/callbacks/compress_callback/dummy_lightning_model.py b/tests/utils/callbacks/compress_callback/dummy_lightning_model.py new file mode 100644 index 0000000000..a0bef14356 --- /dev/null +++ b/tests/utils/callbacks/compress_callback/dummy_lightning_model.py @@ -0,0 +1,104 @@ +from typing import Union + +import pytorch_lightning as pl +import torch.nn.functional as F +from omegaconf import DictConfig, ListConfig +from torch import nn, optim +from torch.utils.data import DataLoader +from torchvision import transforms +from torchvision.datasets import FakeData + +from anomalib.utils.callbacks.visualizer_callback import VisualizerCallback +from anomalib.utils.metrics import AdaptiveThreshold, AnomalyScoreDistribution, MinMax + + +class FakeDataModule(pl.LightningDataModule): + def __init__(self, batch_size: int = 32): + super(FakeDataModule, self).__init__() + self.batch_size = batch_size + self.pre_process = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]) + + def train_dataloader(self): + return DataLoader( + FakeData( + size=1000, + num_classes=10, + transform=self.pre_process, + image_size=(3, 32, 32), + ), + batch_size=self.batch_size, + ) + + def test_dataloader(self): + return DataLoader( + FakeData( + size=100, + num_classes=10, + transform=self.pre_process, + image_size=(3, 32, 32), + ), + batch_size=self.batch_size, + ) + + +class DummyModel(nn.Module): + """Creates a very basic CNN model to fit image data for classification task + The test uses this to check if this model is converted to OpenVINO IR.""" + + def __init__(self, hparams: Union[DictConfig, ListConfig]): + super().__init__() + self.hparams = hparams + self.conv1 = nn.Conv2d(3, 32, 3) + self.conv2 = nn.Conv2d(32, 32, 5) + self.conv3 = nn.Conv2d(32, 1, 7) + self.fc1 = nn.Linear(400, 256) + self.fc2 = nn.Linear(256, 10) + + def forward(self, x): + batch_size, _, _, _ = x.size() + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = x.view(batch_size, -1) + x = self.fc1(x) + x = F.dropout(x, p=self.hparams.model.dropout) + x = self.fc2(x) + x = F.log_softmax(x, dim=1) + return x + + +class DummyLightningModule(pl.LightningModule): + """A dummy model which fits the torchvision FakeData dataset.""" + + def __init__(self, hparams: Union[DictConfig, ListConfig]): + super().__init__() + self.save_hyperparameters(hparams) + self.loss_fn = nn.NLLLoss() + self.callbacks = [VisualizerCallback()] # test if this is removed + + self.image_threshold = AdaptiveThreshold(hparams.model.threshold.image_default).cpu() + self.pixel_threshold = AdaptiveThreshold(hparams.model.threshold.pixel_default).cpu() + + self.training_distribution = AnomalyScoreDistribution().cpu() + self.min_max = MinMax().cpu() + self.model = DummyModel(hparams) + + def training_step(self, batch, _): + x, y = batch + y_hat = self.model(x) + loss = self.loss_fn(y_hat, y) + return {"loss": loss} + + def validation_step(self, batch, _): + x, y = batch + y_hat = self.model(x) + loss = self.loss_fn(y_hat, y) + self.log(name="loss", value=loss.item(), prog_bar=True) + + def configure_optimizers(self): + return optim.SGD( + self.parameters(), + lr=self.hparams.model.lr, + momentum=self.hparams.model.momentum, + weight_decay=self.hparams.model.weight_decay, + ) diff --git a/tests/utils/callbacks/compress_callback/test_compress.py b/tests/utils/callbacks/compress_callback/test_compress.py new file mode 100644 index 0000000000..a18d61b440 --- /dev/null +++ b/tests/utils/callbacks/compress_callback/test_compress.py @@ -0,0 +1,42 @@ +import os +import tempfile + +import pytorch_lightning as pl +from pytorch_lightning.callbacks.early_stopping import EarlyStopping + +from anomalib.config import get_configurable_parameters +from anomalib.utils.callbacks import CompressModelCallback +from tests.pre_merge.utils.callbacks.compress_callback.dummy_lightning_model import ( + DummyLightningModule, + FakeDataModule, +) + + +def test_compress_model_callback(): + """Tests if an optimized model is created.""" + + config = get_configurable_parameters( + model_config_path="tests/pre_merge/utils/callbacks/compress_callback/dummy_config.yml" + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + config.project.path = tmp_dir + model = DummyLightningModule(hparams=config) + model.callbacks = [ + CompressModelCallback( + input_size=config.model.input_size, dirpath=os.path.join(tmp_dir), filename="compressed_model" + ), + EarlyStopping(monitor=config.model.metric), + ] + datamodule = FakeDataModule() + trainer = pl.Trainer( + gpus=1, + callbacks=model.callbacks, + logger=False, + checkpoint_callback=False, + max_epochs=1, + val_check_interval=3, + ) + trainer.fit(model, datamodule=datamodule) + + assert os.path.exists(os.path.join(tmp_dir, "compressed_model.bin")), "Failed to generate OpenVINO model" diff --git a/tests/utils/callbacks/normalization_callback/__init__.py b/tests/utils/callbacks/normalization_callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/utils/callbacks/normalization_callback/test_normalization_callback.py b/tests/utils/callbacks/normalization_callback/test_normalization_callback.py new file mode 100644 index 0000000000..97f0afa6aa --- /dev/null +++ b/tests/utils/callbacks/normalization_callback/test_normalization_callback.py @@ -0,0 +1,49 @@ +from pytorch_lightning import Trainer, seed_everything + +from anomalib.config import get_configurable_parameters +from anomalib.data import get_datamodule +from anomalib.models import get_model +from anomalib.utils.callbacks import get_callbacks +from tests.helpers.dataset import TestDataset, get_dataset_path + + +def run_train_test(config): + model = get_model(config) + datamodule = get_datamodule(config) + callbacks = get_callbacks(config) + + trainer = Trainer(**config.trainer, callbacks=callbacks) + trainer.fit(model=model, datamodule=datamodule) + results = trainer.test(model=model, datamodule=datamodule) + return results + + +@TestDataset(num_train=200, num_test=30, path=get_dataset_path(), seed=42) +def test_normalizer(path=get_dataset_path(), category="shapes"): + config = get_configurable_parameters(model_config_path="anomalib/models/padim/config.yaml") + config.dataset.path = path + config.dataset.category = category + config.model.threshold.adaptive = True + config.project.log_images_to = [] + + # run without normalization + config.model.normalization_method = "none" + seed_everything(42) + results_without_normalization = run_train_test(config) + + # run with cdf normalization + config.model.normalization_method = "cdf" + seed_everything(42) + results_with_cdf_normalization = run_train_test(config) + + # run without normalization + config.model.normalization_method = "min_max" + seed_everything(42) + results_with_minmax_normalization = run_train_test(config) + + # performance should be the same + for metric in ["image_AUROC", "image_F1"]: + assert round(results_without_normalization[0][metric], 3) == round(results_with_cdf_normalization[0][metric], 3) + assert round(results_without_normalization[0][metric], 3) == round( + results_with_minmax_normalization[0][metric], 3 + ) diff --git a/tests/utils/callbacks/visualizer_callback/__init__.py b/tests/utils/callbacks/visualizer_callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/utils/callbacks/visualizer_callback/dummy_lightning_model.py b/tests/utils/callbacks/visualizer_callback/dummy_lightning_model.py new file mode 100644 index 0000000000..9ce70f207d --- /dev/null +++ b/tests/utils/callbacks/visualizer_callback/dummy_lightning_model.py @@ -0,0 +1,72 @@ +from pathlib import Path +from typing import Union + +import pytorch_lightning as pl +import torch +from omegaconf.dictconfig import DictConfig +from omegaconf.listconfig import ListConfig +from torch import nn +from torch.utils.data import DataLoader, Dataset + +from anomalib.models.components import AnomalyModule +from anomalib.utils.callbacks.visualizer_callback import VisualizerCallback + + +class DummyDataset(Dataset): + def __init__(self): + super().__init__() + + def __len__(self): + return 1 + + def __getitem__(self, idx): + return torch.ones(1) + + +class DummyDataModule(pl.LightningDataModule): + def test_dataloader(self) -> DataLoader: + return DataLoader(DummyDataset()) + + +class DummyAnomalyMapGenerator: + def __init__(self): + self.input_size = (100, 100) + self.sigma = 4 + + +class DummyModel(nn.Module): + def __init__(self): + super().__init__() + self.anomaly_map_generator = DummyAnomalyMapGenerator() + + +class DummyModule(AnomalyModule): + """A dummy model which calls visualizer callback on fake images and + masks.""" + + def __init__(self, hparams: Union[DictConfig, ListConfig]): + super().__init__(hparams) + self.model = DummyModel() + self.task = "segmentation" + self.callbacks = [VisualizerCallback()] # test if this is removed + + def test_step(self, batch, _): + """Only used to trigger on_test_epoch_end.""" + self.log(name="loss", value=0.0, prog_bar=True) + outputs = dict( + image_path=[Path("test1.jpg")], + image=torch.rand((1, 3, 100, 100)), + mask=torch.zeros((1, 100, 100)), + anomaly_maps=torch.ones((1, 100, 100)), + label=torch.Tensor([0]), + ) + return outputs + + def validation_epoch_end(self, output): + return None + + def test_epoch_end(self, outputs): + return None + + def configure_optimizers(self): + return None diff --git a/tests/utils/callbacks/visualizer_callback/test_visualizer.py b/tests/utils/callbacks/visualizer_callback/test_visualizer.py new file mode 100644 index 0000000000..6d5d12bbf7 --- /dev/null +++ b/tests/utils/callbacks/visualizer_callback/test_visualizer.py @@ -0,0 +1,45 @@ +import glob +import os +import tempfile +from unittest import mock + +import pytest +import pytorch_lightning as pl +from omegaconf.omegaconf import OmegaConf + +from anomalib.utils.loggers import AnomalibTensorBoardLogger + +from .dummy_lightning_model import DummyDataModule, DummyModule + + +def get_dummy_module(config): + return DummyModule(config) + + +def get_dummy_logger(config, tempdir): + logger = AnomalibTensorBoardLogger(name=f"tensorboard_logs", save_dir=tempdir) + return logger + + +@pytest.mark.parametrize("dataset", ["segmentation"]) +def test_add_images(dataset): + """Tests if tensorboard logs are generated.""" + with tempfile.TemporaryDirectory() as dir_loc: + config = OmegaConf.create( + { + "dataset": {"task": dataset}, + "model": {"threshold": {"image_default": 0.5, "pixel_default": 0.5, "adaptive": True}}, + "project": {"path": dir_loc, "log_images_to": ["tensorboard", "local"]}, + } + ) + logger = get_dummy_logger(config, dir_loc) + model = get_dummy_module(config) + trainer = pl.Trainer(callbacks=model.callbacks, logger=logger, checkpoint_callback=False) + trainer.test(model=model, datamodule=DummyDataModule()) + # test if images are logged + if len(glob.glob(os.path.join(dir_loc, "images", "*.jpg"))) != 1: + raise Exception("Failed to save to local path") + + # test if tensorboard logs are created + if len(glob.glob(os.path.join(dir_loc, "tensorboard_logs", "version_*"))) == 0: + raise Exception("Failed to save to tensorboard") diff --git a/tests/utils/loggers/__init__.py b/tests/utils/loggers/__init__.py new file mode 100644 index 0000000000..23adccdc4b --- /dev/null +++ b/tests/utils/loggers/__init__.py @@ -0,0 +1,15 @@ +"""Test supported loggers.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. diff --git a/tests/utils/loggers/test_get_logger.py b/tests/utils/loggers/test_get_logger.py new file mode 100644 index 0000000000..e48798d7a2 --- /dev/null +++ b/tests/utils/loggers/test_get_logger.py @@ -0,0 +1,59 @@ +"""Tests to ascertain requested logger.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest +from omegaconf import OmegaConf + +from anomalib.utils.loggers import ( + AnomalibTensorBoardLogger, + AnomalibWandbLogger, + UnknownLogger, + get_logger, +) + + +def test_get_logger(): + """Test whether the right logger is returned.""" + + config = OmegaConf.create( + { + "project": {"logger": None, "path": "/tmp"}, + "dataset": {"name": "dummy", "category": "cat1"}, + "model": {"name": "DummyModel"}, + } + ) + + # get no logger + logger = get_logger(config=config) + assert isinstance(logger, bool) + config.project.logger = False + logger = get_logger(config=config) + assert isinstance(logger, bool) + + # get tensorboard + config.project.logger = "tensorboard" + logger = get_logger(config=config) + assert isinstance(logger, AnomalibTensorBoardLogger) + + # get wandb logger + config.project.logger = "wandb" + logger = get_logger(config=config) + assert isinstance(logger, AnomalibWandbLogger) + + # raise unknown + with pytest.raises(UnknownLogger): + config.project.logger = "randomlogger" + logger = get_logger(config=config) diff --git a/tests/utils/test_config.py b/tests/utils/test_config.py new file mode 100644 index 0000000000..6d5cf57069 --- /dev/null +++ b/tests/utils/test_config.py @@ -0,0 +1,34 @@ +"""Test Config Getter.""" + +# Copyright (C) 2020 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. + +import pytest + +from anomalib.config import get_configurable_parameters + + +class TestConfig: + """Test Config Getter.""" + + def test_get_configurable_parameters_return_correct_model_name(self): + """Configurable parameter should return the correct model name.""" + model_name = "stfpm" + configurable_parameters = get_configurable_parameters(model_name) + assert configurable_parameters.model.name == model_name + + def test_get_configurable_parameter_fails_with_none_arguments(self): + """Configurable parameter should raise an error with none arguments.""" + with pytest.raises(ValueError): + get_configurable_parameters() diff --git a/tools/test.py b/tools/test.py index 902d82b0d3..6541cb3b0c 100644 --- a/tools/test.py +++ b/tools/test.py @@ -19,9 +19,9 @@ from pytorch_lightning import Trainer from anomalib.config import get_configurable_parameters -from anomalib.core.callbacks import get_callbacks from anomalib.data import get_datamodule from anomalib.models import get_model +from anomalib.utils.callbacks import get_callbacks def get_args() -> Namespace: diff --git a/tools/train.py b/tools/train.py index d1139a8f60..f1a91871ad 100644 --- a/tools/train.py +++ b/tools/train.py @@ -24,10 +24,10 @@ from pytorch_lightning import Trainer, seed_everything from anomalib.config import get_configurable_parameters -from anomalib.core.callbacks import get_callbacks from anomalib.data import get_datamodule -from anomalib.loggers import get_logger from anomalib.models import get_model +from anomalib.utils.callbacks import get_callbacks +from anomalib.utils.loggers import get_logger def get_args() -> Namespace: diff --git a/tox.ini b/tox.ini index e38866c345..785ff71180 100644 --- a/tox.ini +++ b/tox.ini @@ -74,7 +74,8 @@ deps = commands = coverage erase coverage run --include=anomalib/* -m pytest tests/pre_merge/ -ra - coverage report -m --fail-under=90 + ; https://github.com/openvinotoolkit/anomalib/issues/94 + coverage report -m --fail-under=85 coverage xml -o {toxworkdir}/coverage.xml [testenv:nightly] @@ -93,6 +94,7 @@ deps = commands = coverage erase coverage run --include=anomalib/* -m pytest tests/nightly/ -ra + ; https://github.com/openvinotoolkit/anomalib/issues/94 coverage report -m --fail-under=70 coverage xml -o {toxworkdir}/coverage.xml