diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 399b0a7d5e..9a62d20c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,6 +63,11 @@ repos: types: [python] exclude: "tests|docs" + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + # notebooks. - repo: https://github.com/nbQA-dev/nbQA rev: 1.4.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b4902538..2df1029957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Add `pyupgrade` to `pre-commit` configs, and refactor based on `pyupgrade` and `refurb` () - Add [CFA](https://arxiv.org/abs/2206.04325) model implementation () - Add RKDE model implementation () - Add Visual Anomaly (VisA) dataset adapter () diff --git a/anomalib/config/config.py b/anomalib/config/config.py index 918400a6d8..1d88a0d5b4 100644 --- a/anomalib/config/config.py +++ b/anomalib/config/config.py @@ -6,10 +6,11 @@ # TODO: This would require a new design. # TODO: https://jira.devtools.intel.com/browse/IAAALD-149 +from __future__ import annotations + import time from datetime import datetime from pathlib import Path -from typing import List, Optional, Union from warnings import warn from omegaconf import DictConfig, ListConfig, OmegaConf @@ -22,17 +23,17 @@ def _get_now_str(timestamp: float) -> str: return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d_%H-%M-%S") -def update_input_size_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]: +def update_input_size_config(config: DictConfig | ListConfig) -> DictConfig | ListConfig: """Update config with image size as tuple, effective input size and tiling stride. Convert integer image size parameters into tuples, calculate the effective input size based on image size and crop size, and set tiling stride if undefined. Args: - config (Union[DictConfig, ListConfig]): Configurable parameters object + config (DictConfig | ListConfig): Configurable parameters object Returns: - Union[DictConfig, ListConfig]: Configurable parameters with updated values + DictConfig | ListConfig: Configurable parameters with updated values """ # Image size: Ensure value is in the form [height, width] image_size = config.dataset.get("image_size") @@ -65,14 +66,14 @@ def update_input_size_config(config: Union[DictConfig, ListConfig]) -> Union[Dic return config -def update_nncf_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]: +def update_nncf_config(config: DictConfig | ListConfig) -> DictConfig | ListConfig: """Set the NNCF input size based on the value of the crop_size parameter in the configurable parameters object. - Args: - config (Union[DictConfig, ListConfig]): Configurable parameters of the current run. + Args + config (DictConfig | ListConfig): Configurable parameters of the current run. Returns: - Union[DictConfig, ListConfig]: Updated configurable parameters in DictConfig object. + DictConfig | ListConfig: Updated configurable parameters in DictConfig object. """ crop_size = config.dataset.image_size sample_size = (crop_size, crop_size) if isinstance(crop_size, int) else crop_size @@ -87,19 +88,19 @@ def update_nncf_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfi return config -def update_multi_gpu_training_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]: +def update_multi_gpu_training_config(config: DictConfig | ListConfig) -> DictConfig | ListConfig: """Updates the config to change learning rate based on number of gpus assigned. Current behaviour is to ensure only ddp accelerator is used. Args: - config (Union[DictConfig, ListConfig]): Configurable parameters for the current run + config (DictConfig | ListConfig): Configurable parameters for the current run Raises: ValueError: If unsupported accelerator is passed Returns: - Union[DictConfig, ListConfig]: Updated config + DictConfig | ListConfig: Updated config """ # validate accelerator if config.trainer.accelerator is not None: @@ -119,7 +120,7 @@ def update_multi_gpu_training_config(config: Union[DictConfig, ListConfig]) -> U # increase the learning rate by the number of devices if "lr" in config.model: # Number of GPUs can either be passed as gpus: 2 or gpus: [0,1] - n_gpus: Union[int, List] = 1 + n_gpus: int | list = 1 if "trainer" in config and "gpus" in config.trainer: n_gpus = config.trainer.gpus lr_scaler = n_gpus if isinstance(n_gpus, int) else len(n_gpus) @@ -127,14 +128,14 @@ def update_multi_gpu_training_config(config: Union[DictConfig, ListConfig]) -> U return config -def update_datasets_config(config: Union[DictConfig, ListConfig]) -> Union[DictConfig, ListConfig]: +def update_datasets_config(config: DictConfig | ListConfig) -> DictConfig | ListConfig: """Updates the dataset section of the config. Args: - config (Union[DictConfig, ListConfig]): Configurable parameters for the current run. + config (DictConfig | ListConfig): Configurable parameters for the current run. Returns: - Union[DictConfig, ListConfig]: Updated config + DictConfig | ListConfig: Updated config """ if "format" not in config.dataset.keys(): config.dataset.format = "mvtec" @@ -200,25 +201,25 @@ def update_datasets_config(config: Union[DictConfig, ListConfig]) -> Union[DictC def get_configurable_parameters( - model_name: Optional[str] = None, - config_path: Optional[Union[Path, str]] = None, - weight_file: Optional[str] = None, - config_filename: Optional[str] = "config", - config_file_extension: Optional[str] = "yaml", -) -> Union[DictConfig, ListConfig]: + model_name: str | None = None, + config_path: Path | str | None = None, + weight_file: str | None = None, + config_filename: str | None = "config", + config_file_extension: str | None = "yaml", +) -> DictConfig | ListConfig: """Get configurable parameters. Args: - model_name: Optional[str]: (Default value = None) - config_path: Optional[Union[Path, str]]: (Default value = None) + model_name: str | None: (Default value = None) + config_path: Path | str | None: (Default value = None) weight_file: Path to the weight file - config_filename: Optional[str]: (Default value = "config") - config_file_extension: Optional[str]: (Default value = "yaml") + config_filename: str | None: (Default value = "config") + config_file_extension: str | None: (Default value = "yaml") Returns: - Union[DictConfig, ListConfig]: Configurable parameters in DictConfig object. + DictConfig | ListConfig: Configurable parameters in DictConfig object. """ - if model_name is None and config_path is None: + if model_name is None is config_path: raise ValueError( "Both model_name and model config path cannot be None! " "Please provide a model name or path to a config file!" diff --git a/anomalib/data/__init__.py b/anomalib/data/__init__.py index b89fad0e0c..09544ed139 100644 --- a/anomalib/data/__init__.py +++ b/anomalib/data/__init__.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Union from omegaconf import DictConfig, ListConfig @@ -21,11 +22,11 @@ logger = logging.getLogger(__name__) -def get_datamodule(config: Union[DictConfig, ListConfig]) -> AnomalibDataModule: +def get_datamodule(config: DictConfig | ListConfig) -> AnomalibDataModule: """Get Anomaly Datamodule. Args: - config (Union[DictConfig, ListConfig]): Configuration of the anomaly model. + config (DictConfig | ListConfig): Configuration of the anomaly model. Returns: PyTorch Lightning DataModule diff --git a/anomalib/data/avenue.py b/anomalib/data/avenue.py index 8197e374ef..f52d6f89a4 100644 --- a/anomalib/data/avenue.py +++ b/anomalib/data/avenue.py @@ -13,18 +13,19 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import math from pathlib import Path from shutil import move -from typing import Callable, Optional, Tuple, Union +from typing import Callable import albumentations as A import cv2 import numpy as np import scipy.io from pandas import DataFrame -from torch import Tensor from anomalib.data.base import AnomalibVideoDataModule, AnomalibVideoDataset from anomalib.data.task_type import TaskType @@ -52,7 +53,7 @@ ) -def make_avenue_dataset(root: Path, gt_dir: Path, split: Optional[Union[Split, str]] = None) -> DataFrame: +def make_avenue_dataset(root: Path, gt_dir: Path, split: Split | str | None = None) -> DataFrame: """Create CUHK Avenue dataset by parsing the file structure. The files are expected to follow the structure: @@ -62,7 +63,7 @@ def make_avenue_dataset(root: Path, gt_dir: Path, split: Optional[Union[Split, s Args: root (Path): Path to dataset gt_dir (Path): Path to the ground truth - split (Optional[Union[Split, str]], optional): Dataset split (ie., either train or test). Defaults to None. + split (Split | str | None = None, optional): Dataset split (ie., either train or test). Defaults to None. Example: The following example shows how to get testing samples from Avenue dataset: @@ -106,7 +107,7 @@ def make_avenue_dataset(root: Path, gt_dir: Path, split: Optional[Union[Split, s class AvenueClipsIndexer(ClipsIndexer): """Clips class for UCSDped dataset.""" - def get_mask(self, idx) -> Optional[Tensor]: + def get_mask(self, idx) -> np.ndarray | None: """Retrieve the masks from the file system.""" video_idx, frames_idx = self.get_clip_location(idx) @@ -133,10 +134,10 @@ class AvenueDataset(AnomalibVideoDataset): Args: task (TaskType): Task type, 'classification', 'detection' or 'segmentation' - root (str): Path to the root of the dataset + root (Path | str): Path to the root of the dataset gt_dir (str): Path to the ground truth files transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. - split (Optional[Union[Split, str]]): Split of the dataset, usually Split.TRAIN or Split.TEST + split (Split): Split of the dataset, usually Split.TRAIN or Split.TEST clip_length_in_frames (int, optional): Number of video frames in each clip. frames_between_clips (int, optional): Number of frames between each consecutive video clip. """ @@ -144,21 +145,21 @@ class AvenueDataset(AnomalibVideoDataset): def __init__( self, task: TaskType, - root: Union[Path, str], + root: Path | str, gt_dir: str, transform: A.Compose, split: Split, clip_length_in_frames: int = 1, frames_between_clips: int = 1, - ): + ) -> None: super().__init__(task, transform, clip_length_in_frames, frames_between_clips) - self.root = root - self.gt_dir = gt_dir + self.root = root if isinstance(root, Path) else Path(root) + self.gt_dir = Path(gt_dir) self.split = split self.indexer_cls: Callable = AvenueClipsIndexer - def _setup(self): + def _setup(self) -> None: """Create and assign samples.""" self.samples = make_avenue_dataset(self.root, self.gt_dir, self.split) @@ -172,23 +173,23 @@ class Avenue(AnomalibVideoDataModule): clip_length_in_frames (int, optional): Number of video frames in each clip. frames_between_clips (int, optional): Number of frames between each consecutive video clip. task TaskType): Task type, 'classification', 'detection' or 'segmentation' - image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image. + image_size (int | tuple[int, int] | None, optional): Size of the input image. Defaults to None. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. train_batch_size (int, optional): Training batch size. Defaults to 32. eval_batch_size (int, optional): Test batch size. Defaults to 32. num_workers (int, optional): Number of workers. Defaults to 8. - transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. Defaults to None. - transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_val (str | A.Compose | None, optional): Config for pre-processing during validation. Defaults to None. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed which may be set to a fixed value for reproducibility. + seed (int | None, optional): Seed which may be set to a fixed value for reproducibility. """ def __init__( @@ -198,18 +199,18 @@ def __init__( clip_length_in_frames: int = 1, frames_between_clips: int = 1, task: TaskType = TaskType.SEGMENTATION, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, val_split_mode: ValSplitMode = ValSplitMode.FROM_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, - ): + seed: int | None = None, + ) -> None: super().__init__( train_batch_size=train_batch_size, eval_batch_size=eval_batch_size, @@ -275,7 +276,7 @@ def prepare_data(self) -> None: self._convert_masks(self.gt_dir) @staticmethod - def _convert_masks(gt_dir: Path): + def _convert_masks(gt_dir: Path) -> None: """Convert mask files to .png. The masks in the Avenue datasets are provided as matlab (.mat) files. To speed up data loading, we convert the diff --git a/anomalib/data/base/datamodule.py b/anomalib/data/base/datamodule.py index 9365ae901b..f3dfbbf256 100644 --- a/anomalib/data/base/datamodule.py +++ b/anomalib/data/base/datamodule.py @@ -7,7 +7,7 @@ import logging from abc import ABC -from typing import Any, Dict, List, Optional +from typing import Any from pandas import DataFrame from pytorch_lightning import LightningDataModule @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) -def collate_fn(batch: List) -> Dict[str, Any]: +def collate_fn(batch: list) -> dict[str, Any]: """Custom collate function that collates bounding boxes as lists. Bounding boxes are collated as a list of tensors, while the default collate function is used for all other entries. @@ -35,7 +35,7 @@ def collate_fn(batch: List) -> Dict[str, Any]: batch (List): list of items in the batch where len(batch) is equal to the batch size. Returns: - Dict[str, Any]: Dictionary containing the collated batch information. + dict[str, Any]: Dictionary containing the collated batch information. """ elem = batch[0] # sample an element from the batch to check the type. out_dict = {} @@ -62,7 +62,7 @@ class AnomalibDataModule(LightningDataModule, ABC): val_split_mode (ValSplitMode): Determines how the validation split is obtained. Options: [none, same_as_test, from_test, synthetic] val_split_ratio (float): Fraction of the train or test images held our for validation. - seed (Optional[int], optional): Seed used during random subset splitting. + seed (int | None, optional): Seed used during random subset splitting. """ def __init__( @@ -72,10 +72,10 @@ def __init__( num_workers: int, val_split_mode: ValSplitMode, val_split_ratio: float, - test_split_mode: Optional[TestSplitMode] = None, - test_split_ratio: Optional[float] = None, - seed: Optional[int] = None, - ): + test_split_mode: TestSplitMode | None = None, + test_split_ratio: float | None = None, + seed: int | None = None, + ) -> None: super().__init__() self.train_batch_size = train_batch_size self.eval_batch_size = eval_batch_size @@ -86,23 +86,23 @@ def __init__( self.val_split_ratio = val_split_ratio self.seed = seed - self.train_data: Optional[AnomalibDataset] = None - self.val_data: Optional[AnomalibDataset] = None - self.test_data: Optional[AnomalibDataset] = None + self.train_data: AnomalibDataset + self.val_data: AnomalibDataset + self.test_data: AnomalibDataset - self._samples: Optional[DataFrame] = None + self._samples: DataFrame | None = None - def setup(self, stage: Optional[str] = None): + def setup(self, stage: str | None = None) -> None: """Setup train, validation and test data. Args: - stage: Optional[str]: Train/Val/Test stages. (Default value = None) + stage: str | None: Train/Val/Test stages. (Default value = None) """ if not self.is_setup: self._setup(stage) assert self.is_setup - def _setup(self, _stage: Optional[str] = None) -> None: + def _setup(self, _stage: str | None = None) -> None: """Set up the datasets and perform dynamic subset splitting. This method may be overridden in subclass for custom splitting behaviour. @@ -121,7 +121,7 @@ def _setup(self, _stage: Optional[str] = None) -> None: self._create_test_split() self._create_val_split() - def _create_test_split(self): + def _create_test_split(self) -> None: """Obtain the test set based on the settings in the config.""" if self.test_data.has_normal: # split the test data into normal and anomalous so these can be processed separately @@ -133,7 +133,8 @@ def _create_test_split(self): "No normal test images found. Sampling from training set using a split ratio of %d", self.test_split_ratio, ) - self.train_data, normal_test_data = random_split(self.train_data, self.test_split_ratio) + if self.test_split_ratio is not None: + self.train_data, normal_test_data = random_split(self.train_data, self.test_split_ratio) if self.test_split_mode == TestSplitMode.FROM_DIR: self.test_data += normal_test_data @@ -142,7 +143,7 @@ def _create_test_split(self): elif self.test_split_mode != TestSplitMode.NONE: raise ValueError(f"Unsupported Test Split Mode: {self.test_split_mode}") - def _create_val_split(self): + def _create_val_split(self) -> None: """Obtain the validation set based on the settings in the config.""" if self.val_split_mode == ValSplitMode.FROM_TEST: # randomly sampled from test set @@ -160,16 +161,18 @@ def _create_val_split(self): raise ValueError(f"Unknown validation split mode: {self.val_split_mode}") @property - def is_setup(self): - """Checks if setup() has been called.""" - # at least one of [train_data, val_data, test_data] should be setup - if self.train_data is not None and self.train_data.is_setup: - return True - if self.val_data is not None and self.val_data.is_setup: - return True - if self.test_data is not None and self.test_data.is_setup: - return True - return False + def is_setup(self) -> bool: + """Checks if setup() has been called. + + At least one of [train_data, val_data, test_data] should be setup. + """ + _is_setup: bool = False + for data in ("train_data", "val_data", "test_data"): + if hasattr(self, data): + if getattr(self, data).is_setup: + _is_setup = True + + return _is_setup def train_dataloader(self) -> TRAIN_DATALOADERS: """Get train dataloader.""" diff --git a/anomalib/data/base/dataset.py b/anomalib/data/base/dataset.py index 8d239a5e7d..b0e3f70da9 100644 --- a/anomalib/data/base/dataset.py +++ b/anomalib/data/base/dataset.py @@ -9,7 +9,7 @@ import logging from abc import ABC, abstractmethod from pathlib import Path -from typing import Dict, Sequence, Union +from typing import Sequence import albumentations as A import cv2 @@ -41,7 +41,7 @@ class AnomalibDataset(Dataset, ABC): transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. """ - def __init__(self, task: TaskType, transform: A.Compose): + def __init__(self, task: TaskType, transform: A.Compose) -> None: super().__init__() self.task = task self.transform = transform @@ -76,7 +76,7 @@ def samples(self) -> DataFrame: return self._samples @samples.setter - def samples(self, samples: DataFrame): + def samples(self, samples: DataFrame) -> None: """Overwrite the samples with a new dataframe. Args: @@ -102,14 +102,14 @@ def has_anomalous(self) -> bool: """Check if the dataset contains any anomalous samples.""" return 1 in list(self.samples.label_index) - def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]: + def __getitem__(self, index: int) -> dict[str, str | Tensor]: """Get dataset item for the index ``index``. Args: index (int): Index to get the item. Returns: - Union[Dict[str, Tensor], Dict[str, Union[str, Tensor]]]: Dict of image tensor during training. + Union[dict[str, Tensor], dict[str, str | Tensor]]: Dict of image tensor during training. Otherwise, Dict containing image path, target path, image tensor, label and transformed bounding box. """ diff --git a/anomalib/data/base/video.py b/anomalib/data/base/video.py index 22148c5786..9b5bcfb4aa 100644 --- a/anomalib/data/base/video.py +++ b/anomalib/data/base/video.py @@ -1,7 +1,9 @@ """Base Video Dataset.""" +from __future__ import annotations + from abc import ABC -from typing import Callable, Dict, Optional, Union +from typing import Callable import albumentations as A import torch @@ -25,15 +27,17 @@ class AnomalibVideoDataset(AnomalibDataset, ABC): frames_between_clips (int): Number of frames between each consecutive video clip. """ - def __init__(self, task: TaskType, transform: A.Compose, clip_length_in_frames: int, frames_between_clips: int): + def __init__( + self, task: TaskType, transform: A.Compose, clip_length_in_frames: int, frames_between_clips: int + ) -> None: super().__init__(task, transform) self.clip_length_in_frames = clip_length_in_frames self.frames_between_clips = frames_between_clips self.transform = transform - self.indexer: Optional[ClipsIndexer] = None - self.indexer_cls: Optional[Callable] = None + self.indexer: ClipsIndexer | None = None + self.indexer_cls: Callable | None = None def __len__(self) -> int: """Get length of the dataset.""" @@ -64,7 +68,7 @@ def _setup_clips(self) -> None: frames_between_clips=self.frames_between_clips, ) - def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]: + def __getitem__(self, index: int) -> dict[str, str | Tensor]: """Return mask, clip and file system information.""" assert isinstance(self.indexer, ClipsIndexer) @@ -98,7 +102,7 @@ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]: class AnomalibVideoDataModule(AnomalibDataModule): """Base class for video data modules.""" - def _setup(self, _stage: Optional[str] = None) -> None: + def _setup(self, _stage: str | None = None) -> None: """Set up the datasets and perform dynamic subset splitting. This method may be overridden in subclass for custom splitting behaviour. diff --git a/anomalib/data/btech.py b/anomalib/data/btech.py index 80a1ffe733..87bbcc5a50 100644 --- a/anomalib/data/btech.py +++ b/anomalib/data/btech.py @@ -9,10 +9,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import shutil from pathlib import Path -from typing import Optional, Tuple, Union import albumentations as A import cv2 @@ -40,7 +41,7 @@ ) -def make_btech_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> DataFrame: +def make_btech_dataset(path: Path, split: str | Split | None = None) -> DataFrame: """Create BTech samples by parsing the BTech data file structure. The files are expected to follow the structure: @@ -49,7 +50,7 @@ def make_btech_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> Args: path (Path): Path to dataset - split (Optional[Union[Split, str]], optional): Dataset split (ie., either train or test). Defaults to None. + split (str | Split | None, optional): Dataset split (ie., either train or test). Defaults to None. split_ratio (float, optional): Ratio to split normal training images and add to the test set in case test set doesn't contain any normal images. Defaults to 0.1. @@ -80,7 +81,7 @@ def make_btech_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> samples_list = [ (str(path),) + filename.parts[-3:] for filename in path.glob("**/*") if filename.suffix in (".bmp", ".png") ] - if len(samples_list) == 0: + if not samples_list: raise RuntimeError(f"Found 0 images in {path}") samples = pd.DataFrame(samples_list, columns=["path", "split", "label", "image_path"]) @@ -159,10 +160,10 @@ class BTechDataset(AnomalibDataset): def __init__( self, - root: Union[Path, str], + root: str | Path, category: str, transform: A.Compose, - split: Optional[Union[Split, str]] = None, + split: str | Split | None = None, task: TaskType = TaskType.SEGMENTATION, ) -> None: super().__init__(task, transform) @@ -170,7 +171,7 @@ def __init__( self.root_category = Path(root) / category self.split = split - def _setup(self): + def _setup(self) -> None: self.samples = make_btech_dataset(path=self.root_category, split=self.split) @@ -179,25 +180,36 @@ class BTech(AnomalibDataModule): """BTech Lightning Data Module. Args: - root: Path to the BTech dataset - category: Name of the BTech category. - image_size: Variable to which image is resized. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped - to the provided dimensions. - normalize (bool): When True, the images will be normalized to the ImageNet statistics. - train_batch_size: Training batch size. - test_batch_size: Testing batch size. - num_workers: Number of workers. - task: ``classification``, ``detection`` or ``segmentation`` - transform_config_train: Config for pre-processing during training. - transform_config_val: Config for pre-processing during validation. - create_validation_set: Create a validation subset in addition to the train and test subsets - seed (Optional[int], optional): Seed used during random subset splitting. - test_split_mode (TestSplitMode): Setting that determines how the testing subset is obtained. - test_split_ratio (float): Fraction of images from the train set that will be reserved for testing. - val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. - val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed which may be set to a fixed value for reproducibility. + + root (str): Path to the BTech dataset. + category (str): Name of the BTech category. + image_size (int | tuple[int, int] | None, optional): Variable to which image is resized. Defaults to None. + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the + provided dimensions. + Defaults to None. + normalization (str | InputNormalizationMethod, optional): When True, the images will be normalized to the + ImageNet statistics. + Defaults to InputNormalizationMethod.IMAGENET. + train_batch_size (int, optional): Training batch size. + Defaults to 32. + eval_batch_size (int, optional): Eval batch size. + Defaults to 32. + num_workers (int, optional): Number of workers. Defaults to 8. + task (TaskType, optional): Task type. + Defaults to TaskType.SEGMENTATION. + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. + Defaults to None. + transform_config_eval (str | A.Compose | None, optional): Config for pre-processing during validation. + Defaults to None. + test_split_mode (TestSplitMode, optional): Setting that determines how the testing subset is obtained. + Defaults to TestSplitMode.FROM_DIR. + test_split_ratio (float, optional): Fraction of images from the train set that will be reserved for testing. + Defaults to 0.2. + val_split_mode (ValSplitMode, optional): Setting that determines how the validation subset is obtained. + Defaults to ValSplitMode.SAME_AS_TEST. + val_split_ratio (float, optional): Fraction of train or test images that will be reserved for validation. + Defaults to 0.5. + seed (int | None, optional): Seed which may be set to a fixed value for reproducibility. Defaults to None. Examples: >>> from anomalib.data import BTech @@ -230,20 +242,20 @@ def __init__( self, root: str, category: str, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, task: TaskType = TaskType.SEGMENTATION, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, test_split_mode: TestSplitMode = TestSplitMode.FROM_DIR, test_split_ratio: float = 0.2, val_split_mode: ValSplitMode = ValSplitMode.SAME_AS_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, + seed: int | None = None, ) -> None: super().__init__( train_batch_size=train_batch_size, diff --git a/anomalib/data/folder.py b/anomalib/data/folder.py index 5e29b0bbb2..d0a7892abc 100644 --- a/anomalib/data/folder.py +++ b/anomalib/data/folder.py @@ -6,8 +6,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path -from typing import Optional, Tuple, Union import albumentations as A from pandas import DataFrame @@ -24,11 +25,11 @@ ) -def _check_and_convert_path(path: Union[str, Path]) -> Path: +def _check_and_convert_path(path: str | Path) -> Path: """Check an input path, and convert to Pathlib object. Args: - path (Union[str, Path]): Input path. + path (str | Path): Input path. Returns: Path: Output path converted to pathlib object. @@ -39,14 +40,14 @@ def _check_and_convert_path(path: Union[str, Path]) -> Path: def _prepare_files_labels( - path: Union[str, Path], path_type: str, extensions: Optional[Tuple[str, ...]] = None -) -> Tuple[list, list]: + path: str | Path, path_type: str, extensions: tuple[str, ...] | None = None +) -> tuple[list, list]: """Return a list of filenames and list corresponding labels. Args: - path (Union[str, Path]): Path to the directory containing images. + path (str | Path): Path to the directory containing images. path_type (str): Type of images in the provided path ("normal", "abnormal", "normal_test") - extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the + extensions (tuple[str, ...] | None, optional): Type of the image extensions to read from the directory. Returns: @@ -60,7 +61,7 @@ def _prepare_files_labels( extensions = (extensions,) filenames = [f for f in path.glob(r"**/*") if f.suffix in extensions and not f.is_dir()] - if len(filenames) == 0: + if not filenames: raise RuntimeError(f"Found 0 {path_type} images in {path}") labels = [path_type] * len(filenames) @@ -68,15 +69,15 @@ def _prepare_files_labels( return filenames, labels -def _resolve_path(folder: Union[Path, str], root: Optional[Union[Path, str]] = None) -> Path: +def _resolve_path(folder: str | Path, root: str | Path | None = None) -> Path: """Combines root and folder and returns the absolute path. This allows users to pass either a root directory and relative paths, or absolute paths to each of the image sources. This function makes sure that the samples dataframe always contains absolute paths. Args: - folder (Optional[Union[Path, str]]): Folder location containing image or mask data. - root (Optional[Union[Path, str]]): Root directory for the dataset. + folder (str | Path | None): Folder location containing image or mask data. + root (str | Path | None): Root directory for the dataset. """ folder = Path(folder) if folder.is_absolute(): @@ -93,28 +94,28 @@ def _resolve_path(folder: Union[Path, str], root: Optional[Union[Path, str]] = N def make_folder_dataset( - normal_dir: Union[str, Path], - root: Optional[Union[str, Path]] = None, - abnormal_dir: Optional[Union[str, Path]] = None, - normal_test_dir: Optional[Union[str, Path]] = None, - mask_dir: Optional[Union[str, Path]] = None, - split: Optional[Union[Split, str]] = None, - extensions: Optional[Tuple[str, ...]] = None, -): + normal_dir: str | Path, + root: str | Path | None = None, + abnormal_dir: str | Path | None = None, + normal_test_dir: str | Path | None = None, + mask_dir: str | Path | None = None, + split: str | Split | None = None, + extensions: tuple[str, ...] | None = None, +) -> DataFrame: """Make Folder Dataset. Args: - normal_dir (Union[str, Path]): Path to the directory containing normal images. - root (Optional[Union[str, Path]]): Path to the root directory of the dataset. - abnormal_dir (Optional[Union[str, Path]], optional): Path to the directory containing abnormal images. - normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing + normal_dir (str | Path): Path to the directory containing normal images. + root (str | Path | None): Path to the root directory of the dataset. + abnormal_dir (str | Path | None, optional): Path to the directory containing abnormal images. + normal_test_dir (str | Path | None, optional): Path to the directory containing normal images for the test dataset. Normal test images will be a split of `normal_dir` if `None`. Defaults to None. - mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing + mask_dir (str | Path | None, optional): Path to the directory containing the mask annotations. Defaults to None. - split (Optional[Union[Split, str]], optional): Dataset split (ie., Split.FULL, Split.TRAIN or Split.TEST). + split (str | Split | None, optional): Dataset split (ie., Split.FULL, Split.TRAIN or Split.TEST). Defaults to None. - extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the + extensions (tuple[str, ...] | None, optional): Type of the image extensions to read from the directory. Returns: @@ -186,17 +187,17 @@ class FolderDataset(AnomalibDataset): Args: task (TaskType): Task type. (``classification``, ``detection`` or ``segmentation``). transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. - split (Optional[Union[Split, str]]): Fixed subset split that follows from folder structure on file system. + split (str | Split | None): Fixed subset split that follows from folder structure on file system. Choose from [Split.FULL, Split.TRAIN, Split.TEST] - normal_dir (Union[str, Path]): Path to the directory containing normal images. - root (Optional[Union[str, Path]]): Root folder of the dataset. - abnormal_dir (Optional[Union[str, Path]], optional): Path to the directory containing abnormal images. - normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing + normal_dir (str | Path): Path to the directory containing normal images. + root (str | Path | None): Root folder of the dataset. + abnormal_dir (str | Path | None, optional): Path to the directory containing abnormal images. + normal_test_dir (str | Path | None, optional): Path to the directory containing normal images for the test dataset. Defaults to None. - mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing + mask_dir (str | Path | None, optional): Path to the directory containing the mask annotations. Defaults to None. - extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the + extensions (tuple[str, ...] | None, optional): Type of the image extensions to read from the directory. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. @@ -209,13 +210,13 @@ def __init__( self, task: TaskType, transform: A.Compose, - normal_dir: Union[str, Path], - root: Optional[Union[str, Path]] = None, - abnormal_dir: Optional[Union[str, Path]] = None, - normal_test_dir: Optional[Union[str, Path]] = None, - mask_dir: Optional[Union[str, Path]] = None, - split: Optional[Union[Split, str]] = None, - extensions: Optional[Tuple[str, ...]] = None, + normal_dir: str | Path, + root: str | Path | None = None, + abnormal_dir: str | Path | None = None, + normal_test_dir: str | Path | None = None, + mask_dir: str | Path | None = None, + split: str | Split | None = None, + extensions: tuple[str, ...] | None = None, ) -> None: super().__init__(task, transform) @@ -227,7 +228,7 @@ def __init__( self.mask_dir = mask_dir self.extensions = extensions - def _setup(self): + def _setup(self) -> None: """Assign samples.""" self.samples = make_folder_dataset( root=self.root, @@ -244,23 +245,23 @@ class Folder(AnomalibDataModule): """Folder DataModule. Args: - normal_dir (Union[str, Path]): Name of the directory containing normal images. + normal_dir (str | Path): Name of the directory containing normal images. Defaults to "normal". - root (Optional[Union[str, Path]]): Path to the root folder containing normal and abnormal dirs. - abnormal_dir (Optional[Union[str, Path]]): Name of the directory containing abnormal images. + root (str | Path | None): Path to the root folder containing normal and abnormal dirs. + abnormal_dir (str | Path | None): Name of the directory containing abnormal images. Defaults to "abnormal". - normal_test_dir (Optional[Union[str, Path]], optional): Path to the directory containing + normal_test_dir (str | Path | None, optional): Path to the directory containing normal images for the test dataset. Defaults to None. - mask_dir (Optional[Union[str, Path]], optional): Path to the directory containing + mask_dir (str | Path | None, optional): Path to the directory containing the mask annotations. Defaults to None. normal_split_ratio (float, optional): Ratio to split normal training images and add to the test set in case test set doesn't contain any normal images. Defaults to 0.2. - extensions (Optional[Tuple[str, ...]], optional): Type of the image extensions to read from the + extensions (tuple[str, ...] | None, optional): Type of the image extensions to read from the directory. Defaults to None. - image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image. + image_size (int | tuple[int, int] | None, optional): Size of the input image. Defaults to None. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. train_batch_size (int, optional): Training batch size. Defaults to 32. @@ -268,43 +269,43 @@ class Folder(AnomalibDataModule): num_workers (int, optional): Number of workers. Defaults to 8. task (TaskType, optional): Task type. Could be ``classification``, ``detection`` or ``segmentation``. Defaults to segmentation. - transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. Defaults to None. - transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_val (str | A.Compose | None, optional): Config for pre-processing during validation. Defaults to None. test_split_mode (TestSplitMode): Setting that determines how the testing subset is obtained. test_split_ratio (float): Fraction of images from the train set that will be reserved for testing. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed used during random subset splitting. + seed (int | None, optional): Seed used during random subset splitting. """ def __init__( self, - normal_dir: Union[str, Path], - root: Optional[Union[str, Path]] = None, - abnormal_dir: Optional[Union[str, Path]] = None, - normal_test_dir: Optional[Union[str, Path]] = None, - mask_dir: Optional[Union[str, Path]] = None, + normal_dir: str | Path, + root: str | Path | None = None, + abnormal_dir: str | Path | None = None, + normal_test_dir: str | Path | None = None, + mask_dir: str | Path | None = None, normal_split_ratio: float = 0.2, - extensions: Optional[Tuple[str]] = None, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + extensions: tuple[str] | None = None, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, task: TaskType = TaskType.SEGMENTATION, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, test_split_mode: TestSplitMode = TestSplitMode.FROM_DIR, test_split_ratio: float = 0.2, val_split_mode: ValSplitMode = ValSplitMode.FROM_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, - ): + seed: int | None = None, + ) -> None: super().__init__( train_batch_size=train_batch_size, eval_batch_size=eval_batch_size, diff --git a/anomalib/data/inference.py b/anomalib/data/inference.py index ebb98e2287..ba43ce0d75 100644 --- a/anomalib/data/inference.py +++ b/anomalib/data/inference.py @@ -3,8 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path -from typing import Any, Optional, Tuple, Union +from typing import Any import albumentations as A from torch.utils.data.dataset import Dataset @@ -16,18 +18,18 @@ class InferenceDataset(Dataset): """Inference Dataset to perform prediction. Args: - path (Union[str, Path]): Path to an image or image-folder. - transform (Optional[A.Compose], optional): Albumentations Compose object describing the transforms that are + path (str | Path): Path to an image or image-folder. + transform (A.Compose | None, optional): Albumentations Compose object describing the transforms that are applied to the inputs. - image_size (Optional[Union[int, Tuple[int, int]]], optional): Target image size + image_size (int | tuple[int, int] | None, optional): Target image size to resize the original image. Defaults to None. """ def __init__( self, - path: Union[str, Path], - transform: Optional[A.Compose] = None, - image_size: Optional[Union[int, Tuple[int, int]]] = None, + path: str | Path, + transform: A.Compose | None = None, + image_size: int | tuple[int, int] | None = None, ) -> None: super().__init__() diff --git a/anomalib/data/mvtec.py b/anomalib/data/mvtec.py index e5048ac66c..963a2c25bd 100644 --- a/anomalib/data/mvtec.py +++ b/anomalib/data/mvtec.py @@ -23,9 +23,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from pathlib import Path -from typing import Optional, Sequence, Tuple, Union +from typing import Sequence import albumentations as A from pandas import DataFrame @@ -56,7 +58,7 @@ def make_mvtec_dataset( - root: Union[str, Path], split: Optional[Union[Split, str]] = None, extensions: Optional[Sequence[str]] = None + root: str | Path, split: str | Split | None = None, extensions: Sequence[str] | None = None ) -> DataFrame: """Create MVTec AD samples by parsing the MVTec AD data file structure. @@ -73,7 +75,7 @@ def make_mvtec_dataset( Args: path (Path): Path to dataset - split (Optional[Union[Split, str]], optional): Dataset split (ie., either train or test). Defaults to None. + split (str | Split | None, optional): Dataset split (ie., either train or test). Defaults to None. split_ratio (float, optional): Ratio to split normal training images and add to the test set in case test set doesn't contain any normal images. Defaults to 0.1. @@ -108,7 +110,7 @@ def make_mvtec_dataset( root = Path(root) samples_list = [(str(root),) + f.parts[-3:] for f in root.glob(r"**/*") if f.suffix in extensions] - if len(samples_list) == 0: + if not samples_list: raise RuntimeError(f"Found 0 images in {root}") samples = DataFrame(samples_list, columns=["path", "split", "label", "image_path"]) @@ -150,7 +152,7 @@ class MVTecDataset(AnomalibDataset): Args: task (TaskType): Task type, ``classification``, ``detection`` or ``segmentation`` transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. - split (Optional[Union[Split, str]]): Split of the dataset, usually Split.TRAIN or Split.TEST + split (str | Split | None): Split of the dataset, usually Split.TRAIN or Split.TEST root (str): Path to the root of the dataset category (str): Sub-category of the dataset, e.g. 'bottle' """ @@ -161,14 +163,14 @@ def __init__( transform: A.Compose, root: str, category: str, - split: Optional[Union[Split, str]] = None, + split: str | Split | None = None, ) -> None: super().__init__(task=task, transform=transform) self.root_category = Path(root) / Path(category) self.split = split - def _setup(self): + def _setup(self) -> None: self.samples = make_mvtec_dataset(self.root_category, split=self.split, extensions=IMG_EXTENSIONS) @@ -178,47 +180,47 @@ class MVTec(AnomalibDataModule): Args: root (str): Path to the root of the dataset category (str): Category of the MVTec dataset (e.g. "bottle" or "cable"). - image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image. + image_size (int | tuple[int, int] | None, optional): Size of the input image. Defaults to None. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. train_batch_size (int, optional): Training batch size. Defaults to 32. eval_batch_size (int, optional): Test batch size. Defaults to 32. num_workers (int, optional): Number of workers. Defaults to 8. task TaskType): Task type, 'classification', 'detection' or 'segmentation' - transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. Defaults to None. - transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_val (str | A.Compose | None, optional): Config for pre-processing during validation. Defaults to None. test_split_mode (TestSplitMode): Setting that determines how the testing subset is obtained. test_split_ratio (float): Fraction of images from the train set that will be reserved for testing. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed which may be set to a fixed value for reproducibility. + seed (int | None, optional): Seed which may be set to a fixed value for reproducibility. """ def __init__( self, root: str, category: str, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, task: TaskType = TaskType.SEGMENTATION, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, test_split_mode: TestSplitMode = TestSplitMode.FROM_DIR, test_split_ratio: float = 0.2, val_split_mode: ValSplitMode = ValSplitMode.SAME_AS_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, - ): + seed: int | None = None, + ) -> None: super().__init__( train_batch_size=train_batch_size, eval_batch_size=eval_batch_size, diff --git a/anomalib/data/synthetic.py b/anomalib/data/synthetic.py index 11ae7d2a5a..6287a88d2c 100644 --- a/anomalib/data/synthetic.py +++ b/anomalib/data/synthetic.py @@ -6,13 +6,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import math import shutil from copy import deepcopy from pathlib import Path from tempfile import mkdtemp -from typing import Dict import albumentations as A import cv2 @@ -109,7 +110,7 @@ class SyntheticAnomalyDataset(AnomalibDataset): source_samples (DataFrame): Normal samples to which the anomalous augmentations will be applied. """ - def __init__(self, task: TaskType, transform: A.Compose, source_samples: DataFrame): + def __init__(self, task: TaskType, transform: A.Compose, source_samples: DataFrame) -> None: super().__init__(task, transform) self.source_samples = source_samples @@ -130,7 +131,7 @@ def __init__(self, task: TaskType, transform: A.Compose, source_samples: DataFra self.setup() @classmethod - def from_dataset(cls, dataset: AnomalibDataset) -> "SyntheticAnomalyDataset": + def from_dataset(cls, dataset: AnomalibDataset) -> SyntheticAnomalyDataset: """Create a synthetic anomaly dataset from an existing dataset of normal images. Args: @@ -139,7 +140,7 @@ def from_dataset(cls, dataset: AnomalibDataset) -> "SyntheticAnomalyDataset": """ return cls(task=dataset.task, transform=dataset.transform, source_samples=dataset.samples) - def __copy__(self) -> "SyntheticAnomalyDataset": + def __copy__(self) -> SyntheticAnomalyDataset: """Returns a shallow copy of the dataset object and prevents cleanup when original object is deleted.""" cls = self.__class__ new = cls.__new__(cls) @@ -147,7 +148,7 @@ def __copy__(self) -> "SyntheticAnomalyDataset": self._cleanup = False return new - def __deepcopy__(self, _memo: Dict) -> "SyntheticAnomalyDataset": + def __deepcopy__(self, _memo: dict) -> SyntheticAnomalyDataset: """Returns a deep copy of the dataset object and prevents cleanup when original object is deleted.""" cls = self.__class__ new = cls.__new__(cls) @@ -161,7 +162,7 @@ def _setup(self) -> None: logger.info("Generating synthetic anomalous images for validation set") self.samples = make_synthetic_dataset(self.source_samples, self.im_dir, self.mask_dir, 0.5) - def __del__(self): + def __del__(self) -> None: """Make sure the temporary directory is cleaned up when the dataset object is deleted.""" if self._cleanup: shutil.rmtree(self.root) diff --git a/anomalib/data/ucsd_ped.py b/anomalib/data/ucsd_ped.py index 88cb8f6b7a..429644a951 100644 --- a/anomalib/data/ucsd_ped.py +++ b/anomalib/data/ucsd_ped.py @@ -3,10 +3,12 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from pathlib import Path from shutil import move -from typing import Any, Callable, Dict, Optional, Tuple, Union +from typing import Any, Callable import albumentations as A import cv2 @@ -37,7 +39,7 @@ ) -def make_ucsd_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> DataFrame: +def make_ucsd_dataset(path: Path, split: str | Split | None = None) -> DataFrame: """Create UCSD Pedestrian dataset by parsing the file structure. The files are expected to follow the structure: @@ -46,7 +48,7 @@ def make_ucsd_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> Args: root (Path): Path to dataset - split (Optional[Union[Split, str]], optional): Dataset split (ie., either train or test). Defaults to None. + split (str | Split | None, optional): Dataset split (ie., either train or test). Defaults to None. Example: The following example shows how to get testing samples from UCSDped2 category: @@ -68,7 +70,7 @@ def make_ucsd_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> DataFrame: an output dataframe containing samples for the requested split (ie., train or test) """ folders = [filename for filename in sorted(path.glob("*/*")) if filename.is_dir()] - folders = [folder for folder in folders if len(list(folder.glob("*.tif"))) > 0] + folders = [folder for folder in folders if list(folder.glob("*.tif"))] samples_list = [(str(path),) + folder.parts[-2:] for folder in folders] samples = DataFrame(samples_list, columns=["root", "folder", "image_path"]) @@ -92,7 +94,7 @@ def make_ucsd_dataset(path: Path, split: Optional[Union[Split, str]] = None) -> class UCSDpedClipsIndexer(ClipsIndexer): """Clips class for UCSDped dataset.""" - def get_mask(self, idx) -> Optional[Tensor]: + def get_mask(self, idx) -> np.ndarray | None: """Retrieve the masks from the file system.""" video_idx, frames_idx = self.get_clip_location(idx) @@ -116,7 +118,7 @@ def _compute_frame_pts(self) -> None: self.video_fps = [None] * len(self.video_paths) # fps information cannot be inferred from folder structure - def get_clip(self, idx: int) -> Tuple[Tensor, Tensor, Dict[str, Any], int]: + def get_clip(self, idx: int) -> tuple[Tensor, Tensor, dict[str, Any], int]: """Gets a subclip from a list of videos. Args: @@ -125,7 +127,7 @@ def get_clip(self, idx: int) -> Tuple[Tensor, Tensor, Dict[str, Any], int]: Returns: video (Tensor) audio (Tensor) - info (Dict) + info (dict) video_idx (int): index of the video in `video_paths` """ if idx >= self.num_clips(): @@ -150,7 +152,7 @@ class UCSDpedDataset(AnomalibVideoDataset): root (str): Path to the root of the dataset category (str): Sub-category of the dataset, e.g. 'bottle' transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. - split (Optional[Union[Split, str]]): Split of the dataset, usually Split.TRAIN or Split.TEST + split (str | Split | None): Split of the dataset, usually Split.TRAIN or Split.TEST clip_length_in_frames (int, optional): Number of video frames in each clip. frames_between_clips (int, optional): Number of frames between each consecutive video clip. """ @@ -158,20 +160,20 @@ class UCSDpedDataset(AnomalibVideoDataset): def __init__( self, task: TaskType, - root: Union[Path, str], + root: str | Path, category: str, transform: A.Compose, split: Split, clip_length_in_frames: int = 1, frames_between_clips: int = 1, - ): + ) -> None: super().__init__(task, transform, clip_length_in_frames, frames_between_clips) self.root_category = Path(root) / category self.split = split self.indexer_cls: Callable = UCSDpedClipsIndexer - def _setup(self): + def _setup(self) -> None: """Create and assign samples.""" self.samples = make_ucsd_dataset(self.root_category, self.split) @@ -185,26 +187,26 @@ class UCSDped(AnomalibVideoDataModule): clip_length_in_frames (int, optional): Number of video frames in each clip. frames_between_clips (int, optional): Number of frames between each consecutive video clip. task (TaskType): Task type, 'classification', 'detection' or 'segmentation' - image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image. + image_size (int | tuple[int, int] | None, optional): Size of the input image. Defaults to None. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. train_batch_size (int, optional): Training batch size. Defaults to 32. eval_batch_size (int, optional): Test batch size. Defaults to 32. num_workers (int, optional): Number of workers. Defaults to 8. - transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. Defaults to None. - transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_val (str | A.Compose | None, optional): Config for pre-processing during validation. Defaults to None. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed which may be set to a fixed value for reproducibility. + seed (int | None, optional): Seed which may be set to a fixed value for reproducibility. """ def __init__( @@ -214,18 +216,18 @@ def __init__( clip_length_in_frames: int = 1, frames_between_clips: int = 1, task: TaskType = TaskType.SEGMENTATION, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, val_split_mode: ValSplitMode = ValSplitMode.FROM_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, - ): + seed: int | None = None, + ) -> None: super().__init__( train_batch_size=train_batch_size, eval_batch_size=eval_batch_size, diff --git a/anomalib/data/utils/augmenter.py b/anomalib/data/utils/augmenter.py index ed687652aa..6d41acb98e 100644 --- a/anomalib/data/utils/augmenter.py +++ b/anomalib/data/utils/augmenter.py @@ -10,10 +10,11 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import glob import math import random -from typing import Optional, Tuple, Union import cv2 import imgaug.augmenters as iaa @@ -34,7 +35,7 @@ class Augmenter: """Class that generates noisy augmentations of input images. Args: - anomaly_source_path (Optional[str]): Path to a folder of images that will be used as source of the anomalous + anomaly_source_path (str | None): Path to a folder of images that will be used as source of the anomalous noise. If not specified, random noise will be used instead. p_anomalous (float): Probability that the anomalous perturbation will be applied to a given image. beta (float): Parameter that determines the opacity of the noise mask. @@ -42,9 +43,9 @@ class Augmenter: def __init__( self, - anomaly_source_path: Optional[str] = None, + anomaly_source_path: str | None = None, p_anomalous: float = 0.5, - beta: Union[float, Tuple[float, float]] = (0.2, 1.0), + beta: float | tuple[float, float] = (0.2, 1.0), ): self.p_anomalous = p_anomalous @@ -80,14 +81,14 @@ def rand_augmenter(self) -> iaa.Sequential: return aug def generate_perturbation( - self, height: int, width: int, anomaly_source_path: Optional[str] - ) -> Tuple[np.ndarray, np.ndarray]: + self, height: int, width: int, anomaly_source_path: str | None + ) -> tuple[np.ndarray, np.ndarray]: """Generate an image containing a random anomalous perturbation using a source image. Args: height (int): height of the generated image. width: (int): width of the generated image. - anomaly_source_path (Optional[str]): Path to an image file. If not provided, random noise will be used + anomaly_source_path (str | None): Path to an image file. If not provided, random noise will be used instead. Returns: @@ -126,7 +127,7 @@ def generate_perturbation( return perturbation, mask - def augment_batch(self, batch: Tensor) -> Tuple[Tensor, Tensor]: + def augment_batch(self, batch: Tensor) -> tuple[Tensor, Tensor]: """Generate anomalous augmentations for a batch of input images. Args: diff --git a/anomalib/data/utils/boxes.py b/anomalib/data/utils/boxes.py index 6ef9a42d82..4e5da46c18 100644 --- a/anomalib/data/utils/boxes.py +++ b/anomalib/data/utils/boxes.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Optional, Tuple +from __future__ import annotations import torch from torch import Tensor @@ -11,18 +11,18 @@ from anomalib.utils.cv import connected_components_cpu, connected_components_gpu -def masks_to_boxes(masks: Tensor, anomaly_maps: Optional[Tensor] = None) -> Tuple[List[Tensor], List[Tensor]]: +def masks_to_boxes(masks: Tensor, anomaly_maps: Tensor | None = None) -> tuple[list[Tensor], list[Tensor]]: """Convert a batch of segmentation masks to bounding box coordinates. Args: masks (Tensor): Input tensor of shape (B, 1, H, W), (B, H, W) or (H, W) - anomaly_maps (Optional[Tensor], optional): Anomaly maps of shape (B, 1, H, W), (B, H, W) or (H, W) which are + anomaly_maps (Tensor | None, optional): Anomaly maps of shape (B, 1, H, W), (B, H, W) or (H, W) which are used to determine an anomaly score for the converted bounding boxes. Returns: - List[Tensor]: A list of length B where each element is a tensor of shape (N, 4) containing the bounding box + list[Tensor]: A list of length B where each element is a tensor of shape (N, 4) containing the bounding box coordinates of the objects in the masks in xyxy format. - List[Tensor]: A list of length B where each element is a tensor of length (N) containing an anomaly score for + list[Tensor]: A list of length B where each element is a tensor of length (N) containing an anomaly score for each of the converted boxes. """ height, width = masks.shape[-2:] @@ -47,19 +47,19 @@ def masks_to_boxes(masks: Tensor, anomaly_maps: Optional[Tensor] = None) -> Tupl im_boxes.append(Tensor([torch.min(x_loc), torch.min(y_loc), torch.max(x_loc), torch.max(y_loc)])) if anomaly_maps is not None: im_scores.append(torch.max(anomaly_maps[im_idx, y_loc, x_loc])) - batch_boxes.append(torch.stack(im_boxes) if len(im_boxes) > 0 else torch.empty((0, 4))) - batch_scores.append(torch.stack(im_scores) if len(im_scores) > 0 else torch.empty(0)) + batch_boxes.append(torch.stack(im_boxes) if im_boxes else torch.empty((0, 4))) + batch_scores.append(torch.stack(im_scores) if im_scores else torch.empty(0)) return batch_boxes, batch_scores -def boxes_to_masks(boxes: List[Tensor], image_size: Tuple[int, int]) -> Tensor: +def boxes_to_masks(boxes: list[Tensor], image_size: tuple[int, int]) -> Tensor: """Convert bounding boxes to segmentations masks. Args: - boxes (List[Tensor]): A list of length B where each element is a tensor of shape (N, 4) containing the bounding + boxes (list[Tensor]): A list of length B where each element is a tensor of shape (N, 4) containing the bounding box coordinates of the regions of interest in xyxy format. - image_size (Tuple[int, int]): Image size of the output masks in (H, W) format. + image_size (tuple[int, int]): Image size of the output masks in (H, W) format. Returns: Tensor: Tensor of shape (B, H, W) in which each slice is a binary mask showing the pixels contained by a @@ -73,15 +73,15 @@ def boxes_to_masks(boxes: List[Tensor], image_size: Tuple[int, int]) -> Tensor: return masks -def boxes_to_anomaly_maps(boxes: Tensor, scores: Tensor, image_size: Tuple[int, int]) -> Tensor: +def boxes_to_anomaly_maps(boxes: Tensor, scores: Tensor, image_size: tuple[int, int]) -> Tensor: """Convert bounding box coordinates to anomaly heatmaps. Args: - boxes (List[Tensor]): A list of length B where each element is a tensor of shape (N, 4) containing the bounding + boxes (list[Tensor]): A list of length B where each element is a tensor of shape (N, 4) containing the bounding box coordinates of the regions of interest in xyxy format. - scores (List[Tensor]): A list of length B where each element is a 1D tensor of length N containing the anomaly + scores (list[Tensor]): A list of length B where each element is a 1D tensor of length N containing the anomaly scores for each region of interest. - image_size (Tuple[int, int]): Image size of the output masks in (H, W) format. + image_size (tuple[int, int]): Image size of the output masks in (H, W) format. Returns: Tensor: Tensor of shape (B, H, W). The pixel locations within each bounding box are collectively assigned the diff --git a/anomalib/data/utils/download.py b/anomalib/data/utils/download.py index 24ed1df4c6..a70c7c4bf5 100644 --- a/anomalib/data/utils/download.py +++ b/anomalib/data/utils/download.py @@ -3,13 +3,15 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import hashlib import io import logging import tarfile from dataclasses import dataclass from pathlib import Path -from typing import Dict, Iterable, Optional, Union +from typing import Iterable from urllib.request import urlretrieve from zipfile import ZipFile @@ -33,55 +35,55 @@ class DownloadProgressBar(tqdm): For information about the parameters in constructor, refer to `tqdm`'s documentation. Args: - iterable (Optional[Iterable]): Iterable to decorate with a progressbar. + iterable (Iterable | None): Iterable to decorate with a progressbar. Leave blank to manually manage the updates. - desc (Optional[str]): Prefix for the progressbar. - total (Optional[Union[int, float]]): The number of expected iterations. If unspecified, + desc (str | None): Prefix for the progressbar. + total (int | float | None): The number of expected iterations. If unspecified, len(iterable) is used if possible. If float("inf") or as a last resort, only basic progress statistics are displayed (no ETA, no progressbar). If `gui` is True and this parameter needs subsequent updating, specify an initial arbitrary large positive number, e.g. 9e9. - leave (Optional[bool]): upon termination of iteration. If `None`, will leave only if `position` is `0`. - file (Optional[Union[io.TextIOWrapper, io.StringIO]]): Specifies where to output the progress messages + leave (bool | None): upon termination of iteration. If `None`, will leave only if `position` is `0`. + file (io.TextIOWrapper | io.StringIO | None): Specifies where to output the progress messages (default: sys.stderr). Uses `file.write(str)` and `file.flush()` methods. For encoding, see `write_bytes`. - ncols (Optional[int]): The width of the entire output message. If specified, + ncols (int | None): The width of the entire output message. If specified, dynamically resizes the progressbar to stay within this bound. If unspecified, attempts to use environment width. The fallback is a meter width of 10 and no limit for the counter and statistics. If 0, will not print any meter (only stats). - mininterval (Optional[float]): Minimum progress display update interval [default: 0.1] seconds. - maxinterval (Optional[float]): Maximum progress display update interval [default: 10] seconds. + mininterval (float | None): Minimum progress display update interval [default: 0.1] seconds. + maxinterval (float | None): Maximum progress display update interval [default: 10] seconds. Automatically adjusts `miniters` to correspond to `mininterval` after long display update lag. Only works if `dynamic_miniters` or monitor thread is enabled. - miniters (Optional[Union[int, float]]): Minimum progress display update interval, in iterations. + miniters (int | float | None): Minimum progress display update interval, in iterations. If 0 and `dynamic_miniters`, will automatically adjust to equal `mininterval` (more CPU efficient, good for tight loops). If > 0, will skip display of specified number of iterations. Tweak this and `mininterval` to get very efficient loops. If your progress is erratic with both fast and slow iterations (network, skipping items, etc) you should set miniters=1. - use_ascii (Optional[Union[bool, str]]): If unspecified or False, use unicode (smooth blocks) to fill + use_ascii (str | bool | None): If unspecified or False, use unicode (smooth blocks) to fill the meter. The fallback is to use ASCII characters " 123456789#". - disable (Optional[bool]): Whether to disable the entire progressbar wrapper + disable (bool | None): Whether to disable the entire progressbar wrapper [default: False]. If set to None, disable on non-TTY. - unit (Optional[str]): String that will be used to define the unit of each iteration + unit (str | None): String that will be used to define the unit of each iteration [default: it]. - unit_scale (Union[bool, int, float]): If 1 or True, the number of iterations will be reduced/scaled + unit_scale (int | float | bool): If 1 or True, the number of iterations will be reduced/scaled automatically and a metric prefix following the International System of Units standard will be added (kilo, mega, etc.) [default: False]. If any other non-zero number, will scale `total` and `n`. - dynamic_ncols (Optional[bool]): If set, constantly alters `ncols` and `nrows` to the + dynamic_ncols (bool | None): If set, constantly alters `ncols` and `nrows` to the environment (allowing for window resizes) [default: False]. - smoothing (Optional[float]): Exponential moving average smoothing factor for speed estimates + smoothing (float | None): Exponential moving average smoothing factor for speed estimates (ignored in GUI mode). Ranges from 0 (average speed) to 1 (current/instantaneous speed) [default: 0.3]. - bar_format (Optional[str]): Specify a custom bar string formatting. May impact performance. + bar_format (str | None): Specify a custom bar string formatting. May impact performance. [default: '{l_bar}{bar}{r_bar}'], where l_bar='{desc}: {percentage:3.0f}%|' and r_bar='| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, ' @@ -93,26 +95,26 @@ class DownloadProgressBar(tqdm): remaining, remaining_s, eta. Note that a trailing ": " is automatically removed after {desc} if the latter is empty. - initial (Optional[Union[int, float]]): The initial counter value. Useful when restarting a progress + initial (int | float | None): The initial counter value. Useful when restarting a progress bar [default: 0]. If using float, consider specifying `{n:.3f}` or similar in `bar_format`, or specifying `unit_scale`. - position (Optional[int]): Specify the line offset to print this bar (starting from 0) + position (int | None): Specify the line offset to print this bar (starting from 0) Automatic if unspecified. Useful to manage multiple bars at once (eg, from threads). - postfix (Optional[Dict]): Specify additional stats to display at the end of the bar. + postfix (dict | None): Specify additional stats to display at the end of the bar. Calls `set_postfix(**postfix)` if possible (dict). - unit_divisor (Optional[float]): [default: 1000], ignored unless `unit_scale` is True. - write_bytes (Optional[bool]): If (default: None) and `file` is unspecified, + unit_divisor (float | None): [default: 1000], ignored unless `unit_scale` is True. + write_bytes (bool | None): If (default: None) and `file` is unspecified, bytes will be written in Python 2. If `True` will also write bytes. In all other cases will default to unicode. - lock_args (Optional[tuple]): Passed to `refresh` for intermediate output + lock_args (tuple | None): Passed to `refresh` for intermediate output (initialisation, iterating, and updating). - nrows (Optional[int]): The screen height. If specified, hides nested bars + nrows (int | None): The screen height. If specified, hides nested bars outside this bound. If unspecified, attempts to use environment height. The fallback is 20. - colour (Optional[str]): Bar colour (e.g. 'green', '#00ff00'). - delay (Optional[float]): Don't display until [default: 0] seconds have elapsed. - gui (Optional[bool]): WARNING: internal parameter - do not use. + colour (str | None): Bar colour (e.g. 'green', '#00ff00'). + delay (float | None): Don't display until [default: 0] seconds have elapsed. + gui (bool | None): WARNING: internal parameter - do not use. Use tqdm.gui.tqdm(...) instead. If set, will attempt to use matplotlib animations for a graphical output [default: False]. @@ -124,32 +126,32 @@ class DownloadProgressBar(tqdm): def __init__( self, - iterable: Optional[Iterable] = None, - desc: Optional[str] = None, - total: Optional[Union[int, float]] = None, - leave: Optional[bool] = True, - file: Optional[Union[io.TextIOWrapper, io.StringIO]] = None, - ncols: Optional[int] = None, - mininterval: Optional[float] = 0.1, - maxinterval: Optional[float] = 10.0, - miniters: Optional[Union[int, float]] = None, - use_ascii: Optional[Union[bool, str]] = None, - disable: Optional[bool] = False, - unit: Optional[str] = "it", - unit_scale: Optional[Union[bool, int, float]] = False, - dynamic_ncols: Optional[bool] = False, - smoothing: Optional[float] = 0.3, - bar_format: Optional[str] = None, - initial: Optional[Union[int, float]] = 0, - position: Optional[int] = None, - postfix: Optional[Dict] = None, - unit_divisor: Optional[float] = 1000, - write_bytes: Optional[bool] = None, - lock_args: Optional[tuple] = None, - nrows: Optional[int] = None, - colour: Optional[str] = None, - delay: Optional[float] = 0, - gui: Optional[bool] = False, + iterable: Iterable | None = None, + desc: str | None = None, + total: int | float | None = None, + leave: bool | None = True, + file: io.TextIOWrapper | io.StringIO | None = None, + ncols: int | None = None, + mininterval: float | None = 0.1, + maxinterval: float | None = 10.0, + miniters: int | float | None = None, + use_ascii: bool | str | None = None, + disable: bool | None = False, + unit: str | None = "it", + unit_scale: bool | int | float | None = False, + dynamic_ncols: bool | None = False, + smoothing: float | None = 0.3, + bar_format: str | None = None, + initial: int | float | None = 0, + position: int | None = None, + postfix: dict | None = None, + unit_divisor: float | None = 1000, + write_bytes: bool | None = None, + lock_args: tuple | None = None, + nrows: int | None = None, + colour: str | None = None, + delay: float | None = 0, + gui: bool | None = False, **kwargs, ): super().__init__( @@ -181,9 +183,9 @@ def __init__( gui=gui, **kwargs, ) - self.total: Optional[Union[int, float]] + self.total: int | float | None - def update_to(self, chunk_number: int = 1, max_chunk_size: int = 1, total_size=None): + def update_to(self, chunk_number: int = 1, max_chunk_size: int = 1, total_size=None) -> None: """Progress bar hook for tqdm. Based on https://stackoverflow.com/a/53877507 @@ -200,20 +202,20 @@ def update_to(self, chunk_number: int = 1, max_chunk_size: int = 1, total_size=N self.update(chunk_number * max_chunk_size - self.n) -def hash_check(file_path: Path, expected_hash: str): +def hash_check(file_path: Path, expected_hash: str) -> None: """Raise assert error if hash does not match the calculated hash of the file. Args: file_path (Path): Path to file. expected_hash (str): Expected hash of the file. """ - with open(file_path, "rb") as hash_file: + with file_path.open("rb") as hash_file: assert ( hashlib.md5(hash_file.read()).hexdigest() == expected_hash ), f"Downloaded file {file_path} does not match the required hash." -def download_and_extract(root: Path, info: DownloadInfo): +def download_and_extract(root: Path, info: DownloadInfo) -> None: """Download and extract a dataset. Args: @@ -241,7 +243,7 @@ def download_and_extract(root: Path, info: DownloadInfo): if downloaded_file_path.suffix == ".zip": with ZipFile(downloaded_file_path, "r") as zip_file: zip_file.extractall(root) - elif downloaded_file_path.suffix in [".tar", ".gz", ".xz"]: + elif downloaded_file_path.suffix in (".tar", ".gz", ".xz"): with tarfile.open(downloaded_file_path) as tar_file: tar_file.extractall(root) else: diff --git a/anomalib/data/utils/generators/perlin.py b/anomalib/data/utils/generators/perlin.py index 760222fa44..6b36ace1fd 100644 --- a/anomalib/data/utils/generators/perlin.py +++ b/anomalib/data/utils/generators/perlin.py @@ -11,8 +11,9 @@ # pylint: disable=invalid-name +from __future__ import annotations + import math -from typing import Tuple, Union import numpy as np import torch @@ -66,20 +67,20 @@ def f(t): def random_2d_perlin( - shape: Tuple, - res: Tuple[Union[int, Tensor], Union[int, Tensor]], + shape: tuple, + res: tuple[int | Tensor, int | Tensor], fade=lambda t: 6 * t**5 - 15 * t**4 + 10 * t**3, -) -> Union[np.ndarray, Tensor]: +) -> np.ndarray | Tensor: """Returns a random 2d perlin noise array. Args: - shape (Tuple): Shape of the 2d map. - res (Tuple[Union[int, Tensor]]): Tuple of scales for perlin noise for height and width dimension. + shape (tuple): Shape of the 2d map. + res (tuple[int | Tensor, int | Tensor]): Tuple of scales for perlin noise for height and width dimension. fade (_type_, optional): Function used for fading the resulting 2d map. Defaults to equation 6*t**5-15*t**4+10*t**3. Returns: - Union[np.ndarray, Tensor]: Random 2d-array/tensor generated using perlin noise. + np.ndarray | Tensor: Random 2d-array/tensor generated using perlin noise. """ if isinstance(res[0], int): result = _rand_perlin_2d_np(shape, res, fade) diff --git a/anomalib/data/utils/image.py b/anomalib/data/utils/image.py index d188033de7..cb3ee898ca 100644 --- a/anomalib/data/utils/image.py +++ b/anomalib/data/utils/image.py @@ -3,10 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math import warnings from pathlib import Path -from typing import List, Optional, Tuple, Union import cv2 import numpy as np @@ -15,17 +16,17 @@ from torchvision.datasets.folder import IMG_EXTENSIONS -def get_image_filenames(path: Union[str, Path]) -> List[Path]: +def get_image_filenames(path: str | Path) -> list[Path]: """Get image filenames. Args: - path (Union[str, Path]): Path to image or image-folder. + path (str | Path): Path to image or image-folder. Returns: - List[Path]: List of image filenames + list[Path]: List of image filenames """ - image_filenames: List[Path] + image_filenames: list[Path] if isinstance(path, str): path = Path(path) @@ -36,19 +37,19 @@ def get_image_filenames(path: Union[str, Path]) -> List[Path]: if path.is_dir(): image_filenames = [p for p in path.glob("**/*") if p.suffix in IMG_EXTENSIONS] - if len(image_filenames) == 0: + if image_filenames: raise ValueError(f"Found 0 images in {path}") return image_filenames -def duplicate_filename(path: Union[str, Path]) -> Path: +def duplicate_filename(path: str | Path) -> Path: """Check and duplicate filename. This function checks the path and adds a suffix if it already exists on the file system. Args: - path (Union[str, Path]): Input Path + path (str | Path): Input Path Examples: >>> path = Path("datasets/MVTec/bottle/test/broken_large/000.png") @@ -76,7 +77,7 @@ def duplicate_filename(path: Union[str, Path]) -> Path: return duplicated_path -def generate_output_image_filename(input_path: Union[str, Path], output_path: Union[str, Path]) -> Path: +def generate_output_image_filename(input_path: str | Path, output_path: str | Path) -> Path: """Generate an output filename to save the inference image. This function generates an output filaname by checking the input and output filenames. Input path is @@ -89,8 +90,8 @@ def generate_output_image_filename(input_path: Union[str, Path], output_path: Un filenames of ``input_path`` to ``output_path``. Args: - input_path (Union[str, Path]): Path to the input image to infer. - output_path (Union[str, Path]): Path to output to save the predictions. + input_path (str | Path): Path to the input image to infer. + output_path (str | Path): Path to output to save the predictions. Could be a filename or a directory. Examples: @@ -141,11 +142,11 @@ def generate_output_image_filename(input_path: Union[str, Path], output_path: Un return file_path -def get_image_height_and_width(image_size: Union[int, Tuple[int, int]]) -> Tuple[int, int]: +def get_image_height_and_width(image_size: int | tuple[int, int]) -> tuple[int, int]: """Get image height and width from ``image_size`` variable. Args: - image_size (Optional[Union[int, Tuple[int, int]]], optional): Input image size. + image_size (int | tuple[int, int] | None, optional): Input image size. Raises: ValueError: Image size not None, int or tuple. @@ -164,22 +165,22 @@ def get_image_height_and_width(image_size: Union[int, Tuple[int, int]]) -> Tuple Traceback (most recent call last): File "", line 1, in File "", line 18, in get_image_height_and_width - ValueError: ``image_size`` could be either int or Tuple[int, int] + ValueError: ``image_size`` could be either int or tuple[int, int] Returns: - Tuple[Optional[int], Optional[int]]: A tuple containing image height and width values. + tuple[int | None, int | None]: A tuple containing image height and width values. """ if isinstance(image_size, int): height_and_width = (image_size, image_size) elif isinstance(image_size, tuple): height_and_width = int(image_size[0]), int(image_size[1]) else: - raise ValueError("``image_size`` could be either int or Tuple[int, int]") + raise ValueError("``image_size`` could be either int or tuple[int, int]") return height_and_width -def read_image(path: Union[str, Path], image_size: Optional[Union[int, Tuple[int, int]]] = None) -> np.ndarray: +def read_image(path: str | Path, image_size: int | tuple[int, int] | None = None) -> np.ndarray: """Read image from disk in RGB format. Args: diff --git a/anomalib/data/utils/split.py b/anomalib/data/utils/split.py index 60f8b7f0e1..e78d49ed5f 100644 --- a/anomalib/data/utils/split.py +++ b/anomalib/data/utils/split.py @@ -16,7 +16,7 @@ import math import warnings from enum import Enum -from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Sequence import torch @@ -66,10 +66,10 @@ def concatenate_datasets(datasets: Sequence[AnomalibDataset]) -> AnomalibDataset def random_split( dataset: AnomalibDataset, - split_ratio: Union[float, Sequence[float]], + split_ratio: float | Sequence[float], label_aware: bool = False, - seed: Optional[int] = None, -) -> List[AnomalibDataset]: + seed: int | None = None, +) -> list[AnomalibDataset]: """Perform a random split of a dataset. Args: @@ -79,7 +79,7 @@ def random_split( [1-split_ratio, split_ratio]. label_aware (bool): When True, the relative occurrence of the different class labels of the source dataset will be maintained in each of the subsets. - seed (Optional[int], optional): Seed that can be passed if results need to be reproducible + seed (int | None, optional): Seed that can be passed if results need to be reproducible """ if isinstance(split_ratio, float): @@ -98,13 +98,11 @@ def random_split( per_label_datasets = [dataset] # outer list: per-label unique, inner list: random subsets with the given ratio - subsets: List[List[AnomalibDataset]] = [] + subsets: list[list[AnomalibDataset]] = [] # split each (label-aware) subset of source data for label_dataset in per_label_datasets: # get subset lengths - subset_lengths = [] - for ratio in split_ratio: - subset_lengths.append(int(math.floor(len(label_dataset.samples) * ratio))) + subset_lengths = [math.floor(len(label_dataset.samples) * ratio) for ratio in split_ratio] for i in range(len(label_dataset.samples) - sum(subset_lengths)): subset_idx = i % sum(subset_lengths) subset_lengths[subset_idx] += 1 @@ -127,7 +125,7 @@ def random_split( return [concatenate_datasets(subset) for subset in subsets] -def split_by_label(dataset: AnomalibDataset) -> Tuple[AnomalibDataset, AnomalibDataset]: +def split_by_label(dataset: AnomalibDataset) -> tuple[AnomalibDataset, AnomalibDataset]: """Splits the dataset into the normal and anomalous subsets.""" samples = dataset.samples normal_indices = samples[samples.label_index == 0].index diff --git a/anomalib/data/utils/transform.py b/anomalib/data/utils/transform.py index ebf548d9a1..cbfa3b372b 100644 --- a/anomalib/data/utils/transform.py +++ b/anomalib/data/utils/transform.py @@ -3,9 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from enum import Enum -from typing import Optional, Tuple, Union import albumentations as A from albumentations.pytorch import ToTensorV2 @@ -23,18 +24,18 @@ class InputNormalizationMethod(str, Enum): def get_transforms( - config: Optional[Union[str, A.Compose]] = None, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, + config: str | A.Compose | None = None, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, normalization: InputNormalizationMethod = InputNormalizationMethod.IMAGENET, to_tensor: bool = True, ) -> A.Compose: """Get transforms from config or image size. Args: - config (Optional[Union[str, A.Compose]], optional): Albumentations transforms. + config (str | A.Compose | None, optional): Albumentations transforms. Either config or albumentations ``Compose`` object. Defaults to None. - image_size (Optional[Union[int, Tuple]], optional): Image size to transform. Defaults to None. + image_size (int | tuple | None, optional): Image size to transform. Defaults to None. to_tensor (bool, optional): Boolean to convert the final transforms into Torch tensor. Defaults to True. Raises: diff --git a/anomalib/data/utils/video.py b/anomalib/data/utils/video.py index 7e8dfcf5e0..76c756f822 100644 --- a/anomalib/data/utils/video.py +++ b/anomalib/data/utils/video.py @@ -3,9 +3,11 @@ # Copyright (C) 2023 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import warnings from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any from torch import Tensor from torchvision.datasets.video_utils import VideoClips @@ -19,14 +21,14 @@ class ClipsIndexer(VideoClips, ABC): of folders with single-frame images), the subclass should implement at least get_clip and _compute_frame_pts. Args: - video_paths (List[str]): List of video paths that make up the dataset. - mask_paths (List[str]): List of paths to the masks for each video in the dataset. + video_paths (list[str]): List of video paths that make up the dataset. + mask_paths (list[str]): List of paths to the masks for each video in the dataset. """ def __init__( self, - video_paths: List[str], - mask_paths: List[str], + video_paths: list[str], + mask_paths: list[str], clip_length_in_frames: int = 1, frames_between_clips: int = 1, ) -> None: @@ -42,11 +44,11 @@ def last_frame_idx(self, video_idx: int) -> int: return self.clips[video_idx][-1][-1].item() @abstractmethod - def get_mask(self, idx: int) -> Optional[Tensor]: + def get_mask(self, idx: int) -> Tensor | None: """Return the masks for the given index.""" raise NotImplementedError - def get_item(self, idx: int) -> Dict[str, Any]: + def get_item(self, idx: int) -> dict[str, Any]: """Return a dictionary containing the clip, mask, video path and frame indices.""" with warnings.catch_warnings(): # silence warning caused by bug in torchvision, see https://github.com/pytorch/vision/issues/5787 diff --git a/anomalib/data/visa.py b/anomalib/data/visa.py index 02ef4ce6b0..1935aaa629 100644 --- a/anomalib/data/visa.py +++ b/anomalib/data/visa.py @@ -21,11 +21,12 @@ # Subset splitting code adapted from https://github.com/amazon-science/spot-diff # Original licence: Apache-2.0 +from __future__ import annotations + import csv import logging import shutil from pathlib import Path -from typing import Optional, Tuple, Union import albumentations as A import cv2 @@ -61,8 +62,8 @@ class VisaDataset(AnomalibDataset): Args: task (TaskType): Task type, ``classification``, ``detection`` or ``segmentation`` transform (A.Compose): Albumentations Compose object describing the transforms that are applied to the inputs. - split (Optional[Union[Split, str]]): Split of the dataset, usually Split.TRAIN or Split.TEST - root (Union[str, Path]): Path to the root of the dataset + split (str | Split | None): Split of the dataset, usually Split.TRAIN or Split.TEST + root (str | Path): Path to the root of the dataset category (str): Sub-category of the dataset, e.g. 'candle' """ @@ -70,16 +71,16 @@ def __init__( self, task: TaskType, transform: A.Compose, - root: Union[str, Path], + root: str | Path, category: str, - split: Optional[Union[Split, str]] = None, + split: str | Split | None = None, ) -> None: super().__init__(task=task, transform=transform) self.root_category = Path(root) / category self.split = split - def _setup(self): + def _setup(self) -> None: self.samples = make_mvtec_dataset(self.root_category, split=self.split, extensions=EXTENSIONS) @@ -89,47 +90,47 @@ class Visa(AnomalibDataModule): Args: root (str): Path to the root of the dataset category (str): Category of the MVTec dataset (e.g. "bottle" or "cable"). - image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image. + image_size (int | tuple[int, int] | None, optional): Size of the input image. Defaults to None. - center_crop (Optional[Union[int, Tuple[int, int]]], optional): When provided, the images will be center-cropped + center_crop (int | tuple[int, int] | None, optional): When provided, the images will be center-cropped to the provided dimensions. normalize (bool): When True, the images will be normalized to the ImageNet statistics. train_batch_size (int, optional): Training batch size. Defaults to 32. eval_batch_size (int, optional): Test batch size. Defaults to 32. num_workers (int, optional): Number of workers. Defaults to 8. task (TaskType): Task type, 'classification', 'detection' or 'segmentation' - transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_train (str | A.Compose | None, optional): Config for pre-processing during training. Defaults to None. - transform_config_val (Optional[Union[str, A.Compose]], optional): Config for pre-processing + transform_config_val (str | A.Compose | None, optional): Config for pre-processing during validation. Defaults to None. test_split_mode (TestSplitMode): Setting that determines how the testing subset is obtained. test_split_ratio (float): Fraction of images from the train set that will be reserved for testing. val_split_mode (ValSplitMode): Setting that determines how the validation subset is obtained. val_split_ratio (float): Fraction of train or test images that will be reserved for validation. - seed (Optional[int], optional): Seed which may be set to a fixed value for reproducibility. + seed (int | None, optional): Seed which may be set to a fixed value for reproducibility. """ def __init__( self, root: str, category: str, - image_size: Optional[Union[int, Tuple[int, int]]] = None, - center_crop: Optional[Union[int, Tuple[int, int]]] = None, - normalization: Union[InputNormalizationMethod, str] = InputNormalizationMethod.IMAGENET, + image_size: int | tuple[int, int] | None = None, + center_crop: int | tuple[int, int] | None = None, + normalization: str | InputNormalizationMethod = InputNormalizationMethod.IMAGENET, train_batch_size: int = 32, eval_batch_size: int = 32, num_workers: int = 8, task: TaskType = TaskType.SEGMENTATION, - transform_config_train: Optional[Union[str, A.Compose]] = None, - transform_config_eval: Optional[Union[str, A.Compose]] = None, + transform_config_train: str | A.Compose | None = None, + transform_config_eval: str | A.Compose | None = None, test_split_mode: TestSplitMode = TestSplitMode.FROM_DIR, test_split_ratio: float = 0.2, val_split_mode: ValSplitMode = ValSplitMode.SAME_AS_TEST, val_split_ratio: float = 0.5, - seed: Optional[int] = None, - ): + seed: int | None = None, + ) -> None: super().__init__( train_batch_size=train_batch_size, eval_batch_size=eval_batch_size, @@ -180,7 +181,7 @@ def prepare_data(self) -> None: logger.info("Downloaded the dataset. Applying train/test split.") self.apply_cls1_split() - def apply_cls1_split(self): + def apply_cls1_split(self) -> None: """Apply the 1-class subset splitting using the fixed split in the csv file. adapted from https://github.com/amazon-science/spot-diff @@ -218,7 +219,7 @@ def apply_cls1_split(self): test_img_bad_folder.mkdir(parents=True, exist_ok=True) test_mask_bad_folder.mkdir(parents=True, exist_ok=True) - with open(split_file, "r", encoding="utf-8") as file: + with split_file.open(encoding="utf-8") as file: csvreader = csv.reader(file) next(csvreader) for row in csvreader: diff --git a/anomalib/deploy/export.py b/anomalib/deploy/export.py index 25641bc826..9d2b1da2b5 100644 --- a/anomalib/deploy/export.py +++ b/anomalib/deploy/export.py @@ -3,11 +3,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import json import subprocess # nosec from enum import Enum from pathlib import Path -from typing import Dict, List, Tuple, Union import numpy as np import torch @@ -24,17 +25,17 @@ class ExportMode(str, Enum): OPENVINO = "openvino" -def get_model_metadata(model: AnomalyModule) -> Dict[str, Tensor]: +def get_model_metadata(model: AnomalyModule) -> dict[str, Tensor]: """Get meta data related to normalization from model. Args: model (AnomalyModule): Anomaly model which contains metadata related to normalization. Returns: - Dict[str, Tensor]: metadata + dict[str, Tensor]: metadata """ meta_data = {} - cached_meta_data: Dict[str, Union[Number, Tensor]] = { + cached_meta_data: dict[str, Number | Tensor] = { "image_threshold": model.image_threshold.cpu().value.item(), "pixel_threshold": model.pixel_threshold.cpu().value.item(), } @@ -51,24 +52,24 @@ def get_model_metadata(model: AnomalyModule) -> Dict[str, Tensor]: def export( model: AnomalyModule, - input_size: Union[List[int], Tuple[int, int]], + input_size: list[int] | tuple[int, int], export_mode: ExportMode, - export_root: Union[str, Path], -): + export_root: str | Path, +) -> None: """Export the model to onnx format and (optionally) convert to OpenVINO IR if export mode is set to OpenVINO. Metadata.json is generated regardless of export mode. Args: model (AnomalyModule): Model to convert. - input_size (Union[List[int], Tuple[int, int]]): Image size used as the input for onnx converter. - export_root (Union[str, Path]): Path to exported ONNX/OpenVINO IR. + input_size (list[int] | tuple[int, int]): Image size used as the input for onnx converter. + export_root (str | Path): Path to exported ONNX/OpenVINO IR. export_mode (ExportMode): Mode to export the model. ONNX or OpenVINO. """ # Write metadata to json file. The file is written in the same directory as the target model. export_path: Path = Path(str(export_root)) / export_mode.value export_path.mkdir(parents=True, exist_ok=True) - with open(Path(export_path) / "meta_data.json", "w", encoding="utf-8") as metadata_file: + with (Path(export_path) / "meta_data.json").open("w", encoding="utf-8") as metadata_file: meta_data = get_model_metadata(model) # Convert metadata from torch for key, value in meta_data.items(): @@ -81,12 +82,12 @@ def export( _export_to_openvino(export_path, onnx_path) -def _export_to_onnx(model: AnomalyModule, input_size: Union[List[int], Tuple[int, int]], export_path: Path) -> Path: +def _export_to_onnx(model: AnomalyModule, input_size: list[int] | tuple[int, int], export_path: Path) -> Path: """Export model to onnx. Args: model (AnomalyModule): Model to export. - input_size (Union[List[int], Tuple[int, int]]): Image size used as the input for onnx converter. + input_size (list[int] | tuple[int, int]): Image size used as the input for onnx converter. export_path (Path): Path to the root folder of the exported model. Returns: @@ -105,11 +106,11 @@ def _export_to_onnx(model: AnomalyModule, input_size: Union[List[int], Tuple[int return onnx_path -def _export_to_openvino(export_path: Union[str, Path], onnx_path: Path): +def _export_to_openvino(export_path: str | Path, onnx_path: Path) -> None: """Convert onnx model to OpenVINO IR. Args: - export_path (Union[str, Path]): Path to the root folder of the exported model. + export_path (str | Path): Path to the root folder of the exported model. onnx_path (Path): Path to the exported onnx model. """ optimize_command = ["mo", "--input_model", str(onnx_path), "--output_dir", str(export_path)] diff --git a/anomalib/deploy/inferencers/base_inferencer.py b/anomalib/deploy/inferencers/base_inferencer.py index c4d9219750..03c76f1973 100644 --- a/anomalib/deploy/inferencers/base_inferencer.py +++ b/anomalib/deploy/inferencers/base_inferencer.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Dict, Optional, Tuple, Union, cast +from typing import Any, cast import cv2 import numpy as np @@ -30,31 +32,29 @@ class Inferencer(ABC): """ @abstractmethod - def load_model(self, path: Union[str, Path]): + def load_model(self, path: str | Path) -> Any: """Load Model.""" raise NotImplementedError @abstractmethod - def pre_process(self, image: np.ndarray) -> Union[np.ndarray, Tensor]: + def pre_process(self, image: np.ndarray) -> np.ndarray | Tensor: """Pre-process.""" raise NotImplementedError @abstractmethod - def forward(self, image: Union[np.ndarray, Tensor]) -> Union[np.ndarray, Tensor]: + def forward(self, image: np.ndarray | Tensor) -> np.ndarray | Tensor: """Forward-Pass input to model.""" raise NotImplementedError @abstractmethod - def post_process( - self, predictions: Union[np.ndarray, Tensor], meta_data: Optional[Dict[str, Any]] - ) -> Dict[str, Any]: + def post_process(self, predictions: np.ndarray | Tensor, meta_data: dict[str, Any] | None) -> dict[str, Any]: """Post-Process.""" raise NotImplementedError def predict( self, - image: Union[str, np.ndarray, Path], - meta_data: Optional[Dict[str, Any]] = None, + image: str | Path | np.ndarray, + meta_data: dict[str, Any] | None = None, ) -> ImageResult: """Perform a prediction for a given input image. @@ -95,7 +95,7 @@ def predict( ) @staticmethod - def _superimpose_segmentation_mask(meta_data: dict, anomaly_map: np.ndarray, image: np.ndarray): + def _superimpose_segmentation_mask(meta_data: dict, anomaly_map: np.ndarray, image: np.ndarray) -> np.ndarray: """Superimpose segmentation mask on top of image. Args: @@ -128,20 +128,20 @@ def __call__(self, image: np.ndarray) -> ImageResult: @staticmethod def _normalize( - pred_scores: Union[Tensor, np.float32], - meta_data: Union[Dict, DictConfig], - anomaly_maps: Optional[Union[Tensor, np.ndarray]] = None, - ) -> Tuple[Optional[Union[np.ndarray, Tensor]], float]: + pred_scores: Tensor | np.float32, + meta_data: dict | DictConfig, + anomaly_maps: Tensor | np.ndarray | None = None, + ) -> tuple[np.ndarray | Tensor | None, float]: """Applies normalization and resizes the image. Args: - pred_scores (Union[Tensor, np.float32]): Predicted anomaly score - meta_data (Dict): Meta data. Post-processing step sometimes requires + pred_scores (Tensor | np.float32): Predicted anomaly score + meta_data (dict | DictConfig): Meta data. Post-processing step sometimes requires additional meta data such as image shape. This variable comprises such info. - anomaly_maps (Optional[Union[Tensor, np.ndarray]]): Predicted raw anomaly map. + anomaly_maps (Tensor | np.ndarray | None): Predicted raw anomaly map. Returns: - Tuple[Optional[Union[np.ndarray, Tensor], float]]: Post processed predictions that are ready to be + tuple[np.ndarray | Tensor | None, float]: Post processed predictions that are ready to be visualized and predicted scores. """ @@ -170,17 +170,17 @@ def _normalize( return anomaly_maps, float(pred_scores) - def _load_meta_data(self, path: Optional[Union[str, Path]] = None) -> Union[DictConfig, Dict]: + def _load_meta_data(self, path: str | Path | None = None) -> dict | DictConfig: """Loads the meta data from the given path. Args: - path (Optional[Union[str, Path]], optional): Path to JSON file containing the metadata. + path (str | Path | None, optional): Path to JSON file containing the metadata. If no path is provided, it returns an empty dict. Defaults to None. Returns: - Union[DictConfig, Dict]: Dictionary containing the metadata. + dict | DictConfig: Dictionary containing the metadata. """ - meta_data: Union[DictConfig, Dict[str, Union[float, np.ndarray, Tensor]]] = {} + meta_data: dict[str, float | np.ndarray | Tensor] | DictConfig = {} if path is not None: config = OmegaConf.load(path) meta_data = cast(DictConfig, config) diff --git a/anomalib/deploy/inferencers/openvino_inferencer.py b/anomalib/deploy/inferencers/openvino_inferencer.py index 2296ec5908..5f6dc0f957 100644 --- a/anomalib/deploy/inferencers/openvino_inferencer.py +++ b/anomalib/deploy/inferencers/openvino_inferencer.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from importlib.util import find_spec from pathlib import Path -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any import cv2 import numpy as np @@ -27,19 +29,19 @@ class OpenVINOInferencer(Inferencer): """OpenVINO implementation for the inference. Args: - config (Union[str, Path, DictConfig, ListConfig]): Configurable parameters that are used + config (str | Path | DictConfig | ListConfig): Configurable parameters that are used during the training stage. - path (Union[str, Path]): Path to the openvino onnx, xml or bin file. - meta_data_path (Union[str, Path], optional): Path to metadata file. Defaults to None. + path (str | Path): Path to the openvino onnx, xml or bin file. + meta_data_path (str | Path, optional): Path to metadata file. Defaults to None. """ def __init__( self, - config: Union[str, Path, DictConfig, ListConfig], - path: Union[str, Path, Tuple[bytes, bytes]], - meta_data_path: Union[str, Path] = None, - device: Optional[str] = "CPU", - ): + config: str | Path | DictConfig | ListConfig, + path: str | Path | tuple[bytes, bytes], + meta_data_path: str | Path | None = None, + device: str | None = "CPU", + ) -> None: # Check and load the configuration if isinstance(config, (str, Path)): self.config = get_configurable_parameters(config_path=config) @@ -52,15 +54,15 @@ def __init__( self.input_blob, self.output_blob, self.network = self.load_model(path) self.meta_data = super()._load_meta_data(meta_data_path) - def load_model(self, path: Union[str, Path, Tuple[bytes, bytes]]): + def load_model(self, path: str | Path | tuple[bytes, bytes]): """Load the OpenVINO model. Args: - path (Union[str, Path, Tuple[bytes, bytes]]): Path to the onnx or xml and bin files + path (str | Path | tuple[bytes, bytes]): Path to the onnx or xml and bin files or tuple of .xml and .bin data as bytes. Returns: - [Tuple[str, str, ExecutableNetwork]]: Input and Output blob names + [tuple[str, str, ExecutableNetwork]]: Input and Output blob names together with the Executable network. """ ie_core = IECore() @@ -129,9 +131,7 @@ def forward(self, image: np.ndarray) -> np.ndarray: """ return self.network.infer(inputs={self.input_blob: image}) - def post_process( - self, predictions: np.ndarray, meta_data: Optional[Union[Dict, DictConfig]] = None - ) -> Dict[str, Any]: + def post_process(self, predictions: np.ndarray, meta_data: dict | DictConfig | None = None) -> dict[str, Any]: """Post process the output predictions. Args: @@ -141,7 +141,7 @@ def post_process( Defaults to None. Returns: - Dict[str, Any]: Post processed prediction results. + dict[str, Any]: Post processed prediction results. """ if meta_data is None: meta_data = self.meta_data @@ -149,9 +149,9 @@ def post_process( predictions = predictions[self.output_blob] # Initialize the result variables. - anomaly_map: Optional[np.ndarray] = None - pred_label: Optional[float] = None - pred_mask: Optional[float] = None + anomaly_map: np.ndarray | None = None + pred_label: float | None = None + pred_mask: float | None = None # If predictions returns a single value, this means that the task is # classification, and the value is the classification prediction score. @@ -171,7 +171,7 @@ def post_process( if task == TaskType.CLASSIFICATION: _, pred_score = self._normalize(pred_scores=pred_score, meta_data=meta_data) - elif task in [TaskType.SEGMENTATION, TaskType.DETECTION]: + elif task in (TaskType.SEGMENTATION, TaskType.DETECTION): if "pixel_threshold" in meta_data: pred_mask = (anomaly_map >= meta_data["pixel_threshold"]).astype(np.uint8) @@ -224,5 +224,5 @@ def _get_boxes(mask: np.ndarray) -> np.ndarray: for label in labels[labels != 0]: y_loc, x_loc = np.where(comps == label) boxes.append([np.min(x_loc), np.min(y_loc), np.max(x_loc), np.max(y_loc)]) - boxes = np.stack(boxes) if len(boxes) > 0 else np.empty((0, 4)) + boxes = np.stack(boxes) if boxes else np.empty((0, 4)) return boxes diff --git a/anomalib/deploy/inferencers/torch_inferencer.py b/anomalib/deploy/inferencers/torch_inferencer.py index 66be787e1a..924e9d1713 100644 --- a/anomalib/deploy/inferencers/torch_inferencer.py +++ b/anomalib/deploy/inferencers/torch_inferencer.py @@ -3,8 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import Any import cv2 import numpy as np @@ -27,21 +29,21 @@ class TorchInferencer(Inferencer): """PyTorch implementation for the inference. Args: - config (Union[str, Path, DictConfig, ListConfig]): Configurable parameters that are used + config (str | Path | DictConfig | ListConfig): Configurable parameters that are used during the training stage. - model_source (Union[str, Path, AnomalyModule]): Path to the model ckpt file or the Anomaly model. - meta_data_path (Union[str, Path], optional): Path to metadata file. If none, it tries to load the params + model_source (str | Path | AnomalyModule): Path to the model ckpt file or the Anomaly model. + meta_data_path (str | Path, optional): Path to metadata file. If none, it tries to load the params from the model state_dict. Defaults to None. - device (Optional[str], optional): Device to use for inference. Options are auto, cpu, cuda. Defaults to "auto". + device (str | None, optional): Device to use for inference. Options are auto, cpu, cuda. Defaults to "auto". """ def __init__( self, - config: Union[str, Path, DictConfig, ListConfig], - model_source: Union[str, Path, AnomalyModule], - meta_data_path: Optional[Union[str, Path]] = None, + config: str | Path | DictConfig | ListConfig, + model_source: str | Path | AnomalyModule, + meta_data_path: str | Path | None = None, device: str = "auto", - ): + ) -> None: self.device = self._get_device(device) @@ -80,28 +82,28 @@ def _get_device(device: str) -> torch.device: device = "cuda" return torch.device(device) - def _load_meta_data(self, path: Optional[Union[str, Path]] = None) -> Union[Dict, DictConfig]: + def _load_meta_data(self, path: str | Path | None = None) -> dict | DictConfig: """Load metadata from file or from model state dict. Args: - path (Optional[Union[str, Path]], optional): Path to metadata file. If none, it tries to load the params + path (str | Path | None, optional): Path to metadata file. If none, it tries to load the params from the model state_dict. Defaults to None. Returns: - Dict: Dictionary containing the meta_data. + dict: Dictionary containing the meta_data. """ - meta_data: Union[DictConfig, Dict[str, Union[float, Tensor, np.ndarray]]] + meta_data: dict[str, float | np.ndarray | Tensor] | DictConfig if path is None: meta_data = get_model_metadata(self.model) else: meta_data = super()._load_meta_data(path) return meta_data - def load_model(self, path: Union[str, Path]) -> AnomalyModule: + def load_model(self, path: str | Path) -> AnomalyModule: """Load the PyTorch model. Args: - path (Union[str, Path]): Path to model ckpt file. + path (str | Path): Path to model ckpt file. Returns: (AnomalyModule): PyTorch Lightning model. @@ -150,17 +152,17 @@ def forward(self, image: Tensor) -> Tensor: """ return self.model(image) - def post_process(self, predictions: Tensor, meta_data: Optional[Union[Dict, DictConfig]] = None) -> Dict[str, Any]: + def post_process(self, predictions: Tensor, meta_data: dict | DictConfig | None = None) -> dict[str, Any]: """Post process the output predictions. Args: predictions (Tensor): Raw output predicted by the model. - meta_data (Dict, optional): Meta data. Post-processing step sometimes requires + meta_data (dict, optional): Meta data. Post-processing step sometimes requires additional meta data such as image shape. This variable comprises such info. Defaults to None. Returns: - Dict[str, Union[str, float, np.ndarray]]: Post processed prediction results. + dict[str, str | float | np.ndarray]: Post processed prediction results. """ if meta_data is None: meta_data = self.meta_data @@ -184,12 +186,12 @@ def post_process(self, predictions: Tensor, meta_data: Optional[Union[Dict, Dict # Common practice in anomaly detection is to assign anomalous # label to the prediction if the prediction score is greater # than the image threshold. - pred_label: Optional[str] = None + pred_label: str | None = None if "image_threshold" in meta_data: pred_idx = pred_score >= meta_data["image_threshold"] pred_label = "Anomalous" if pred_idx else "Normal" - pred_mask: Optional[np.ndarray] = None + pred_mask: np.ndarray | None = None if "pixel_threshold" in meta_data: pred_mask = (anomaly_map >= meta_data["pixel_threshold"]).squeeze().astype(np.uint8) diff --git a/anomalib/models/__init__.py b/anomalib/models/__init__.py index dee8120aaa..b881cf8cf4 100644 --- a/anomalib/models/__init__.py +++ b/anomalib/models/__init__.py @@ -3,10 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import os from importlib import import_module -from typing import List, Union from omegaconf import DictConfig, ListConfig from torch import load @@ -57,7 +58,7 @@ def _snake_to_pascal_case(model_name: str) -> str: return "".join([split.capitalize() for split in model_name.split("_")]) -def get_model(config: Union[DictConfig, ListConfig]) -> AnomalyModule: +def get_model(config: DictConfig | ListConfig) -> AnomalyModule: """Load model from the configuration file. Works only when the convention for model naming is followed. @@ -67,7 +68,7 @@ def get_model(config: Union[DictConfig, ListConfig]) -> AnomalyModule: `anomalib.models.stfpm.lightning_model.StfpmLightning` Args: - config (Union[DictConfig, ListConfig]): Config.yaml loaded using OmegaConf + config (DictConfig | ListConfig): Config.yaml loaded using OmegaConf Raises: ValueError: If unsupported model is passed @@ -77,7 +78,7 @@ def get_model(config: Union[DictConfig, ListConfig]) -> AnomalyModule: """ logger.info("Loading the model.") - model_list: List[str] = [ + model_list: list[str] = [ "cfa", "cflow", "csflow", diff --git a/anomalib/models/cfa/anomaly_map.py b/anomalib/models/cfa/anomaly_map.py index 087ceab8e6..15ecf4321d 100644 --- a/anomalib/models/cfa/anomaly_map.py +++ b/anomalib/models/cfa/anomaly_map.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -20,7 +20,7 @@ class AnomalyMapGenerator(nn.Module): def __init__( self, - image_size: Union[ListConfig, Tuple], + image_size: ListConfig | tuple, num_nearest_neighbors: int, sigma: int = 4, ) -> None: @@ -29,13 +29,13 @@ def __init__( self.num_nearest_neighbors = num_nearest_neighbors self.sigma = sigma - def compute_score(self, distance: Tensor, scale: Tuple[int, int]) -> Tensor: + def compute_score(self, distance: Tensor, scale: tuple[int, int]) -> Tensor: """Compute score based on the distance. Args: distance (Tensor): Distance tensor computed using target oriented features. - scale (Tuple[int, int]): Height and width of the largest feature + scale (tuple[int, int]): Height and width of the largest feature map. Returns: @@ -78,7 +78,7 @@ def forward(self, **kwargs) -> Tensor: raise ValueError(f"Expected keys `distance` and `scale. Found {kwargs.keys()}") distance: Tensor = kwargs["distance"] - scale: Tuple[int, int] = kwargs["scale"] + scale: tuple[int, int] = kwargs["scale"] score = self.compute_score(distance=distance, scale=scale) anomaly_map = self.compute_anomaly_map(score) diff --git a/anomalib/models/cfa/lightning_model.py b/anomalib/models/cfa/lightning_model.py index d9b777b75d..51d65a1326 100644 --- a/anomalib/models/cfa/lightning_model.py +++ b/anomalib/models/cfa/lightning_model.py @@ -8,8 +8,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Optional, Tuple, Union import torch from omegaconf import DictConfig, ListConfig @@ -34,7 +35,7 @@ class Cfa(AnomalyModule): """CFA: Coupled-hypersphere-based Feature Adaptation for Target-Oriented Anomaly Localization. Args: - input_size (Tuple[int, int]): Size of the model input. + input_size (tuple[int, int]): Size of the model input. backbone (str): Backbone CNN network gamma_c (int, optional): gamma_c value from the paper. Defaults to 1. gamma_d (int, optional): gamma_d value from the paper. Defaults to 1. @@ -45,7 +46,7 @@ class Cfa(AnomalyModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, gamma_c: int = 1, gamma_d: int = 1, @@ -63,7 +64,7 @@ def __init__( num_hard_negative_features=num_hard_negative_features, radius=radius, ) - self.loss_func = CfaLoss( + self.loss = CfaLoss( num_nearest_neighbors=num_nearest_neighbors, num_hard_negative_features=num_hard_negative_features, radius=radius, @@ -73,25 +74,24 @@ def on_train_start(self) -> None: """Initialize the centroid for the memory bank computation.""" self.model.initialize_centroid(data_loader=self.trainer.datamodule.train_dataloader()) # type: ignore - def training_step(self, batch) -> STEP_OUTPUT: + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training step for the CFA model. Args: - batch (dict): Batch input. + batch (dict[str, str | Tensor]): Batch input. Returns: STEP_OUTPUT: Loss value. """ distance = self.model(batch["image"]) - loss = self.loss_func(distance) + loss = self.loss(distance) return {"loss": loss} - def validation_step(self, batch, batch_idx) -> dict: + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation step for the CFA model. Args: - batch (dict): Input batch. - batch_idx (int): Index of the batch. + batch (dict[str, str | Tensor]): Input batch. Returns: dict: Anomaly map computed by the model. @@ -99,17 +99,15 @@ def validation_step(self, batch, batch_idx) -> dict: batch["anomaly_maps"] = self.model(batch["image"]) return batch - # pylint: disable=unused-argument - def backward( - self, loss: Tensor, optimizer: Optional[Optimizer], optimizer_idx: Optional[int], *args, **kwargs - ) -> None: + def backward(self, loss: Tensor, optimizer: Optimizer | None, optimizer_idx: int | None, *args, **kwargs) -> None: """Backward step for the CFA model. Args: loss (Tensor): Loss value. - optimizer (Optional[Optimizer]): Optimizer. - optimizer_idx (Optional[int]): Optimizer index. + optimizer (Optimizer | None): Optimizer. + optimizer_idx (int | None): Optimizer index. """ + del optimizer, optimizer_idx # These variables are not used. # TODO: Investigate why retain_graph is needed. loss.backward(retain_graph=True) @@ -118,20 +116,20 @@ class CfaLightning(Cfa): """PL Lightning Module for the CFA model. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, gamma_c=hparams.model.gamma_c, gamma_d=hparams.model.gamma_d, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self) -> List[Callback]: + def configure_callbacks(self) -> list[Callback]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/cfa/torch_model.py b/anomalib/models/cfa/torch_model.py index 5a1d731b56..f523c36531 100644 --- a/anomalib/models/cfa/torch_model.py +++ b/anomalib/models/cfa/torch_model.py @@ -8,7 +8,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List, Optional, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -29,7 +29,7 @@ SUPPORTED_BACKBONES = ("vgg19_bn", "resnet18", "wide_resnet50_2", "efficientnet_b5") -def get_return_nodes(backbone: str) -> List[str]: +def get_return_nodes(backbone: str) -> list[str]: """Get the return nodes for a given backbone. Args: @@ -41,12 +41,12 @@ def get_return_nodes(backbone: str) -> List[str]: ValueError: If the backbone is not one of the supported backbones. Returns: - List[str]: A list of return nodes for the given backbone. + list[str]: A list of return nodes for the given backbone. """ if backbone == "efficientnet_b5": raise NotImplementedError("EfficientNet feature extractor has not implemented yet.") - return_nodes: List[str] + return_nodes: list[str] if backbone in ("resnet18", "wide_resnet50_2"): return_nodes = ["layer1", "layer2", "layer3"] elif backbone == "vgg19_bn": @@ -57,12 +57,12 @@ def get_return_nodes(backbone: str) -> List[str]: # TODO: Replace this with the new torchfx feature extractor. -def get_feature_extractor(backbone: str, return_nodes: List[str]) -> GraphModule: +def get_feature_extractor(backbone: str, return_nodes: list[str]) -> GraphModule: """Get the feature extractor from the backbone CNN. Args: backbone (str): Backbone CNN network - return_nodes (List[str]): A list of return nodes for the given backbone. + return_nodes (list[str]): A list of return nodes for the given backbone. Raises: NotImplementedError: When the backbone is efficientnet_b5 @@ -82,7 +82,7 @@ class CfaModel(DynamicBufferModule): """Torch implementation of the CFA Model. Args: - input_size: (Tuple[int, int]): Input size of the image tensor. + input_size: (tuple[int, int]): Input size of the image tensor. backbone (str): Backbone CNN network. gamma_c (int): gamma_c parameter from the paper. gamma_d (int): gamma_d parameter from the paper. @@ -93,7 +93,7 @@ class CfaModel(DynamicBufferModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, gamma_c: int, gamma_d: int, @@ -129,7 +129,7 @@ def __init__( self.scale = resolution else: raise ValueError( - f"Unknown type {type(resolution)} for `resolution`. Expected types are either int or Tuple[int, int]." + f"Unknown type {type(resolution)} for `resolution`. Expected types are either int or tuple[int, int]." ) self.descriptor = Descriptor(self.gamma_d, backbone) @@ -234,12 +234,12 @@ def __init__(self, gamma_d: int, backbone: str) -> None: self.layer = CoordConv2d(in_channels=dim, out_channels=out_channels, kernel_size=1) - def forward(self, features: Union[List[Tensor], Dict[str, Tensor]]) -> Tensor: + def forward(self, features: list[Tensor] | dict[str, Tensor]) -> Tensor: """Forward pass.""" if isinstance(features, dict): features = list(features.values()) - patch_features: Optional[Tensor] = None + patch_features: Tensor | None = None for i in features: i = F.avg_pool2d(i, 3, 1, 1) / i.size(1) if self.backbone == "efficientnet_b5" else F.avg_pool2d(i, 3, 1, 1) patch_features = ( @@ -268,7 +268,7 @@ def __init__( out_channels: int, kernel_size: _size_2_t, stride: _size_2_t = 1, - padding: Union[str, _size_2_t] = 0, + padding: str | _size_2_t = 0, dilation: _size_2_t = 1, groups: int = 1, bias: bool = True, diff --git a/anomalib/models/cflow/anomaly_map.py b/anomalib/models/cflow/anomaly_map.py index 744cf0b3c6..006c6b17a6 100644 --- a/anomalib/models/cflow/anomaly_map.py +++ b/anomalib/models/cflow/anomaly_map.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union, cast +from __future__ import annotations + +from typing import List, cast import torch import torch.nn.functional as F @@ -16,15 +18,15 @@ class AnomalyMapGenerator(nn.Module): def __init__( self, - image_size: Union[ListConfig, Tuple], - pool_layers: List[str], - ): + image_size: ListConfig | tuple, + pool_layers: list[str], + ) -> None: super().__init__() self.distance = torch.nn.PairwiseDistance(p=2, keepdim=True) self.image_size = image_size if isinstance(image_size, tuple) else tuple(image_size) - self.pool_layers: List[str] = pool_layers + self.pool_layers: list[str] = pool_layers - def compute_anomaly_map(self, distribution: List[Tensor], height: List[int], width: List[int]) -> Tensor: + def compute_anomaly_map(self, distribution: list[Tensor], height: list[int], width: list[int]) -> Tensor: """Compute the layer map based on likelihood estimation. Args: @@ -36,7 +38,7 @@ def compute_anomaly_map(self, distribution: List[Tensor], height: List[int], wid Final Anomaly Map """ - layer_maps: List[Tensor] = [] + layer_maps: list[Tensor] = [] for layer_idx in range(len(self.pool_layers)): layer_distribution = distribution[layer_idx].clone().detach() # Normalize the likelihoods to (-Inf:0] and convert to probs in range [0:1] @@ -58,7 +60,7 @@ def compute_anomaly_map(self, distribution: List[Tensor], height: List[int], wid return anomaly_map - def forward(self, **kwargs: Union[List[Tensor], List[int], List[List]]) -> Tensor: + def forward(self, **kwargs: list[Tensor] | list[int] | list[list]) -> Tensor: """Returns anomaly_map. Expects `distribution`, `height` and 'width' keywords to be passed explicitly @@ -78,7 +80,7 @@ def forward(self, **kwargs: Union[List[Tensor], List[int], List[List]]) -> Tenso raise KeyError(f"Expected keys `distribution`, `height` and `width`. Found {kwargs.keys()}") # placate mypy - distribution: List[Tensor] = cast(List[Tensor], kwargs["distribution"]) - height: List[int] = cast(List[int], kwargs["height"]) - width: List[int] = cast(List[int], kwargs["width"]) + distribution: list[Tensor] = cast(List[Tensor], kwargs["distribution"]) + height: list[int] = cast(List[int], kwargs["height"]) + width: list[int] = cast(List[int], kwargs["width"]) return self.compute_anomaly_map(distribution, height, width) diff --git a/anomalib/models/cflow/lightning_model.py b/anomalib/models/cflow/lightning_model.py index dbc4f9c62d..aa2f45637e 100644 --- a/anomalib/models/cflow/lightning_model.py +++ b/anomalib/models/cflow/lightning_model.py @@ -6,7 +6,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union +from __future__ import annotations import einops import torch @@ -14,7 +14,9 @@ from omegaconf import DictConfig, ListConfig from pytorch_lightning.callbacks import EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY -from torch import optim +from pytorch_lightning.utilities.types import STEP_OUTPUT +from torch import Tensor, optim +from torch.optim import Optimizer from anomalib.models.cflow.torch_model import CflowModel from anomalib.models.cflow.utils import get_logp, positional_encoding_2d @@ -29,9 +31,9 @@ class Cflow(AnomalyModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, - layers: List[str], + layers: list[str], pre_trained: bool = True, fiber_batch_size: int = 64, decoder: str = "freia-cflow", @@ -40,7 +42,7 @@ def __init__( clamp_alpha: float = 1.9, permute_soft: bool = False, lr: float = 0.0001, - ): + ) -> None: super().__init__() self.model: CflowModel = CflowModel( @@ -60,7 +62,7 @@ def __init__( # optimizer this is to be addressed later. self.learning_rate = lr - def configure_optimizers(self) -> torch.optim.Optimizer: + def configure_optimizers(self) -> Optimizer: """Configures optimizers for each decoder. Note: @@ -82,7 +84,7 @@ def configure_optimizers(self) -> torch.optim.Optimizer: ) return optimizer - def training_step(self, batch, _): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training Step of CFLOW. For each batch, decoder layers are trained with a dynamic fiber batch size. @@ -90,8 +92,7 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ per batch of input images Args: - batch: Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: Loss value for the batch @@ -100,7 +101,7 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ opt = self.optimizers() self.model.encoder.eval() - images = batch["image"] + images: Tensor = batch["image"] activation = self.model.encoder(images) avg_loss = torch.zeros([1], dtype=torch.float64).to(images.device) @@ -153,7 +154,7 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ self.log("train_loss", avg_loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": avg_loss} - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of CFLOW. Similar to the training step, encoder features @@ -161,12 +162,11 @@ def validation_step(self, batch, _): # pylint: disable=arguments-differ map is computed. Args: - batch: Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: - Dictionary containing images, anomaly maps, true labels and masks. - These are required in `validation_epoch_end` for feature concatenation. + Dictionary containing images, anomaly maps, true labels and masks. + These are required in `validation_epoch_end` for feature concatenation. """ batch["anomaly_maps"] = self.model(batch["image"]) @@ -178,10 +178,10 @@ class CflowLightning(Cflow): """PL Lightning Module for the CFLOW algorithm. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, @@ -194,10 +194,10 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: clamp_alpha=hparams.model.clamp_alpha, permute_soft=hparams.model.permute_soft, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[EarlyStopping]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/cflow/torch_model.py b/anomalib/models/cflow/torch_model.py index fa86da0932..2c518475d1 100644 --- a/anomalib/models/cflow/torch_model.py +++ b/anomalib/models/cflow/torch_model.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple +from __future__ import annotations + +from typing import Any import einops import torch @@ -19,9 +21,9 @@ class CflowModel(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, - layers: List[str], + layers: list[str], pre_trained: bool = True, fiber_batch_size: int = 64, decoder: str = "freia-cflow", @@ -29,7 +31,7 @@ def __init__( coupling_blocks: int = 8, clamp_alpha: float = 1.9, permute_soft: bool = False, - ): + ) -> None: super().__init__() self.backbone = backbone @@ -57,9 +59,9 @@ def __init__( for parameters in self.encoder.parameters(): parameters.requires_grad = False - self.anomaly_map_generator = AnomalyMapGenerator(image_size=tuple(input_size), pool_layers=self.pool_layers) + self.anomaly_map_generator = AnomalyMapGenerator(image_size=input_size, pool_layers=self.pool_layers) - def forward(self, images): + def forward(self, images) -> Any: """Forward-pass images into the network to extract encoder features and compute probability. Args: @@ -77,8 +79,8 @@ def forward(self, images): distribution = [torch.Tensor(0).to(images.device) for _ in self.pool_layers] - height: List[int] = [] - width: List[int] = [] + height: list[int] = [] + width: list[int] = [] for layer_idx, layer in enumerate(self.pool_layers): encoder_activations = activation[layer] # BxCxHxW diff --git a/anomalib/models/cflow/utils.py b/anomalib/models/cflow/utils.py index dad432ec46..326668ce08 100644 --- a/anomalib/models/cflow/utils.py +++ b/anomalib/models/cflow/utils.py @@ -3,6 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import math @@ -10,28 +12,28 @@ import torch from FrEIA.framework import SequenceINN from FrEIA.modules import AllInOneBlock -from torch import nn +from torch import Tensor, nn logger = logging.getLogger(__name__) -def get_logp(dim_feature_vector: int, p_u: torch.Tensor, logdet_j: torch.Tensor) -> torch.Tensor: +def get_logp(dim_feature_vector: int, p_u: Tensor, logdet_j: Tensor) -> Tensor: """Returns the log likelihood estimation. Args: dim_feature_vector (int): Dimensions of the condition vector - p_u (torch.Tensor): Random variable u - logdet_j (torch.Tensor): log of determinant of jacobian returned from the invertable decoder + p_u (Tensor): Random variable u + logdet_j (Tensor): log of determinant of jacobian returned from the invertable decoder Returns: - torch.Tensor: Log probability + Tensor: Log probability """ ln_sqrt_2pi = -np.log(np.sqrt(2 * np.pi)) # ln(sqrt(2*pi)) logp = dim_feature_vector * ln_sqrt_2pi - 0.5 * torch.sum(p_u**2, 1) + logdet_j return logp -def positional_encoding_2d(condition_vector: int, height: int, width: int) -> torch.Tensor: +def positional_encoding_2d(condition_vector: int, height: int, width: int) -> Tensor: """Creates embedding to store relative position of the feature vector using sine and cosine functions. Args: @@ -43,7 +45,7 @@ def positional_encoding_2d(condition_vector: int, height: int, width: int) -> to ValueError: Cannot generate encoding with conditional vector length not as multiple of 4 Returns: - torch.Tensor: condition_vector x HEIGHT x WIDTH position matrix + Tensor: condition_vector x HEIGHT x WIDTH position matrix """ if condition_vector % 4 != 0: raise ValueError(f"Cannot use sin/cos positional encoding with odd dimension (got dim={condition_vector})") @@ -68,7 +70,7 @@ def positional_encoding_2d(condition_vector: int, height: int, width: int) -> to return pos_encoding -def subnet_fc(dims_in: int, dims_out: int): +def subnet_fc(dims_in: int, dims_out: int) -> nn.Sequential: """Subnetwork which predicts the affine coefficients. Args: diff --git a/anomalib/models/components/base/anomaly_module.py b/anomalib/models/components/base/anomaly_module.py index af527f7105..df7785853c 100644 --- a/anomalib/models/components/base/anomaly_module.py +++ b/anomalib/models/components/base/anomaly_module.py @@ -3,14 +3,17 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from abc import ABC -from typing import Any, Dict, List, Optional, OrderedDict +from typing import Any, OrderedDict from warnings import warn import pytorch_lightning as pl import torch from pytorch_lightning.callbacks.base import Callback +from pytorch_lightning.utilities.types import EPOCH_OUTPUT, STEP_OUTPUT from torch import Tensor, nn from torchmetrics import Metric @@ -32,14 +35,14 @@ class AnomalyModule(pl.LightningModule, ABC): Acts as a base class for all the Anomaly Modules in the library. """ - def __init__(self): + def __init__(self) -> None: super().__init__() logger.info("Initializing %s model.", self.__class__.__name__) self.save_hyperparameters() self.model: nn.Module - self.loss: Tensor - self.callbacks: List[Callback] + self.loss: nn.Module + self.callbacks: list[Callback] self.threshold_method: ThresholdMethod self.image_threshold = AnomalyScoreThreshold().cpu() @@ -50,78 +53,81 @@ def __init__(self): self.image_metrics: AnomalibMetricCollection self.pixel_metrics: AnomalibMetricCollection - def forward(self, batch): # pylint: disable=arguments-differ + def forward(self, batch: dict[str, str | Tensor], *args, **kwargs) -> Any: """Forward-pass input tensor to the module. Args: - batch (Tensor): Input Tensor + batch (dict[str, str | Tensor]): Input batch. Returns: Tensor: Output tensor from the model. """ return self.model(batch) - def validation_step(self, batch, batch_idx) -> dict: # type: ignore # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """To be implemented in the subclasses.""" raise NotImplementedError - def predict_step(self, batch: Any, batch_idx: int, _dataloader_idx: Optional[int] = None) -> Any: + def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any: """Step function called during :meth:`~pytorch_lightning.trainer.trainer.Trainer.predict`. By default, it calls :meth:`~pytorch_lightning.core.lightning.LightningModule.forward`. Override to add any processing logic. Args: - batch (Tensor): Current batch + batch (Any): Current batch batch_idx (int): Index of current batch - _dataloader_idx (int): Index of the current dataloader + dataloader_idx (int): Index of the current dataloader Return: Predicted output """ - outputs = self.validation_step(batch, batch_idx) + del batch_idx, dataloader_idx # These variables are not used. + + outputs: Tensor | dict[str, Any] = self.validation_step(batch) self._post_process(outputs) - outputs["pred_labels"] = outputs["pred_scores"] >= self.image_threshold.value - if "anomaly_maps" in outputs.keys(): - outputs["pred_masks"] = outputs["anomaly_maps"] >= self.pixel_threshold.value - if "pred_boxes" not in outputs.keys(): - outputs["pred_boxes"], outputs["box_scores"] = masks_to_boxes( - outputs["pred_masks"], outputs["anomaly_maps"] - ) - outputs["box_labels"] = [torch.ones(boxes.shape[0]) for boxes in outputs["pred_boxes"]] - # apply thresholding to boxes - if "box_scores" in outputs and "box_labels" not in outputs: - # apply threshold to assign normal/anomalous label to boxes - is_anomalous = [scores > self.pixel_threshold.value for scores in outputs["box_scores"]] - outputs["box_labels"] = [labels.int() for labels in is_anomalous] + if outputs is not None and isinstance(outputs, dict): + outputs["pred_labels"] = outputs["pred_scores"] >= self.image_threshold.value + if "anomaly_maps" in outputs.keys(): + outputs["pred_masks"] = outputs["anomaly_maps"] >= self.pixel_threshold.value + if "pred_boxes" not in outputs.keys(): + outputs["pred_boxes"], outputs["box_scores"] = masks_to_boxes( + outputs["pred_masks"], outputs["anomaly_maps"] + ) + outputs["box_labels"] = [torch.ones(boxes.shape[0]) for boxes in outputs["pred_boxes"]] + # apply thresholding to boxes + if "box_scores" in outputs and "box_labels" not in outputs: + # apply threshold to assign normal/anomalous label to boxes + is_anomalous = [scores > self.pixel_threshold.value for scores in outputs["box_scores"]] + outputs["box_labels"] = [labels.int() for labels in is_anomalous] return outputs - def test_step(self, batch, _): # pylint: disable=arguments-differ + def test_step(self, batch: dict[str, str | Tensor], batch_idx: int, *args, **kwargs) -> STEP_OUTPUT: """Calls validation_step for anomaly map/score calculation. Args: - batch (Tensor): Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch + batch_idx (int): Batch index Returns: Dictionary containing images, features, true labels and masks. These are required in `validation_epoch_end` for feature concatenation. """ - return self.predict_step(batch, _) + return self.predict_step(batch, batch_idx) - def validation_step_end(self, val_step_outputs): # pylint: disable=arguments-differ + def validation_step_end(self, val_step_outputs: STEP_OUTPUT, *args, **kwargs) -> STEP_OUTPUT: """Called at the end of each validation step.""" self._outputs_to_cpu(val_step_outputs) self._post_process(val_step_outputs) return val_step_outputs - def test_step_end(self, test_step_outputs): # pylint: disable=arguments-differ + def test_step_end(self, test_step_outputs: STEP_OUTPUT, *args, **kwargs) -> STEP_OUTPUT: """Called at the end of each test step.""" self._outputs_to_cpu(test_step_outputs) self._post_process(test_step_outputs) return test_step_outputs - def validation_epoch_end(self, outputs): + def validation_epoch_end(self, outputs: EPOCH_OUTPUT) -> None: """Compute threshold and performance metrics. Args: @@ -132,7 +138,7 @@ def validation_epoch_end(self, outputs): self._collect_outputs(self.image_metrics, self.pixel_metrics, outputs) self._log_metrics() - def test_epoch_end(self, outputs): + def test_epoch_end(self, outputs: EPOCH_OUTPUT) -> None: """Compute and save anomaly scores of the test set. Args: @@ -141,7 +147,7 @@ def test_epoch_end(self, outputs): self._collect_outputs(self.image_metrics, self.pixel_metrics, outputs) self._log_metrics() - def _compute_adaptive_threshold(self, outputs): + def _compute_adaptive_threshold(self, outputs: EPOCH_OUTPUT) -> None: self.image_threshold.reset() self.pixel_threshold.reset() self._collect_outputs(self.image_threshold, self.pixel_threshold, outputs) @@ -155,7 +161,11 @@ def _compute_adaptive_threshold(self, outputs): self.pixel_metrics.set_threshold(self.pixel_threshold.value.item()) @staticmethod - def _collect_outputs(image_metric, pixel_metric, outputs): + def _collect_outputs( + image_metric: AnomalibMetricCollection, + pixel_metric: AnomalibMetricCollection, + outputs: EPOCH_OUTPUT, + ) -> None: for output in outputs: image_metric.cpu() image_metric.update(output["pred_scores"], output["label"].int()) @@ -164,37 +174,42 @@ def _collect_outputs(image_metric, pixel_metric, outputs): pixel_metric.update(output["anomaly_maps"], output["mask"].int()) @staticmethod - def _post_process(outputs): + def _post_process(outputs: STEP_OUTPUT) -> None: """Compute labels based on model predictions.""" - if "pred_scores" not in outputs and "anomaly_maps" in outputs: - # infer image scores from anomaly maps - outputs["pred_scores"] = ( - outputs["anomaly_maps"].reshape(outputs["anomaly_maps"].shape[0], -1).max(dim=1).values - ) - elif "pred_scores" not in outputs and "box_scores" in outputs: - # infer image score from bbox confidence scores - outputs["pred_scores"] = torch.zeros_like(outputs["label"]).float() - for idx, (boxes, scores) in enumerate(zip(outputs["pred_boxes"], outputs["box_scores"])): - if boxes.numel(): - outputs["pred_scores"][idx] = scores.max().item() - - if "pred_boxes" in outputs and "anomaly_maps" not in outputs: - # create anomaly maps from bbox predictions for thresholding and evaluation - image_size = tuple(outputs["image"].shape[-2:]) - outputs["anomaly_maps"] = boxes_to_anomaly_maps(outputs["pred_boxes"], outputs["box_scores"], image_size) - outputs["mask"] = boxes_to_masks(outputs["boxes"], image_size) + if isinstance(outputs, dict): + if "pred_scores" not in outputs and "anomaly_maps" in outputs: + # infer image scores from anomaly maps + outputs["pred_scores"] = ( + outputs["anomaly_maps"].reshape(outputs["anomaly_maps"].shape[0], -1).max(dim=1).values + ) + elif "pred_scores" not in outputs and "box_scores" in outputs: + # infer image score from bbox confidence scores + outputs["pred_scores"] = torch.zeros_like(outputs["label"]).float() + for idx, (boxes, scores) in enumerate(zip(outputs["pred_boxes"], outputs["box_scores"])): + if boxes.numel(): + outputs["pred_scores"][idx] = scores.max().item() + + if "pred_boxes" in outputs and "anomaly_maps" not in outputs: + # create anomaly maps from bbox predictions for thresholding and evaluation + image_size: tuple[int, int] = outputs["image"].shape[-2:] + true_boxes: list[Tensor] = outputs["boxes"] + pred_boxes: Tensor = outputs["pred_boxes"] + box_scores: Tensor = outputs["box_scores"] + + outputs["anomaly_maps"] = boxes_to_anomaly_maps(pred_boxes, box_scores, image_size) + outputs["mask"] = boxes_to_masks(true_boxes, image_size) def _outputs_to_cpu(self, output): - if isinstance(output, Dict): + if isinstance(output, dict): for key, value in output.items(): output[key] = self._outputs_to_cpu(value) - elif isinstance(output, List): + elif isinstance(output, list): output = [self._outputs_to_cpu(item) for item in output] elif isinstance(output, Tensor): output = output.cpu() return output - def _log_metrics(self): + def _log_metrics(self) -> None: """Log computed performance metrics.""" if self.pixel_metrics.update_called: self.log_dict(self.pixel_metrics, prog_bar=True) @@ -202,7 +217,7 @@ def _log_metrics(self): else: self.log_dict(self.image_metrics, prog_bar=True) - def _load_normalization_class(self, state_dict: OrderedDict[str, Tensor]): + def _load_normalization_class(self, state_dict: OrderedDict[str, Tensor]) -> None: """Assigns the normalization method to use.""" if "normalization_metrics.max" in state_dict.keys(): self.normalization_metrics = MinMax() diff --git a/anomalib/models/components/classification/kde_classifier.py b/anomalib/models/components/classification/kde_classifier.py index ce6d743dca..8f1160c5ef 100644 --- a/anomalib/models/components/classification/kde_classifier.py +++ b/anomalib/models/components/classification/kde_classifier.py @@ -3,10 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import random from enum import Enum -from typing import Optional, Tuple import torch from torch import Tensor, nn @@ -52,12 +53,12 @@ def __init__( self.register_buffer("max_length", torch.empty([])) self.max_length = torch.empty([]) - def pre_process(self, feature_stack: Tensor, max_length: Optional[Tensor] = None) -> Tuple[Tensor, Tensor]: + def pre_process(self, feature_stack: Tensor, max_length: Tensor | None = None) -> tuple[Tensor, Tensor]: """Pre-process the CNN features. Args: feature_stack (Tensor): Features extracted from CNN - max_length (Optional[Tensor]): Used to unit normalize the feature_stack vector. If ``max_len`` is not + max_length (Tensor | None): Used to unit normalize the feature_stack vector. If ``max_len`` is not provided, the length is calculated from the ``feature_stack``. Defaults to None. Returns: @@ -103,7 +104,7 @@ def fit(self, embeddings: Tensor) -> bool: return True - def compute_kde_scores(self, features: Tensor, as_log_likelihood: Optional[bool] = False) -> Tensor: + def compute_kde_scores(self, features: Tensor, as_log_likelihood: bool | None = False) -> Tensor: """Compute the KDE scores. The scores calculated from the KDE model are converted to densities. If `as_log_likelihood` is set to true then @@ -111,7 +112,7 @@ def compute_kde_scores(self, features: Tensor, as_log_likelihood: Optional[bool] Args: features (Tensor): Features to which the PCA model is fit. - as_log_likelihood (Optional[bool], optional): If true, gets log likelihood scores. Defaults to False. + as_log_likelihood (bool | None, optional): If true, gets log likelihood scores. Defaults to False. Returns: (Tensor): Score diff --git a/anomalib/models/components/dimensionality_reduction/pca.py b/anomalib/models/components/dimensionality_reduction/pca.py index b9e2c7c9bb..7ff85d9296 100644 --- a/anomalib/models/components/dimensionality_reduction/pca.py +++ b/anomalib/models/components/dimensionality_reduction/pca.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Union +from __future__ import annotations import torch from torch import Tensor @@ -19,7 +19,7 @@ class PCA(DynamicBufferModule): or a ratio between 0-1. """ - def __init__(self, n_components: Union[float, int]): + def __init__(self, n_components: int | float): super().__init__() self.n_components = n_components diff --git a/anomalib/models/components/dimensionality_reduction/random_projection.py b/anomalib/models/components/dimensionality_reduction/random_projection.py index f239595d2a..f9c6e66139 100644 --- a/anomalib/models/components/dimensionality_reduction/random_projection.py +++ b/anomalib/models/components/dimensionality_reduction/random_projection.py @@ -7,7 +7,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Optional +from __future__ import annotations import numpy as np import torch @@ -25,11 +25,11 @@ class SparseRandomProjection: Args: eps (float, optional): Minimum distortion rate parameter for calculating Johnson-Lindenstrauss minimum dimensions. Defaults to 0.1. - random_state (Optional[int], optional): Uses the seed to set the random + random_state (int | None, optional): Uses the seed to set the random state for sample_without_replacement function. Defaults to None. """ - def __init__(self, eps: float = 0.1, random_state: Optional[int] = None) -> None: + def __init__(self, eps: float = 0.1, random_state: int | None = None) -> None: self.n_components: int self.sparse_random_matrix: Tensor self.eps = eps @@ -91,7 +91,7 @@ def johnson_lindenstrauss_min_dim(self, n_samples: int, eps: float = 0.1): denominator = (eps**2 / 2) - (eps**3 / 3) return (4 * np.log(n_samples) / denominator).astype(np.int64) - def fit(self, embedding: Tensor) -> "SparseRandomProjection": + def fit(self, embedding: Tensor) -> SparseRandomProjection: """Generates sparse matrix from the embedding tensor. Args: diff --git a/anomalib/models/components/feature_extractors/timm.py b/anomalib/models/components/feature_extractors/timm.py index 3a12baf337..363cde0320 100644 --- a/anomalib/models/components/feature_extractors/timm.py +++ b/anomalib/models/components/feature_extractors/timm.py @@ -6,9 +6,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import warnings -from typing import Dict, List import timm import torch @@ -42,7 +43,7 @@ class TimmFeatureExtractor(nn.Module): [torch.Size([32, 64, 64, 64]), torch.Size([32, 128, 32, 32]), torch.Size([32, 256, 16, 16])] """ - def __init__(self, backbone: str, layers: List[str], pre_trained: bool = True, requires_grad: bool = False): + def __init__(self, backbone: str, layers: list[str], pre_trained: bool = True, requires_grad: bool = False): super().__init__() self.backbone = backbone self.layers = layers @@ -58,7 +59,7 @@ def __init__(self, backbone: str, layers: List[str], pre_trained: bool = True, r self.out_dims = self.feature_extractor.feature_info.channels() self._features = {layer: torch.empty(0) for layer in self.layers} - def _map_layer_to_idx(self, offset: int = 3) -> List[int]: + def _map_layer_to_idx(self, offset: int = 3) -> list[int]: """Maps set of layer names to indices of model. Args: @@ -84,7 +85,7 @@ def _map_layer_to_idx(self, offset: int = 3) -> List[int]: return idx - def forward(self, inputs: Tensor) -> Dict[str, Tensor]: + def forward(self, inputs: Tensor) -> dict[str, Tensor]: """Forward-pass input tensor into the CNN. Args: diff --git a/anomalib/models/components/feature_extractors/torchfx.py b/anomalib/models/components/feature_extractors/torchfx.py index 6f0972c8bf..9971cde32f 100644 --- a/anomalib/models/components/feature_extractors/torchfx.py +++ b/anomalib/models/components/feature_extractors/torchfx.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import importlib from dataclasses import dataclass, field -from typing import Callable, Dict, List, Optional, Union +from typing import Callable import torch from torch import Tensor, nn @@ -18,20 +20,20 @@ class BackboneParams: """Used for serializing the backbone.""" - class_path: Union[str, nn.Module] - init_args: Dict = field(default_factory=dict) + class_path: str | nn.Module + init_args: dict = field(default_factory=dict) class TorchFXFeatureExtractor(nn.Module): """Extract features from a CNN. Args: - backbone (Union[str, BackboneParams, Dict, nn.Module]): The backbone to which the feature extraction hooks are + backbone (str | BackboneParams | dict | nn.Module): The backbone to which the feature extraction hooks are attached. If the name is provided, the model is loaded from torchvision. Otherwise, the model class can be provided and it will try to load the weights from the provided weights file. return_nodes (Iterable[str]): List of layer names of the backbone to which the hooks are attached. You can find the names of these nodes by using ``get_graph_node_names`` function. - weights (Optional[Union[WeightsEnum,str]]): Weights enum to use for the model. Torchvision models require + weights (str | WeightsEnum | None): Weights enum to use for the model. Torchvision models require ``WeightsEnum``. These enums are defined in ``torchvision.models.``. You can pass the weights path for custom models. requires_grad (bool): Models like ``stfpm`` use the feature extractor for training. In such cases we should @@ -69,9 +71,9 @@ class TorchFXFeatureExtractor(nn.Module): def __init__( self, - backbone: Union[str, BackboneParams, Dict, nn.Module], - return_nodes: List[str], - weights: Optional[Union[WeightsEnum, str]] = None, + backbone: str | BackboneParams | dict | nn.Module, + return_nodes: list[str], + weights: str | WeightsEnum | None = None, requires_grad: bool = False, ): super().__init__() @@ -85,19 +87,19 @@ def __init__( def initialize_feature_extractor( self, backbone: BackboneParams, - return_nodes: List[str], - weights: Optional[Union[WeightsEnum, str]] = None, + return_nodes: list[str], + weights: str | WeightsEnum | None = None, requires_grad: bool = False, - ) -> Union[GraphModule, nn.Module]: + ) -> GraphModule | nn.Module: """Extract features from a CNN. Args: - backbone (Union[str, BackboneParams]): The backbone to which the feature extraction hooks are attached. + backbone (BackboneParams): The backbone to which the feature extraction hooks are attached. If the name is provided, the model is loaded from torchvision. Otherwise, the model class can be provided and it will try to load the weights from the provided weights file. return_nodes (Iterable[str]): List of layer names of the backbone to which the hooks are attached. You can find the names of these nodes by using ``get_graph_node_names`` function. - weights (Optional[Union[WeightsEnum,str]]): Weights enum to use for the model. Torchvision models require + weights (str | WeightsEnum | None): Weights enum to use for the model. Torchvision models require ``WeightsEnum``. These enums are defined in ``torchvision.models.``. You can pass the weights path for custom models. requires_grad (bool): Models like ``stfpm`` use the feature extractor for training. In such cases we should @@ -141,7 +143,7 @@ def _get_backbone_class(backbone: str) -> Callable[..., nn.Module]: >>> TorchFXFeatureExtractor._get_backbone_class("efficientnet_b5") torchvision.models.efficientnet.EfficientNet> @@ -170,6 +172,6 @@ def _get_backbone_class(backbone: str) -> Callable[..., nn.Module]: return backbone_class - def forward(self, inputs: Tensor) -> Dict[str, Tensor]: + def forward(self, inputs: Tensor) -> dict[str, Tensor]: """Extract features from the input.""" return self.feature_extractor(inputs) diff --git a/anomalib/models/components/feature_extractors/utils.py b/anomalib/models/components/feature_extractors/utils.py index a9336821fa..5780ad3304 100644 --- a/anomalib/models/components/feature_extractors/utils.py +++ b/anomalib/models/components/feature_extractors/utils.py @@ -1,6 +1,6 @@ """Utility functions to manipulate feature extractors.""" -from typing import Dict, List, Tuple, Union +from __future__ import annotations import torch from torch.fx.graph_module import GraphModule @@ -9,15 +9,15 @@ def dryrun_find_featuremap_dims( - feature_extractor: Union[FeatureExtractor, GraphModule], - input_size: Tuple[int, int], - layers: List[str], -) -> Dict[str, Dict[str, Union[int, Tuple[int, int]]]]: + feature_extractor: FeatureExtractor | GraphModule, + input_size: tuple[int, int], + layers: list[str], +) -> dict[str, dict[str, int | tuple[int, int]]]: """Dry run an empty image of `input_size` size to get the featuremap tensors' dimensions (num_features, resolution). Returns: - Tuple[int, int]: maping of `layer -> dimensions dict` - Each `dimension dict` has two keys: `num_features` (int) and `resolution`(Tuple[int, int]). + tuple[int, int]: maping of `layer -> dimensions dict` + Each `dimension dict` has two keys: `num_features` (int) and `resolution`(tuple[int, int]). """ dryrun_input = torch.empty(1, 3, *input_size) diff --git a/anomalib/models/components/filters/blur.py b/anomalib/models/components/filters/blur.py index 5edadc81d7..e56e3eba58 100644 --- a/anomalib/models/components/filters/blur.py +++ b/anomalib/models/components/filters/blur.py @@ -1,6 +1,6 @@ """Gaussian blurring via pytorch.""" -from typing import Optional, Tuple, Union +from __future__ import annotations from kornia.filters import get_gaussian_kernel2d from kornia.filters.filter import _compute_padding @@ -31,9 +31,9 @@ class GaussianBlur2d(nn.Module): def __init__( self, - sigma: Union[Tuple[float, float], float], + sigma: float | tuple[float, float], channels: int = 1, - kernel_size: Optional[Union[Tuple[int, int], int]] = None, + kernel_size: int | tuple[int, int] | None = None, normalize: bool = True, border_type: str = "reflect", padding: str = "same", @@ -41,9 +41,9 @@ def __init__( """Initialize model, setup kernel etc.. Args: - sigma (Union[Tuple[float, float], float]): standard deviation to use for constructing the Gaussian kernel. + sigma (float | tuple[float, float]): standard deviation to use for constructing the Gaussian kernel. channels (int): channels of the input. Defaults to 1. - kernel_size (Optional[Union[Tuple[int, int], int]]): size of the Gaussian kernel to use. Defaults to None. + kernel_size (int | tuple[int, int] | None): size of the Gaussian kernel to use. Defaults to None. normalize (bool, optional): Whether to normalize the kernel or not (i.e. all elements sum to 1). Defaults to True. border_type (str, optional): Border type to use for padding of the input. Defaults to "reflect". diff --git a/anomalib/models/components/sampling/k_center_greedy.py b/anomalib/models/components/sampling/k_center_greedy.py index 1703c74149..920b16a5d0 100644 --- a/anomalib/models/components/sampling/k_center_greedy.py +++ b/anomalib/models/components/sampling/k_center_greedy.py @@ -5,7 +5,7 @@ . https://arxiv.org/abs/1708.00489 """ -from typing import List, Optional +from __future__ import annotations import torch import torch.nn.functional as F @@ -44,11 +44,11 @@ def reset_distances(self) -> None: """Reset minimum distances.""" self.min_distances = None - def update_distances(self, cluster_centers: List[int]) -> None: + def update_distances(self, cluster_centers: list[int]) -> None: """Update min distances given cluster centers. Args: - cluster_centers (List[int]): indices of cluster centers + cluster_centers (list[int]): indices of cluster centers """ if cluster_centers: @@ -77,7 +77,7 @@ def get_new_idx(self) -> int: return idx - def select_coreset_idxs(self, selected_idxs: Optional[List[int]] = None) -> List[int]: + def select_coreset_idxs(self, selected_idxs: list[int] | None = None) -> list[int]: """Greedily form a coreset to minimize the maximum distance of a cluster. Args: @@ -98,7 +98,7 @@ def select_coreset_idxs(self, selected_idxs: Optional[List[int]] = None) -> List self.features = self.embedding.reshape(self.embedding.shape[0], -1) self.update_distances(cluster_centers=selected_idxs) - selected_coreset_idxs: List[int] = [] + selected_coreset_idxs: list[int] = [] idx = int(torch.randint(high=self.n_observations, size=(1,)).item()) for _ in range(self.coreset_size): self.update_distances(cluster_centers=[idx]) @@ -110,7 +110,7 @@ def select_coreset_idxs(self, selected_idxs: Optional[List[int]] = None) -> List return selected_coreset_idxs - def sample_coreset(self, selected_idxs: Optional[List[int]] = None) -> Tensor: + def sample_coreset(self, selected_idxs: list[int] | None = None) -> Tensor: """Select coreset from the embedding. Args: diff --git a/anomalib/models/components/stats/kde.py b/anomalib/models/components/stats/kde.py index 0929165869..5fbe4bbaff 100644 --- a/anomalib/models/components/stats/kde.py +++ b/anomalib/models/components/stats/kde.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math -from typing import Optional import torch from torch import Tensor @@ -16,10 +17,10 @@ class GaussianKDE(DynamicBufferModule): """Gaussian Kernel Density Estimation. Args: - dataset (Optional[Tensor], optional): Dataset on which to fit the KDE model. Defaults to None. + dataset (Tensor | None, optional): Dataset on which to fit the KDE model. Defaults to None. """ - def __init__(self, dataset: Optional[Tensor] = None): + def __init__(self, dataset: Tensor | None = None): super().__init__() if dataset is not None: diff --git a/anomalib/models/components/stats/multi_variate_gaussian.py b/anomalib/models/components/stats/multi_variate_gaussian.py index 14eb8f3c73..d6ba6f8baa 100644 --- a/anomalib/models/components/stats/multi_variate_gaussian.py +++ b/anomalib/models/components/stats/multi_variate_gaussian.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, List, Optional +from __future__ import annotations + +from typing import Any import torch from torch import Tensor, nn @@ -26,7 +28,7 @@ def _cov( observations: Tensor, rowvar: bool = False, bias: bool = False, - ddof: Optional[int] = None, + ddof: int | None = None, aweights: Tensor = None, ) -> Tensor: """Estimates covariance matrix like numpy.cov. @@ -43,7 +45,7 @@ def _cov( number of observations given (unbiased estimate). If `bias` is True, then normalization is by ``N``. These values can be overridden by using the keyword ``ddof`` in numpy versions >= 1.5. Defaults to False - ddof (Optional, int): If not ``None`` the default value implied by `bias` is overridden. + ddof (int | None): If not ``None`` the default value implied by `bias` is overridden. Note that ``ddof=1`` will return the unbiased estimate, even if both `fweights` and `aweights` are specified, and ``ddof=0`` will return the simple average. See the notes for the details. The default value @@ -104,7 +106,7 @@ def _cov( return covariance.squeeze() - def forward(self, embedding: Tensor) -> List[Tensor]: + def forward(self, embedding: Tensor) -> list[Tensor]: """Calculate multivariate Gaussian distribution. Args: @@ -128,7 +130,7 @@ def forward(self, embedding: Tensor) -> List[Tensor]: return [self.mean, self.inv_covariance] - def fit(self, embedding: Tensor) -> List[Tensor]: + def fit(self, embedding: Tensor) -> list[Tensor]: """Fit multi-variate gaussian distribution to the input embedding. Args: diff --git a/anomalib/models/csflow/anomaly_map.py b/anomalib/models/csflow/anomaly_map.py index 918cd3c7f4..3fced453b8 100644 --- a/anomalib/models/csflow/anomaly_map.py +++ b/anomalib/models/csflow/anomaly_map.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from enum import Enum -from typing import Tuple import torch import torch.nn.functional as F @@ -22,11 +23,11 @@ class AnomalyMapGenerator(nn.Module): """Anomaly Map Generator for CS-Flow model. Args: - input_dims (Tuple[int, int, int]): Input dimensions. + input_dims (tuple[int, int, int]): Input dimensions. mode (AnomalyMapMode): Anomaly map mode. Defaults to AnomalyMapMode.ALL. """ - def __init__(self, input_dims: Tuple[int, int, int], mode: AnomalyMapMode = AnomalyMapMode.ALL): + def __init__(self, input_dims: tuple[int, int, int], mode: AnomalyMapMode = AnomalyMapMode.ALL) -> None: super().__init__() self.mode = mode self.input_dims = input_dims diff --git a/anomalib/models/csflow/lightning_model.py b/anomalib/models/csflow/lightning_model.py index f4df795780..ec99fe198b 100644 --- a/anomalib/models/csflow/lightning_model.py +++ b/anomalib/models/csflow/lightning_model.py @@ -6,13 +6,15 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Dict, Tuple, Union import torch from omegaconf import DictConfig, ListConfig -from pytorch_lightning.callbacks import EarlyStopping +from pytorch_lightning.callbacks import Callback, EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -30,7 +32,7 @@ class Csflow(AnomalyModule): """Fully Convolutional Cross-Scale-Flows for Image-based Defect Detection. Args: - input_size (Tuple[int, int]): Size of the model input. + input_size (tuple[int, int]): Size of the model input. n_coupling_blocks (int): Number of coupling blocks in the model. cross_conv_hidden_channels (int): Number of hidden channels in the cross convolution. clamp (int): Clamp value for glow layer. @@ -39,12 +41,12 @@ class Csflow(AnomalyModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], cross_conv_hidden_channels: int, n_coupling_blocks: int, clamp: int, num_channels: int, - ): + ) -> None: super().__init__() self.model: CsFlowModel = CsFlowModel( input_size=input_size, @@ -55,11 +57,11 @@ def __init__( ) self.loss = CsFlowLoss() - def training_step(self, batch, _) -> Dict[str, Tensor]: + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training Step of CS-Flow. Args: - batch (Tensor): Input batch + batch (dict[str, str | Tensor]): Input batch _: Index of the batch. Returns: @@ -71,14 +73,14 @@ def training_step(self, batch, _) -> Dict[str, Tensor]: self.log("train_loss", loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": loss} - def validation_step(self, batch, _) -> Dict[str, Tensor]: + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation step for CS Flow. Args: batch (Tensor): Input batch Returns: - Dict[str, Tensor]: Dictionary containing the anomaly map, scores, etc. + dict[str, Tensor]: Dictionary containing the anomaly map, scores, etc. """ anomaly_maps, anomaly_scores = self.model(batch["image"]) batch["anomaly_maps"] = anomaly_maps @@ -90,10 +92,10 @@ class CsflowLightning(Csflow): """Fully Convolutional Cross-Scale-Flows for Image-based Defect Detection. Args: - hprams (Union[DictConfig, ListConfig]): Model params + hprams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]): + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, n_coupling_blocks=hparams.model.n_coupling_blocks, @@ -101,10 +103,10 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]): clamp=hparams.model.clamp, num_channels=3, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[Callback]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/csflow/torch_model.py b/anomalib/models/csflow/torch_model.py index 8c58eddc64..153672f73e 100644 --- a/anomalib/models/csflow/torch_model.py +++ b/anomalib/models/csflow/torch_model.py @@ -11,8 +11,9 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from math import exp -from typing import Dict, List, Optional, Tuple import numpy as np import torch @@ -49,7 +50,7 @@ def __init__( leaky_slope: float = 0.1, batch_norm: bool = False, use_gamma: bool = True, - ): + ) -> None: super().__init__() pad = kernel_size // 2 @@ -146,13 +147,13 @@ def __init__( self.leaky_relu = nn.LeakyReLU(self.leaky_slope) - def forward(self, scale0, scale1, scale2) -> Tuple[Tensor, Tensor, Tensor]: + def forward(self, scale0, scale1, scale2) -> tuple[Tensor, Tensor, Tensor]: """Applies the cross convolution to the three scales. This block is represented in figure 4 of the paper. Returns: - Tuple[Tensor, Tensor, Tensor]: Tensors indicating scale and transform parameters as a single tensor for + tuple[Tensor, Tensor, Tensor]: Tensors indicating scale and transform parameters as a single tensor for each scale. The scale parameters are the first part across channel dimension and the transform parameters are the second. """ @@ -195,11 +196,11 @@ class ParallelPermute(InvertibleModule): """Permutes input vector in a random but fixed way. Args: - dim (List[Tuple[int]]): Dimension of the input vector. - seed (Optional[float]=None): Seed for the random permutation. + dim (list[tuple[int]]): Dimension of the input vector. + seed (float | None=None): Seed for the random permutation. """ - def __init__(self, dims_in: List[Tuple[int]], seed: Optional[float] = None): + def __init__(self, dims_in: list[tuple[int]], seed: float | None = None) -> None: super().__init__(dims_in) self.n_inputs: int = len(dims_in) self.in_channels = [dims_in[i][0] for i in range(self.n_inputs)] @@ -214,14 +215,14 @@ def __init__(self, dims_in: List[Tuple[int]], seed: Optional[float] = None): self.perm.append(perm) self.perm_inv.append(perm_inv) - def get_random_perm(self, index: int) -> Tuple[Tensor, Tensor]: + def get_random_perm(self, index: int) -> tuple[Tensor, Tensor]: """Returns a random permutation of the channels for each input. Args: i: index of the input Returns: - Tuple[Tensor, Tensor]: permutation and inverse permutation + tuple[Tensor, Tensor]: permutation and inverse permutation """ perm = np.random.permutation(self.in_channels[index]) perm_inv = np.zeros_like(perm) @@ -233,7 +234,7 @@ def get_random_perm(self, index: int) -> Tuple[Tensor, Tensor]: return perm, perm_inv # pylint: disable=unused-argument - def forward(self, input_tensor: List[Tensor], rev=False, jac=True) -> Tuple[List[Tensor], float]: + def forward(self, input_tensor: list[Tensor], rev=False, jac=True) -> tuple[list[Tensor], float]: """Applies the permutation to the input. Args: @@ -242,14 +243,14 @@ def forward(self, input_tensor: List[Tensor], rev=False, jac=True) -> Tuple[List jac: (unused) if True, computes the log determinant of the Jacobian Returns: - Tuple[Tensor, Tensor]: output tensor and log determinant of the Jacobian + tuple[Tensor, Tensor]: output tensor and log determinant of the Jacobian """ if not rev: return [input_tensor[i][:, self.perm[i]] for i in range(self.n_inputs)], 0.0 return [input_tensor[i][:, self.perm_inv[i]] for i in range(self.n_inputs)], 0.0 - def output_dims(self, input_dims: List[Tuple[int]]) -> List[Tuple[int]]: + def output_dims(self, input_dims: list[tuple[int]]) -> list[tuple[int]]: """Returns the output dimensions of the module.""" return input_dims @@ -258,12 +259,12 @@ class ParallelGlowCouplingLayer(InvertibleModule): """Coupling block that follows the GLOW design but is applied to all the scales in parallel. Args: - dims_in (List[Tuple[int]]): list of dimensions of the input tensors - subnet_args (Dict): arguments of the subnet + dims_in (list[tuple[int]]): list of dimensions of the input tensors + subnet_args (dict): arguments of the subnet clamp (float): clamp value for the output of the subnet """ - def __init__(self, dims_in: List[Tuple[int]], subnet_args: Dict, clamp: float = 5.0): + def __init__(self, dims_in: list[tuple[int]], subnet_args: dict, clamp: float = 5.0) -> None: super().__init__(dims_in) channels = dims_in[0][0] self.ndims = len(dims_in[0]) @@ -279,20 +280,19 @@ def __init__(self, dims_in: List[Tuple[int]], subnet_args: Dict, clamp: float = self.cross_convolution1 = CrossConvolutions(self.split_len1, self.split_len2 * 2, **subnet_args) self.cross_convolution2 = CrossConvolutions(self.split_len2, self.split_len1 * 2, **subnet_args) - def exp(self, input_tensor): + def exp(self, input_tensor: Tensor) -> Tensor: """Exponentiates the input and, optionally, clamps it to avoid numerical issues.""" if self.clamp > 0: return torch.exp(self.log_e(input_tensor)) return torch.exp(input_tensor) - def log_e(self, input_tensor): + def log_e(self, input_tensor: Tensor) -> Tensor: """Returns log of input. And optionally clamped to avoid numerical issues.""" if self.clamp > 0: return self.clamp * 0.636 * torch.atan(input_tensor / self.clamp) return input_tensor - # pylint: disable=unused-argument - def forward(self, input_tensor: List[Tensor], rev=False, jac=True): + def forward(self, input_tensor: list[Tensor], rev=False, jac=True) -> tuple[list[Tensor], Tensor]: """Applies GLOW coupling for the three scales.""" # Even channel split. The two splits are used by cross-scale convolution to compute scale and transform @@ -374,7 +374,7 @@ def forward(self, input_tensor: List[Tensor], rev=False, jac=True): # Since Jacobians are only used for computing loss and summed in the loss, the idea is to sum them here return [z_dist0, z_dist1, z_dist2], torch.stack([jac0, jac1, jac2], dim=1).sum() - def output_dims(self, input_dims: List[Tuple[int]]): + def output_dims(self, input_dims: list[tuple[int]]) -> list[tuple[int]]: """Output dimensions of the module.""" return input_dims @@ -383,15 +383,15 @@ class CrossScaleFlow(nn.Module): """Cross scale coupling layer. Args: - input_dims (Tuple[int, int, int]): Input dimensions of the module. + input_dims (tuple[int, int, int]): Input dimensions of the module. n_coupling_blocks (int): Number of coupling blocks. clamp (float): Clamp value for the inputs. corss_conv_hidden_channels (int): Number of hidden channels in the cross convolution. """ def __init__( - self, input_dims: Tuple[int, int, int], n_coupling_blocks: int, clamp: float, cross_conv_hidden_channels: int - ): + self, input_dims: tuple[int, int, int], n_coupling_blocks: int, clamp: float, cross_conv_hidden_channels: int + ) -> None: super().__init__() self.input_dims = input_dims self.n_coupling_blocks = n_coupling_blocks @@ -400,12 +400,15 @@ def __init__( self.cross_conv_hidden_channels = cross_conv_hidden_channels self.graph = self._create_graph() - def _create_graph(self): - nodes = [] + def _create_graph(self) -> GraphINN: + nodes: list[Node] = [] # 304 is the number of features extracted from EfficientNet-B5 feature extractor - nodes.append(InputNode(304, (self.input_dims[1] // 32), (self.input_dims[2] // 32), name="input")) - nodes.append(InputNode(304, (self.input_dims[1] // 64), (self.input_dims[2] // 64), name="input2")) - nodes.append(InputNode(304, (self.input_dims[1] // 128), (self.input_dims[2] // 128), name="input3")) + input_nodes = [ + InputNode(304, (self.input_dims[1] // 32), (self.input_dims[2] // 32), name="input"), + InputNode(304, (self.input_dims[1] // 64), (self.input_dims[2] // 64), name="input2"), + InputNode(304, (self.input_dims[1] // 128), (self.input_dims[2] // 128), name="input3"), + ] + nodes.extend(input_nodes) for coupling_block in range(self.n_coupling_blocks): if coupling_block == 0: @@ -413,37 +416,43 @@ def _create_graph(self): else: node_to_permute = [nodes[-1].out0, nodes[-1].out1, nodes[-1].out2] - nodes.append( - Node(node_to_permute, ParallelPermute, {"seed": coupling_block}, name=f"permute_{coupling_block}") + permute_node = Node( + inputs=node_to_permute, + module_type=ParallelPermute, + module_args={"seed": coupling_block}, + name=f"permute_{coupling_block}", ) - nodes.append( - Node( - [nodes[-1].out0, nodes[-1].out1, nodes[-1].out2], - ParallelGlowCouplingLayer, - { - "clamp": self.clamp, - "subnet_args": { - "channels_hidden": self.cross_conv_hidden_channels, - "kernel_size": self.kernel_sizes[coupling_block], - }, + nodes.extend([permute_node]) + coupling_layer_node = Node( + inputs=[nodes[-1].out0, nodes[-1].out1, nodes[-1].out2], + module_type=ParallelGlowCouplingLayer, + module_args={ + "clamp": self.clamp, + "subnet_args": { + "channels_hidden": self.cross_conv_hidden_channels, + "kernel_size": self.kernel_sizes[coupling_block], }, - name=f"fc1_{coupling_block}", - ) + }, + name=f"fc1_{coupling_block}", ) - - nodes.append(OutputNode([nodes[-1].out0], name="output_end0")) - nodes.append(OutputNode([nodes[-2].out1], name="output_end1")) - nodes.append(OutputNode([nodes[-3].out2], name="output_end2")) + nodes.extend([coupling_layer_node]) + + output_nodes = [ + OutputNode([nodes[-1].out0], name="output_end0"), + OutputNode([nodes[-1].out1], name="output_end1"), + OutputNode([nodes[-1].out2], name="output_end2"), + ] + nodes.extend(output_nodes) return GraphINN(nodes) - def forward(self, inputs: Tensor) -> Tuple[Tensor, Tensor]: + def forward(self, inputs: Tensor) -> tuple[Tensor, Tensor]: """Forward pass. Args: inputs (Tensor): Input tensor. Returns: - Tuple[Tensor, Tensor]: Output tensor and log determinant of Jacobian. + tuple[Tensor, Tensor]: Output tensor and log determinant of Jacobian. """ return self.graph(inputs) @@ -455,10 +464,10 @@ class MultiScaleFeatureExtractor(nn.Module): Args: n_scales (int): Number of scales for input image. - input_size (Tuple[int, int]): Size of input image. + input_size (tuple[int, int]): Size of input image. """ - def __init__(self, n_scales: int, input_size: Tuple[int, int]): + def __init__(self, n_scales: int, input_size: tuple[int, int]) -> None: super().__init__() self.n_scales = n_scales @@ -467,14 +476,14 @@ def __init__(self, n_scales: int, input_size: Tuple[int, int]): backbone="efficientnet_b5", weights=EfficientNet_B5_Weights.DEFAULT, return_nodes=["features.6.8"] ) - def forward(self, input_tensor: Tensor) -> List[Tensor]: + def forward(self, input_tensor: Tensor) -> list[Tensor]: """Extracts features at three scales. Args: input_tensor (Tensor): Input images. Returns: - List[Tensor]: List of tensors containing features at three scales. + list[Tensor]: List of tensors containing features at three scales. """ output = [] for scale in range(self.n_scales): @@ -495,7 +504,7 @@ class CsFlowModel(nn.Module): """CS Flow Module. Args: - input_size (Tuple[int, int]): Input image size. + input_size (tuple[int, int]): Input image size. cross_conv_hidden_channels (int): Number of hidden channels in the cross convolution. n_coupling_blocks (int): Number of coupling blocks. clamp (float): Clamp value for the coupling blocks. @@ -504,12 +513,12 @@ class CsFlowModel(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], cross_conv_hidden_channels: int, n_coupling_blocks: int = 4, clamp: int = 3, num_channels: int = 3, - ): + ) -> None: super().__init__() self.input_dims = (num_channels, *input_size) @@ -524,15 +533,15 @@ def __init__( ) self.anomaly_map_generator = AnomalyMapGenerator(input_dims=self.input_dims, mode=AnomalyMapMode.ALL) - def forward(self, images) -> Tuple[Tensor, Tensor]: + def forward(self, images: Tensor) -> tuple[Tensor, Tensor]: """Forward method of the model. Args: images (Tensor): Input images. Returns: - Tuple[Tensor, Tensor]: During training: Tuple containing the z_distribution for three scales and the sum - of log determinant of the Jacobian. During evaluation: Tuple containing anomaly maps and anomaly scores + tuple[Tensor, Tensor]: During training: tuple containing the z_distribution for three scales and the sum + of log determinant of the Jacobian. During evaluation: tuple containing anomaly maps and anomaly scores """ features = self.feature_extractor(images) if self.training: @@ -554,9 +563,7 @@ def _compute_anomaly_scores(self, z_dists: Tensor) -> Tensor: Tensor: Anomaly scores. """ # z_dist is a 3 length list of tensors with shape b x 304 x fx x fy - flat_maps: List[Tensor] = [] - for z_dist in z_dists: - flat_maps.append(z_dist.reshape(z_dist.shape[0], -1)) + flat_maps = [z_dist.reshape(z_dist.shape[0], -1) for z_dist in z_dists] flat_maps_tensor = torch.cat(flat_maps, dim=1) anomaly_scores = torch.mean(flat_maps_tensor**2 / 2, dim=1) return anomaly_scores diff --git a/anomalib/models/dfkde/lightning_model.py b/anomalib/models/dfkde/lightning_model.py index e740996c86..dc39586179 100644 --- a/anomalib/models/dfkde/lightning_model.py +++ b/anomalib/models/dfkde/lightning_model.py @@ -3,12 +3,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Union import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -38,13 +40,13 @@ class Dfkde(AnomalyModule): def __init__( self, - layers: List[str], + layers: list[str], backbone: str, pre_trained: bool = True, n_pca_components: int = 16, feature_scaling_method: FeatureScalingMethod = FeatureScalingMethod.SCALE, max_training_points: int = 40000, - ): + ) -> None: super().__init__() self.model = DfkdeModel( @@ -56,19 +58,18 @@ def __init__( max_training_points=max_training_points, ) - self.embeddings: List[Tensor] = [] + self.embeddings: list[Tensor] = [] @staticmethod - def configure_optimizers(): # pylint: disable=arguments-differ + def configure_optimizers() -> None: # pylint: disable=arguments-differ """DFKDE doesn't require optimization, therefore returns no optimizers.""" return None - def training_step(self, batch, _batch_idx): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> None: """Training Step of DFKDE. For each batch, features are extracted from the CNN. Args: - batch (Dict[str, Any]): Batch containing image filename, image, label and mask - _batch_idx: Index of the batch. + batch (batch: dict[str, str | Tensor]): Batch containing image filename, image, label and mask Returns: Deep CNN features. @@ -92,13 +93,13 @@ def on_validation_start(self) -> None: logger.info("Fitting a KDE model to the embedding collected from the training set.") self.model.classifier.fit(embeddings) - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of DFKDE. Similar to the training step, features are extracted from the CNN for each batch. Args: - batch: Input batch + batch (dict[str, str | Tensor]): Input batch Returns: Dictionary containing probability, prediction and ground truth values. @@ -112,10 +113,10 @@ class DfkdeLightning(Dfkde): """DFKDE: Deep Feature Kernel Density Estimation. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( layers=hparams.model.layers, backbone=hparams.model.backbone, @@ -124,5 +125,5 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: feature_scaling_method=FeatureScalingMethod(hparams.model.feature_scaling_method), max_training_points=hparams.model.max_training_points, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) diff --git a/anomalib/models/dfkde/torch_model.py b/anomalib/models/dfkde/torch_model.py index dfc71c47a0..5746442820 100644 --- a/anomalib/models/dfkde/torch_model.py +++ b/anomalib/models/dfkde/torch_model.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List import torch import torch.nn.functional as F @@ -35,13 +36,13 @@ class DfkdeModel(nn.Module): def __init__( self, - layers: List[str], + layers: list[str], backbone: str, pre_trained: bool = True, n_pca_components: int = 16, feature_scaling_method: FeatureScalingMethod = FeatureScalingMethod.SCALE, max_training_points: int = 40000, - ): + ) -> None: super().__init__() self.feature_extractor = FeatureExtractor(backbone=backbone, pre_trained=pre_trained, layers=layers).eval() diff --git a/anomalib/models/dfm/lightning_model.py b/anomalib/models/dfm/lightning_model.py index b90525ca86..c83ad7a1c5 100644 --- a/anomalib/models/dfm/lightning_model.py +++ b/anomalib/models/dfm/lightning_model.py @@ -3,12 +3,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Tuple, Union import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -25,7 +27,7 @@ class Dfm(AnomalyModule): Args: backbone (str): Backbone CNN network layer (str): Layer to extract features from the backbone CNN - input_size (Tuple[int, int]): Input size for the model. + input_size (tuple[int, int]): Input size for the model. pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. pooling_kernel_size (int, optional): Kernel size to pool features extracted from the CNN. Defaults to 4. @@ -40,12 +42,12 @@ def __init__( self, backbone: str, layer: str, - input_size: Tuple[int, int], + input_size: tuple[int, int], pre_trained: bool = True, pooling_kernel_size: int = 4, pca_level: float = 0.97, score_type: str = "fre", - ): + ) -> None: super().__init__() self.model: DFMModel = DFMModel( @@ -57,7 +59,7 @@ def __init__( n_comps=pca_level, score_type=score_type, ) - self.embeddings: List[Tensor] = [] + self.embeddings: list[Tensor] = [] self.score_type = score_type @staticmethod @@ -65,13 +67,13 @@ def configure_optimizers() -> None: # pylint: disable=arguments-differ """DFM doesn't require optimization, therefore returns no optimizers.""" return None - def training_step(self, batch, _): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> None: """Training Step of DFM. For each batch, features are extracted from the CNN. Args: - batch (Dict[str, Tensor]): Input batch + batch (dict[str, str | Tensor]): Input batch _: Index of the batch. Returns: @@ -96,13 +98,13 @@ def on_validation_start(self) -> None: logger.info("Fitting a PCA and a Gaussian model to dataset.") self.model.fit(embeddings) - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of DFM. Similar to the training step, features are extracted from the CNN for each batch. Args: - batch (List[Dict[str, Any]]): Input batch + batch (dict[str, str | Tensor]): Input batch Returns: Dictionary containing FRE anomaly scores and anomaly maps. @@ -119,10 +121,10 @@ class DfmLightning(Dfm): """DFM: Deep Featured Kernel Density Estimation. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, @@ -132,5 +134,5 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: pca_level=hparams.model.pca_level, score_type=hparams.model.score_type, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) diff --git a/anomalib/models/dfm/torch_model.py b/anomalib/models/dfm/torch_model.py index ec47613b01..05a61c37c8 100644 --- a/anomalib/models/dfm/torch_model.py +++ b/anomalib/models/dfm/torch_model.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math -from typing import Tuple import torch import torch.nn.functional as F @@ -76,7 +77,7 @@ class DFMModel(nn.Module): Args: backbone (str): Pre-trained model backbone. layer (str): Layer from which to extract features. - input_size (Tuple[int, int]): Input size for the model. + input_size (tuple[int, int]): Input size for the model. pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. pooling_kernel_size (int, optional): Kernel size to pool features extracted from the CNN. n_comps (float, optional): Ratio from which number of components for PCA are calculated. Defaults to 0.97. @@ -88,7 +89,7 @@ def __init__( self, backbone: str, layer: str, - input_size: Tuple[int, int], + input_size: tuple[int, int], pre_trained: bool = True, pooling_kernel_size: int = 4, n_comps: float = 0.97, @@ -102,7 +103,7 @@ def __init__( self.gaussian_model = SingleClassGaussian() self.score_type = score_type self.layer = layer - self.input_size = tuple(input_size) + self.input_size = input_size if isinstance(input_size, tuple) else tuple(input_size) self.feature_extractor = FeatureExtractor( backbone=self.backbone, pre_trained=pre_trained, layers=[layer] ).eval() diff --git a/anomalib/models/draem/lightning_model.py b/anomalib/models/draem/lightning_model.py index 63933c8255..ac7f55bd2c 100644 --- a/anomalib/models/draem/lightning_model.py +++ b/anomalib/models/draem/lightning_model.py @@ -6,12 +6,15 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Callable, Dict, Optional, Union +from __future__ import annotations + +from typing import Callable import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.callbacks import EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor, nn from anomalib.data.utils import Augmenter @@ -27,13 +30,13 @@ class Draem(AnomalyModule): """DRÆM: A discriminatively trained reconstruction embedding for surface anomaly detection. Args: - anomaly_source_path (Optional[str]): Path to folder that contains the anomaly source images. Random noise will + anomaly_source_path (str | None): Path to folder that contains the anomaly source images. Random noise will be used if left empty. """ def __init__( - self, enable_sspcab: bool = False, sspcab_lambda: float = 0.1, anomaly_source_path: Optional[str] = None - ): + self, enable_sspcab: bool = False, sspcab_lambda: float = 0.1, anomaly_source_path: str | None = None + ) -> None: super().__init__() self.augmenter = Augmenter(anomaly_source_path) @@ -42,12 +45,12 @@ def __init__( self.sspcab = enable_sspcab if self.sspcab: - self.sspcab_activations: Dict = {} + self.sspcab_activations: dict = {} self.setup_sspcab() self.sspcab_loss = nn.MSELoss() self.sspcab_lambda = sspcab_lambda - def setup_sspcab(self): + def setup_sspcab(self) -> None: """Prepare the model for the SSPCAB training step by adding forward hooks for the SSPCAB layer activations.""" def get_activation(name: str) -> Callable: @@ -57,7 +60,7 @@ def get_activation(name: str) -> Callable: name (str): Identifier for the retrieved activations. """ - def hook(_, __, output: Tensor): + def hook(_, __, output: Tensor) -> None: """Hook for retrieving the activations.""" self.sspcab_activations[name] = output @@ -66,14 +69,14 @@ def hook(_, __, output: Tensor): self.model.reconstructive_subnetwork.encoder.mp4.register_forward_hook(get_activation("input")) self.model.reconstructive_subnetwork.encoder.block5.register_forward_hook(get_activation("output")) - def training_step(self, batch, _): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training Step of DRAEM. Feeds the original image and the simulated anomaly image through the network and computes the training loss. Args: - batch (Dict[str, Any]): Batch containing image filename, image, label and mask + batch (dict[str, str | Tensor]): Batch containing image filename, image, label and mask Returns: Loss dictionary @@ -94,11 +97,11 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ self.log("train_loss", loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": loss} - def validation_step(self, batch, _): + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation step of DRAEM. The Softmax predictions of the anomalous class are used as anomaly map. Args: - batch: Batch of input images + batch (dict[str, str | Tensor]): Batch of input images Returns: Dictionary to which predicted anomaly maps have been added. @@ -112,19 +115,19 @@ class DraemLightning(Draem): """DRÆM: A discriminatively trained reconstruction embedding for surface anomaly detection. Args: - hparams (Union[DictConfig, ListConfig]): Model parameters + hparams (DictConfig | ListConfig): Model parameters """ - def __init__(self, hparams: Union[DictConfig, ListConfig]): + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( enable_sspcab=hparams.model.enable_sspcab, sspcab_lambda=hparams.model.sspcab_lambda, anomaly_source_path=hparams.model.anomaly_source_path, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[EarlyStopping]: """Configure model-specific callbacks. Note: @@ -140,6 +143,6 @@ def configure_callbacks(self): ) return [early_stopping] - def configure_optimizers(self): # pylint: disable=arguments-differ + def configure_optimizers(self) -> torch.optim.Optimizer: """Configure the Adam optimizer.""" return torch.optim.Adam(params=self.model.parameters(), lr=self.hparams.model.lr) diff --git a/anomalib/models/draem/loss.py b/anomalib/models/draem/loss.py index 1e05bfa073..f035af769f 100644 --- a/anomalib/models/draem/loss.py +++ b/anomalib/models/draem/loss.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 from kornia.losses import FocalLoss, SSIMLoss -from torch import nn +from torch import Tensor, nn class DraemLoss(nn.Module): @@ -14,14 +14,14 @@ class DraemLoss(nn.Module): image, and the Structural Similarity loss between the predicted and GT anomaly masks. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.l2_loss = nn.modules.loss.MSELoss() self.focal_loss = FocalLoss(alpha=1, reduction="mean") self.ssim_loss = SSIMLoss(window_size=11) - def forward(self, input_image, reconstruction, anomaly_mask, prediction): + def forward(self, input_image: Tensor, reconstruction: Tensor, anomaly_mask: Tensor, prediction: Tensor) -> Tensor: """Compute the loss over a batch for the DRAEM model.""" l2_loss_val = self.l2_loss(reconstruction, input_image) focal_loss_val = self.focal_loss(prediction, anomaly_mask.squeeze(1).long()) diff --git a/anomalib/models/draem/torch_model.py b/anomalib/models/draem/torch_model.py index e8d92bcb42..6d74953a33 100644 --- a/anomalib/models/draem/torch_model.py +++ b/anomalib/models/draem/torch_model.py @@ -9,7 +9,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple, Union +from __future__ import annotations import torch from torch import Tensor, nn @@ -20,12 +20,12 @@ class DraemModel(nn.Module): """DRAEM PyTorch model consisting of the reconstructive and discriminative sub networks.""" - def __init__(self, sspcab: bool = False): + def __init__(self, sspcab: bool = False) -> None: super().__init__() self.reconstructive_subnetwork = ReconstructiveSubNetwork(sspcab=sspcab) self.discriminative_subnetwork = DiscriminativeSubNetwork(in_channels=6, out_channels=2) - def forward(self, batch: Tensor) -> Union[Tensor, Tuple[Tensor, Tensor]]: + def forward(self, batch: Tensor) -> Tensor | tuple[Tensor, Tensor]: """Compute the reconstruction and anomaly mask from an input image. Args: @@ -52,7 +52,7 @@ class ReconstructiveSubNetwork(nn.Module): base_width (int): Base dimensionality of the layers of the autoencoder. """ - def __init__(self, in_channels: int = 3, out_channels: int = 3, base_width=128, sspcab: bool = False): + def __init__(self, in_channels: int = 3, out_channels: int = 3, base_width=128, sspcab: bool = False) -> None: super().__init__() self.encoder = EncoderReconstructive(in_channels, base_width, sspcab=sspcab) self.decoder = DecoderReconstructive(base_width, out_channels=out_channels) @@ -80,7 +80,7 @@ class DiscriminativeSubNetwork(nn.Module): base_width (int): Base dimensionality of the layers of the autoencoder. """ - def __init__(self, in_channels: int = 3, out_channels: int = 3, base_width: int = 64): + def __init__(self, in_channels: int = 3, out_channels: int = 3, base_width: int = 64) -> None: super().__init__() self.encoder_segment = EncoderDiscriminative(in_channels, base_width) self.decoder_segment = DecoderDiscriminative(base_width, out_channels=out_channels) @@ -108,7 +108,7 @@ class EncoderDiscriminative(nn.Module): base_width (int): Base dimensionality of the layers of the autoencoder. """ - def __init__(self, in_channels: int, base_width: int): + def __init__(self, in_channels: int, base_width: int) -> None: super().__init__() self.block1 = nn.Sequential( nn.Conv2d(in_channels, base_width, kernel_size=3, padding=1), @@ -165,7 +165,7 @@ def __init__(self, in_channels: int, base_width: int): nn.ReLU(inplace=True), ) - def forward(self, batch: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: + def forward(self, batch: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: """Convert the inputs to the salient space by running them through the encoder network. Args: @@ -197,7 +197,7 @@ class DecoderDiscriminative(nn.Module): out_channels (int): Number of output channels. """ - def __init__(self, base_width: int, out_channels: int = 1): + def __init__(self, base_width: int, out_channels: int = 1) -> None: super().__init__() self.up_b = nn.Sequential( @@ -323,7 +323,7 @@ class EncoderReconstructive(nn.Module): base_width (int): Base dimensionality of the layers of the autoencoder. """ - def __init__(self, in_channels: int, base_width: int, sspcab: bool = False): + def __init__(self, in_channels: int, base_width: int, sspcab: bool = False) -> None: super().__init__() self.block1 = nn.Sequential( nn.Conv2d(in_channels, base_width, kernel_size=3, padding=1), @@ -402,7 +402,7 @@ class DecoderReconstructive(nn.Module): out_channels (int): Number of output channels. """ - def __init__(self, base_width: int, out_channels: int = 1): + def __init__(self, base_width: int, out_channels: int = 1) -> None: super().__init__() self.up1 = nn.Sequential( diff --git a/anomalib/models/fastflow/anomaly_map.py b/anomalib/models/fastflow/anomaly_map.py index ea1ecdabd1..12ee734a0d 100644 --- a/anomalib/models/fastflow/anomaly_map.py +++ b/anomalib/models/fastflow/anomaly_map.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -14,11 +14,11 @@ class AnomalyMapGenerator(nn.Module): """Generate Anomaly Heatmap.""" - def __init__(self, input_size: Union[ListConfig, Tuple]): + def __init__(self, input_size: ListConfig | tuple) -> None: super().__init__() self.input_size = input_size if isinstance(input_size, tuple) else tuple(input_size) - def forward(self, hidden_variables: List[Tensor]) -> Tensor: + def forward(self, hidden_variables: list[Tensor]) -> Tensor: """Generate Anomaly Heatmap. This implementation generates the heatmap based on the flow maps @@ -27,12 +27,12 @@ def forward(self, hidden_variables: List[Tensor]) -> Tensor: map. Args: - hidden_variables (List[Tensor]): List of hidden variables from each NF FastFlow block. + hidden_variables (list[Tensor]): List of hidden variables from each NF FastFlow block. Returns: Tensor: Anomaly Map. """ - flow_maps: List[Tensor] = [] + flow_maps: list[Tensor] = [] for hidden_variable in hidden_variables: log_prob = -torch.mean(hidden_variable**2, dim=1, keepdim=True) * 0.5 prob = torch.exp(log_prob) diff --git a/anomalib/models/fastflow/lightning_model.py b/anomalib/models/fastflow/lightning_model.py index 9ae64e4cfb..7b80a1dde7 100644 --- a/anomalib/models/fastflow/lightning_model.py +++ b/anomalib/models/fastflow/lightning_model.py @@ -3,13 +3,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple, Union +from __future__ import annotations import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.callbacks import EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY -from torch import optim +from pytorch_lightning.utilities.types import STEP_OUTPUT +from torch import Tensor, optim from anomalib.models.components import AnomalyModule from anomalib.models.fastflow.loss import FastflowLoss @@ -21,7 +22,7 @@ class Fastflow(AnomalyModule): """PL Lightning Module for the FastFlow algorithm. Args: - input_size (Tuple[int, int]): Model input size. + input_size (tuple[int, int]): Model input size. backbone (str): Backbone CNN network pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. flow_steps (int, optional): Flow steps. @@ -31,13 +32,13 @@ class Fastflow(AnomalyModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, pre_trained: bool = True, flow_steps: int = 8, conv3x3_only: bool = False, hidden_ratio: float = 1.0, - ): + ) -> None: super().__init__() self.model = FastflowModel( @@ -50,11 +51,11 @@ def __init__( ) self.loss = FastflowLoss() - def training_step(self, batch, _): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Forward-pass input and return the loss. Args: - batch (Tensor): Input batch + batch (batch: dict[str, str | Tensor]): Input batch _batch_idx: Index of the batch. Returns: @@ -65,15 +66,14 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ self.log("train_loss", loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": loss} - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Forward-pass the input and return the anomaly map. Args: - batch (Tensor): Input batch - _batch_idx: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: - dict: batch dictionary containing anomaly-maps. + STEP_OUTPUT | None: batch dictionary containing anomaly-maps. """ anomaly_maps = self.model(batch["image"]) batch["anomaly_maps"] = anomaly_maps @@ -84,10 +84,10 @@ class FastflowLightning(Fastflow): """PL Lightning Module for the FastFlow algorithm. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, @@ -96,10 +96,10 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: conv3x3_only=hparams.model.conv3x3_only, hidden_ratio=hparams.model.hidden_ratio, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[EarlyStopping]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/fastflow/loss.py b/anomalib/models/fastflow/loss.py index 608b0cfc87..848f86980f 100644 --- a/anomalib/models/fastflow/loss.py +++ b/anomalib/models/fastflow/loss.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List +from __future__ import annotations import torch from torch import Tensor, nn @@ -12,12 +12,12 @@ class FastflowLoss(nn.Module): """FastFlow Loss.""" - def forward(self, hidden_variables: List[Tensor], jacobians: List[Tensor]) -> Tensor: + def forward(self, hidden_variables: list[Tensor], jacobians: list[Tensor]) -> Tensor: """Calculate the Fastflow loss. Args: - hidden_variables (List[Tensor]): Hidden variables from the fastflow model. f: X -> Z - jacobians (List[Tensor]): Log of the jacobian determinants from the fastflow model. + hidden_variables (list[Tensor]): Hidden variables from the fastflow model. f: X -> Z + jacobians (list[Tensor]): Log of the jacobian determinants from the fastflow model. Returns: Tensor: Fastflow loss computed based on the hidden variables and the log of the Jacobians. diff --git a/anomalib/models/fastflow/torch_model.py b/anomalib/models/fastflow/torch_model.py index 3a96a46377..3720194686 100644 --- a/anomalib/models/fastflow/torch_model.py +++ b/anomalib/models/fastflow/torch_model.py @@ -9,7 +9,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Callable, List, Tuple, Union +from __future__ import annotations + +from typing import Callable import timm import torch @@ -49,7 +51,7 @@ def subnet_conv(in_channels: int, out_channels: int) -> nn.Sequential: def create_fast_flow_block( - input_dimensions: List[int], + input_dimensions: list[int], conv3x3_only: bool, hidden_ratio: float, flow_steps: int, @@ -61,7 +63,7 @@ def create_fast_flow_block( Figure 2 and Section 3.3 in the paper. Args: - input_dimensions (List[int]): Input dimensions (Channel, Height, Width) + input_dimensions (list[int]): Input dimensions (Channel, Height, Width) conv3x3_only (bool): Boolean whether to use conv3x3 only or conv3x3 and conv1x1. hidden_ratio (float): Ratio for the hidden layer channels. flow_steps (int): Flow steps. @@ -91,7 +93,7 @@ class FastflowModel(nn.Module): Unsupervised Anomaly Detection and Localization via 2D Normalizing Flows. Args: - input_size (Tuple[int, int]): Model input size. + input_size (tuple[int, int]): Model input size. backbone (str): Backbone CNN network pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. flow_steps (int, optional): Flow steps. @@ -104,7 +106,7 @@ class FastflowModel(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, pre_trained: bool = True, flow_steps: int = 8, @@ -115,11 +117,11 @@ def __init__( self.input_size = input_size - if backbone in ["cait_m48_448", "deit_base_distilled_patch16_384"]: + if backbone in ("cait_m48_448", "deit_base_distilled_patch16_384"): self.feature_extractor = timm.create_model(backbone, pretrained=pre_trained) channels = [768] scales = [16] - elif backbone in ["resnet18", "wide_resnet50_2"]: + elif backbone in ("resnet18", "wide_resnet50_2"): self.feature_extractor = timm.create_model( backbone, pretrained=pre_trained, @@ -160,19 +162,19 @@ def __init__( ) self.anomaly_map_generator = AnomalyMapGenerator(input_size=input_size) - def forward(self, input_tensor: Tensor) -> Union[Tuple[List[Tensor], List[Tensor]], Tensor]: + def forward(self, input_tensor: Tensor) -> Tensor | list[Tensor] | tuple[list[Tensor]]: """Forward-Pass the input to the FastFlow Model. Args: input_tensor (Tensor): Input tensor. Returns: - Union[Tuple[Tensor, Tensor], Tensor]: During training, return + Tensor | list[Tensor] | tuple[list[Tensor]]: During training, return (hidden_variables, log-of-the-jacobian-determinants). During the validation/test, return the anomaly map. """ - return_val: Union[Tuple[List[Tensor], List[Tensor]], Tensor] + return_val: Tensor | list[Tensor] | tuple[list[Tensor]] self.feature_extractor.eval() if isinstance(self.feature_extractor, VisionTransformer): @@ -185,8 +187,8 @@ def forward(self, input_tensor: Tensor) -> Union[Tuple[List[Tensor], List[Tensor # Compute the hidden variable f: X -> Z and log-likelihood of the jacobian # (See Section 3.3 in the paper.) # NOTE: output variable has z, and jacobian tuple for each fast-flow blocks. - hidden_variables: List[Tensor] = [] - log_jacobians: List[Tensor] = [] + hidden_variables: list[Tensor] = [] + log_jacobians: list[Tensor] = [] for fast_flow_block, feature in zip(self.fast_flow_blocks, features): hidden_variable, log_jacobian = fast_flow_block(feature) hidden_variables.append(hidden_variable) @@ -199,27 +201,27 @@ def forward(self, input_tensor: Tensor) -> Union[Tuple[List[Tensor], List[Tensor return return_val - def _get_cnn_features(self, input_tensor: Tensor) -> List[Tensor]: + def _get_cnn_features(self, input_tensor: Tensor) -> list[Tensor]: """Get CNN-based features. Args: input_tensor (Tensor): Input Tensor. Returns: - List[Tensor]: List of features. + list[Tensor]: List of features. """ features = self.feature_extractor(input_tensor) features = [self.norms[i](feature) for i, feature in enumerate(features)] return features - def _get_cait_features(self, input_tensor: Tensor) -> List[Tensor]: + def _get_cait_features(self, input_tensor: Tensor) -> list[Tensor]: """Get Class-Attention-Image-Transformers (CaiT) features. Args: input_tensor (Tensor): Input Tensor. Returns: - List[Tensor]: List of features. + list[Tensor]: List of features. """ feature = self.feature_extractor.patch_embed(input_tensor) feature = feature + self.feature_extractor.pos_embed @@ -233,14 +235,14 @@ def _get_cait_features(self, input_tensor: Tensor) -> List[Tensor]: features = [feature] return features - def _get_vit_features(self, input_tensor: Tensor) -> List[Tensor]: + def _get_vit_features(self, input_tensor: Tensor) -> list[Tensor]: """Get Vision Transformers (ViT) features. Args: input_tensor (Tensor): Input Tensor. Returns: - List[Tensor]: List of features. + list[Tensor]: List of features. """ feature = self.feature_extractor.patch_embed(input_tensor) cls_token = self.feature_extractor.cls_token.expand(feature.shape[0], -1, -1) diff --git a/anomalib/models/ganomaly/lightning_model.py b/anomalib/models/ganomaly/lightning_model.py index e4e7a84b1d..0034e8ee5e 100644 --- a/anomalib/models/ganomaly/lightning_model.py +++ b/anomalib/models/ganomaly/lightning_model.py @@ -6,13 +6,15 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Dict, List, Tuple, Union import torch from omegaconf import DictConfig, ListConfig -from pytorch_lightning.callbacks import EarlyStopping +from pytorch_lightning.callbacks import Callback, EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import EPOCH_OUTPUT, STEP_OUTPUT from torch import Tensor, optim from anomalib.models.components import AnomalyModule @@ -29,7 +31,7 @@ class Ganomaly(AnomalyModule): Args: batch_size (int): Batch size. - input_size (Tuple[int,int]): Input dimension. + input_size (tuple[int, int]): Input dimension. n_features (int): Number of features layers in the CNNs. latent_vec_size (int): Size of autoencoder latent vector. extra_layers (int, optional): Number of extra layers for encoder/decoder. Defaults to 0. @@ -42,7 +44,7 @@ class Ganomaly(AnomalyModule): def __init__( self, batch_size: int, - input_size: Tuple[int, int], + input_size: tuple[int, int], n_features: int, latent_vec_size: int, extra_layers: int = 0, @@ -53,7 +55,7 @@ def __init__( lr: float = 0.0002, beta1: float = 0.5, beta2: float = 0.999, - ): + ) -> None: super().__init__() self.model: GanomalyModel = GanomalyModel( @@ -80,12 +82,12 @@ def __init__( self.beta1 = beta1 self.beta2 = beta2 - def _reset_min_max(self): + def _reset_min_max(self) -> None: """Resets min_max scores.""" self.min_scores = torch.tensor(float("inf"), dtype=torch.float32) # pylint: disable=not-callable self.max_scores = torch.tensor(float("-inf"), dtype=torch.float32) # pylint: disable=not-callable - def configure_optimizers(self) -> List[optim.Optimizer]: + def configure_optimizers(self) -> list[optim.Optimizer]: """Configures optimizers for each decoder. Note: @@ -109,16 +111,21 @@ def configure_optimizers(self) -> List[optim.Optimizer]: ) return [optimizer_d, optimizer_g] - def training_step(self, batch, _, optimizer_idx): # pylint: disable=arguments-differ + def training_step( + self, batch: dict[str, str | Tensor], batch_idx: int, optimizer_idx: int + ) -> STEP_OUTPUT: # pylint: disable=arguments-differ """Training step. Args: - batch (Dict): Input batch containing images. + batch (dict[str, str | Tensor]): Input batch containing images. + batch_idx (int): Batch index. optimizer_idx (int): Optimizer which is being called for current training step. Returns: - Dict[str, Tensor]: Loss + STEP_OUTPUT: Loss """ + del batch_idx # `batch_idx` variables is not used. + # forward pass padded, fake, latent_i, latent_o = self.model(batch["image"]) pred_real, _ = self.model.discriminator(padded) @@ -138,21 +145,21 @@ def on_validation_start(self) -> None: self._reset_min_max() return super().on_validation_start() - def validation_step(self, batch, _) -> Dict[str, Tensor]: # type: ignore # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Update min and max scores from the current step. Args: - batch (Dict[str, Tensor]): Predicted difference between z and z_hat. + batch (dict[str, str | Tensor]): Predicted difference between z and z_hat. Returns: - Dict[str, Tensor]: batch + (STEP_OUTPUT): Output predictions. """ batch["pred_scores"] = self.model(batch["image"]) self.max_scores = max(self.max_scores, torch.max(batch["pred_scores"])) self.min_scores = min(self.min_scores, torch.min(batch["pred_scores"])) return batch - def validation_epoch_end(self, outputs): + def validation_epoch_end(self, outputs: EPOCH_OUTPUT) -> EPOCH_OUTPUT: """Normalize outputs based on min/max values.""" logger.info("Normalizing validation outputs based on min/max values.") for prediction in outputs: @@ -165,14 +172,14 @@ def on_test_start(self) -> None: self._reset_min_max() return super().on_test_start() - def test_step(self, batch, _): + def test_step(self, batch: dict[str, str | Tensor], batch_idx: int, *args, **kwargs) -> STEP_OUTPUT: """Update min and max scores from the current step.""" - super().test_step(batch, _) + super().test_step(batch, batch_idx) self.max_scores = max(self.max_scores, torch.max(batch["pred_scores"])) self.min_scores = min(self.min_scores, torch.min(batch["pred_scores"])) return batch - def test_epoch_end(self, outputs): + def test_epoch_end(self, outputs: EPOCH_OUTPUT) -> EPOCH_OUTPUT: """Normalize outputs based on min/max values.""" logger.info("Normalizing test outputs based on min/max values.") for prediction in outputs: @@ -199,10 +206,10 @@ class GanomalyLightning(Ganomaly): """PL Lightning Module for the GANomaly Algorithm. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( batch_size=hparams.dataset.train_batch_size, @@ -218,10 +225,10 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: beta1=hparams.model.beta1, beta2=hparams.model.beta2, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[Callback]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/ganomaly/loss.py b/anomalib/models/ganomaly/loss.py index 77a6e19fba..f432c62aa6 100644 --- a/anomalib/models/ganomaly/loss.py +++ b/anomalib/models/ganomaly/loss.py @@ -3,6 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import torch from torch import Tensor, nn @@ -16,7 +18,7 @@ class GeneratorLoss(nn.Module): wenc (int, optional): Latent vector encoder weight. Defaults to 1. """ - def __init__(self, wadv=1, wcon=50, wenc=1): + def __init__(self, wadv=1, wcon=50, wenc=1) -> None: super().__init__() self.loss_enc = nn.SmoothL1Loss() @@ -54,13 +56,13 @@ def forward( class DiscriminatorLoss(nn.Module): """Discriminator loss for the GANomaly model.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.loss_bce = nn.BCELoss() - def forward(self, pred_real, pred_fake): - """Compye the loss for a predicted batch. + def forward(self, pred_real: Tensor, pred_fake: Tensor) -> Tensor: + """Compute the loss for a predicted batch. Args: pred_real (Tensor): Discriminator predictions for the real image. diff --git a/anomalib/models/ganomaly/torch_model.py b/anomalib/models/ganomaly/torch_model.py index fbf67fc1b5..bc707e9ab7 100644 --- a/anomalib/models/ganomaly/torch_model.py +++ b/anomalib/models/ganomaly/torch_model.py @@ -9,8 +9,9 @@ # Copyright (C) 2020-2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math -from typing import Tuple, Union import torch from torch import Tensor, nn @@ -22,7 +23,7 @@ class Encoder(nn.Module): """Encoder Network. Args: - input_size (Tuple[int, int]): Size of input image + input_size (tuple[int, int]): Size of input image latent_vec_size (int): Size of latent vector z num_input_channels (int): Number of input channels in the image n_features (int): Number of features per convolution layer @@ -32,13 +33,13 @@ class Encoder(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], latent_vec_size: int, num_input_channels: int, n_features: int, extra_layers: int = 0, add_final_conv_layer: bool = True, - ): + ) -> None: super().__init__() self.input_layers = nn.Sequential() @@ -85,7 +86,7 @@ def __init__( bias=False, ) - def forward(self, input_tensor: Tensor): + def forward(self, input_tensor: Tensor) -> Tensor: """Return latent vectors.""" output = self.input_layers(input_tensor) @@ -101,7 +102,7 @@ class Decoder(nn.Module): """Decoder Network. Args: - input_size (Tuple[int, int]): Size of input image + input_size (tuple[int, int]): Size of input image latent_vec_size (int): Size of latent vector z num_input_channels (int): Number of input channels in the image n_features (int): Number of features per convolution layer @@ -111,12 +112,12 @@ class Decoder(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], latent_vec_size: int, num_input_channels: int, n_features: int, extra_layers: int = 0, - ): + ) -> None: super().__init__() self.latent_input = nn.Sequential() @@ -191,7 +192,7 @@ def __init__( ) self.final_layers.add_module(f"final-{num_input_channels}-tanh", nn.Tanh()) - def forward(self, input_tensor): + def forward(self, input_tensor: Tensor) -> Tensor: """Return generated image.""" output = self.latent_input(input_tensor) output = self.inverse_pyramid(output) @@ -206,13 +207,15 @@ class Discriminator(nn.Module): Made of only one encoder layer which takes x and x_hat to produce a score. Args: - input_size (Tuple[int,int]): Input image size. + input_size (tuple[int, int]): Input image size. num_input_channels (int): Number of image channels. n_features (int): Number of feature maps in each convolution layer. extra_layers (int, optional): Add extra intermediate layers. Defaults to 0. """ - def __init__(self, input_size: Tuple[int, int], num_input_channels: int, n_features: int, extra_layers: int = 0): + def __init__( + self, input_size: tuple[int, int], num_input_channels: int, n_features: int, extra_layers: int = 0 + ) -> None: super().__init__() encoder = Encoder(input_size, 1, num_input_channels, n_features, extra_layers) layers = [] @@ -226,7 +229,7 @@ def __init__(self, input_size: Tuple[int, int], num_input_channels: int, n_featu self.classifier = nn.Sequential(layers[-1]) self.classifier.add_module("Sigmoid", nn.Sigmoid()) - def forward(self, input_tensor): + def forward(self, input_tensor: Tensor) -> tuple[Tensor, Tensor]: """Return class of object and features.""" features = self.features(input_tensor) classifier = self.classifier(features) @@ -240,7 +243,7 @@ class Generator(nn.Module): Made of an encoder-decoder-encoder architecture. Args: - input_size (Tuple[int,int]): Size of input data. + input_size (tuple[int, int]): Size of input data. latent_vec_size (int): Dimension of latent vector produced between the first encoder-decoder. num_input_channels (int): Number of channels in input image. n_features (int): Number of feature maps in each convolution layer. @@ -250,13 +253,13 @@ class Generator(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], latent_vec_size: int, num_input_channels: int, n_features: int, extra_layers: int = 0, add_final_conv_layer: bool = True, - ): + ) -> None: super().__init__() self.encoder1 = Encoder( input_size, latent_vec_size, num_input_channels, n_features, extra_layers, add_final_conv_layer @@ -266,7 +269,7 @@ def __init__( input_size, latent_vec_size, num_input_channels, n_features, extra_layers, add_final_conv_layer ) - def forward(self, input_tensor): + def forward(self, input_tensor: Tensor) -> tuple[Tensor, Tensor, Tensor]: """Return generated image and the latent vectors.""" latent_i = self.encoder1(input_tensor) gen_image = self.decoder(latent_i) @@ -278,7 +281,7 @@ class GanomalyModel(nn.Module): """Ganomaly Model. Args: - input_size (Tuple[int,int]): Input dimension. + input_size (tuple[int, int]): Input dimension. num_input_channels (int): Number of input channels. n_features (int): Number of features layers in the CNNs. latent_vec_size (int): Size of autoencoder latent vector. @@ -288,7 +291,7 @@ class GanomalyModel(nn.Module): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], num_input_channels: int, n_features: int, latent_vec_size: int, @@ -314,7 +317,7 @@ def __init__( self.weights_init(self.discriminator) @staticmethod - def weights_init(module: nn.Module): + def weights_init(module: nn.Module) -> None: """Initialize DCGAN weights. Args: @@ -327,7 +330,7 @@ def weights_init(module: nn.Module): nn.init.normal_(module.weight.data, 1.0, 0.02) nn.init.constant_(module.bias.data, 0) - def forward(self, batch: Tensor) -> Union[Tuple[Tensor, Tensor, Tensor, Tensor], Tensor]: + def forward(self, batch: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor] | Tensor: """Get scores for batch. Args: diff --git a/anomalib/models/padim/anomaly_map.py b/anomalib/models/padim/anomaly_map.py index b706cbad14..08df61cbce 100644 --- a/anomalib/models/padim/anomaly_map.py +++ b/anomalib/models/padim/anomaly_map.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -17,25 +17,25 @@ class AnomalyMapGenerator(nn.Module): """Generate Anomaly Heatmap. Args: - image_size (Union[ListConfig, Tuple]): Size of the input image. The anomaly map is upsampled to this dimension. + image_size (ListConfig, tuple): Size of the input image. The anomaly map is upsampled to this dimension. sigma (int, optional): Standard deviation for Gaussian Kernel. Defaults to 4. """ - def __init__(self, image_size: Union[ListConfig, Tuple], sigma: int = 4): + def __init__(self, image_size: ListConfig | tuple, sigma: int = 4) -> None: super().__init__() self.image_size = image_size if isinstance(image_size, tuple) else tuple(image_size) kernel_size = 2 * int(4.0 * sigma + 0.5) + 1 self.blur = GaussianBlur2d(kernel_size=(kernel_size, kernel_size), sigma=(sigma, sigma), channels=1) @staticmethod - def compute_distance(embedding: Tensor, stats: List[Tensor]) -> Tensor: + def compute_distance(embedding: Tensor, stats: list[Tensor]) -> Tensor: """Compute anomaly score to the patch in position(i,j) of a test image. Ref: Equation (2), Section III-C of the paper. Args: embedding (Tensor): Embedding Vector - stats (List[Tensor]): Mean and Covariance Matrix of the multivariate Gaussian distribution + stats (list[Tensor]): Mean and Covariance Matrix of the multivariate Gaussian distribution Returns: Anomaly score of a test image via mahalanobis distance. @@ -109,7 +109,7 @@ def compute_anomaly_map(self, embedding: Tensor, mean: Tensor, inv_covariance: T return smoothed_anomaly_map - def forward(self, **kwargs): + def forward(self, **kwargs) -> Tensor: """Returns anomaly_map. Expects `embedding`, `mean` and `covariance` keywords to be passed explicitly. diff --git a/anomalib/models/padim/lightning_model.py b/anomalib/models/padim/lightning_model.py index e9b1e70278..ec1b4f073d 100644 --- a/anomalib/models/padim/lightning_model.py +++ b/anomalib/models/padim/lightning_model.py @@ -6,12 +6,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Optional, Tuple, Union import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -27,8 +29,8 @@ class Padim(AnomalyModule): """PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization. Args: - layers (List[str]): Layers to extract features from the backbone CNN - input_size (Tuple[int, int]): Size of the model input. + layers (list[str]): Layers to extract features from the backbone CNN + input_size (tuple[int, int]): Size of the model input. backbone (str): Backbone CNN network pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. n_features (int, optional): Number of features to retain in the dimension reduction step. @@ -37,12 +39,12 @@ class Padim(AnomalyModule): def __init__( self, - layers: List[str], - input_size: Tuple[int, int], + layers: list[str], + input_size: tuple[int, int], backbone: str, pre_trained: bool = True, - n_features: Optional[int] = None, - ): + n_features: int | None = None, + ) -> None: super().__init__() self.layers = layers @@ -54,19 +56,19 @@ def __init__( n_features=n_features, ).eval() - self.stats: List[Tensor] = [] - self.embeddings: List[Tensor] = [] + self.stats: list[Tensor] = [] + self.embeddings: list[Tensor] = [] @staticmethod - def configure_optimizers(): # pylint: disable=arguments-differ + def configure_optimizers() -> None: # pylint: disable=arguments-differ """PADIM doesn't require optimization, therefore returns no optimizers.""" return None - def training_step(self, batch, _batch_idx): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> None: """Training Step of PADIM. For each batch, hierarchical features are extracted from the CNN. Args: - batch (Dict[str, Any]): Batch containing image filename, image, label and mask + batch (dict[str, str | Tensor]): Batch containing image filename, image, label and mask _batch_idx: Index of the batch. Returns: @@ -92,14 +94,13 @@ def on_validation_start(self) -> None: logger.info("Fitting a Gaussian to the embedding collected from the training set.") self.stats = self.model.gaussian.fit(embeddings) - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of PADIM. Similar to the training step, hierarchical features are extracted from the CNN for each batch. Args: - batch: Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: Dictionary containing images, features, true labels and masks. @@ -114,10 +115,10 @@ class PadimLightning(Padim): """PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]): + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, layers=hparams.model.layers, @@ -125,5 +126,5 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]): pre_trained=hparams.model.pre_trained, n_features=hparams.model.n_features if "n_features" in hparams.model else None, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) diff --git a/anomalib/models/padim/torch_model.py b/anomalib/models/padim/torch_model.py index 98cdada3c2..702a788bfb 100644 --- a/anomalib/models/padim/torch_model.py +++ b/anomalib/models/padim/torch_model.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from random import sample -from typing import Dict, List, Optional, Tuple import torch import torch.nn.functional as F @@ -23,15 +24,15 @@ def _deduce_dims( - feature_extractor: FeatureExtractor, input_size: Tuple[int, int], layers: List[str] -) -> Tuple[int, int]: + feature_extractor: FeatureExtractor, input_size: tuple[int, int], layers: list[str] +) -> tuple[int, int]: """Run a dry run to deduce the dimensions of the extracted features. Important: `layers` is assumed to be ordered and the first (layers[0]) is assumed to be the layer with largest resolution. Returns: - Tuple[int, int]: Dimensions of the extracted features: (n_dims_original, n_patches) + tuple[int, int]: Dimensions of the extracted features: (n_dims_original, n_patches) """ dimensions_mapping = dryrun_find_featuremap_dims(feature_extractor, input_size, layers) @@ -49,8 +50,8 @@ class PadimModel(nn.Module): """Padim Module. Args: - input_size (Tuple[int, int]): Input size for the model. - layers (List[str]): Layers used for feature extraction + input_size (tuple[int, int]): Input size for the model. + layers (list[str]): Layers used for feature extraction backbone (str, optional): Pre-trained model backbone. Defaults to "resnet18". pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. n_features (int, optional): Number of features to retain in the dimension reduction step. @@ -59,14 +60,14 @@ class PadimModel(nn.Module): def __init__( self, - input_size: Tuple[int, int], - layers: List[str], + input_size: tuple[int, int], + layers: list[str], backbone: str = "resnet18", pre_trained: bool = True, - n_features: Optional[int] = None, - ): + n_features: int | None = None, + ) -> None: super().__init__() - self.tiler: Optional[Tiler] = None + self.tiler: Tiler | None = None self.backbone = backbone self.layers = layers @@ -139,11 +140,11 @@ def forward(self, input_tensor: Tensor) -> Tensor: ) return output - def generate_embedding(self, features: Dict[str, Tensor]) -> Tensor: + def generate_embedding(self, features: dict[str, Tensor]) -> Tensor: """Generate embedding from hierarchical feature map. Args: - features (Dict[str, Tensor]): Hierarchical feature map from a CNN (ResNet18 or WideResnet) + features (dict[str, Tensor]): Hierarchical feature map from a CNN (ResNet18 or WideResnet) Returns: Embedding vector diff --git a/anomalib/models/patchcore/anomaly_map.py b/anomalib/models/patchcore/anomaly_map.py index 7b29f83ef3..18b5238582 100644 --- a/anomalib/models/patchcore/anomaly_map.py +++ b/anomalib/models/patchcore/anomaly_map.py @@ -3,9 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple, Union +from __future__ import annotations -import torch import torch.nn.functional as F from omegaconf import ListConfig from torch import Tensor, nn @@ -18,7 +17,7 @@ class AnomalyMapGenerator(nn.Module): def __init__( self, - input_size: Union[ListConfig, Tuple], + input_size: ListConfig | tuple, sigma: int = 4, ) -> None: super().__init__() @@ -26,14 +25,14 @@ def __init__( kernel_size = 2 * int(4.0 * sigma + 0.5) + 1 self.blur = GaussianBlur2d(kernel_size=(kernel_size, kernel_size), sigma=(sigma, sigma), channels=1) - def compute_anomaly_map(self, patch_scores: Tensor) -> torch.Tensor: + def compute_anomaly_map(self, patch_scores: Tensor) -> Tensor: """Pixel Level Anomaly Heatmap. Args: patch_scores (Tensor): Patch-level anomaly scores Returns: - torch.Tensor: Map of the pixel-level anomaly scores + Tensor: Map of the pixel-level anomaly scores """ anomaly_map = F.interpolate(patch_scores, size=(self.input_size[0], self.input_size[1])) anomaly_map = self.blur(anomaly_map) diff --git a/anomalib/models/patchcore/lightning_model.py b/anomalib/models/patchcore/lightning_model.py index 5a5583d42b..5e34c301e4 100644 --- a/anomalib/models/patchcore/lightning_model.py +++ b/anomalib/models/patchcore/lightning_model.py @@ -6,12 +6,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Tuple, Union import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -25,9 +27,9 @@ class Patchcore(AnomalyModule): """PatchcoreLightning Module to train PatchCore algorithm. Args: - input_size (Tuple[int, int]): Size of the model input. + input_size (tuple[int, int]): Size of the model input. backbone (str): Backbone CNN network - layers (List[str]): Layers to extract features from the backbone CNN + layers (list[str]): Layers to extract features from the backbone CNN pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. coreset_sampling_ratio (float, optional): Coreset sampling ratio to subsample embedding. Defaults to 0.1. @@ -36,9 +38,9 @@ class Patchcore(AnomalyModule): def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, - layers: List[str], + layers: list[str], pre_trained: bool = True, coreset_sampling_ratio: float = 0.1, num_neighbors: int = 9, @@ -53,7 +55,7 @@ def __init__( num_neighbors=num_neighbors, ) self.coreset_sampling_ratio = coreset_sampling_ratio - self.embeddings: List[Tensor] = [] + self.embeddings: list[Tensor] = [] def configure_optimizers(self) -> None: """Configure optimizers. @@ -63,15 +65,14 @@ def configure_optimizers(self) -> None: """ return None - def training_step(self, batch, _batch_idx): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> None: """Generate feature embedding of the batch. Args: - batch (Dict[str, Any]): Batch containing image filename, image, label and mask - _batch_idx (int): Batch Index + batch (dict[str, str | Tensor]): Batch containing image filename, image, label and mask Returns: - Dict[str, np.ndarray]: Embedding Vector + dict[str, np.ndarray]: Embedding Vector """ self.model.feature_extractor.eval() embedding = self.model(batch["image"]) @@ -93,16 +94,15 @@ def on_validation_start(self) -> None: logger.info("Applying core-set subsampling to get the embedding.") self.model.subsample_embedding(embeddings, self.coreset_sampling_ratio) - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Get batch of anomaly maps from input image batch. Args: - batch (Dict[str, Any]): Batch containing image filename, - image, label and mask - _ (int): Batch Index + batch (dict[str, str | Tensor]): Batch containing image filename, + image, label and mask Returns: - Dict[str, Any]: Image filenames, test images, GT and predicted label/masks + dict[str, Any]: Image filenames, test images, GT and predicted label/masks """ anomaly_maps, anomaly_score = self.model(batch["image"]) @@ -116,7 +116,7 @@ class PatchcoreLightning(Patchcore): """PatchcoreLightning Module to train PatchCore algorithm. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ def __init__(self, hparams) -> None: @@ -128,5 +128,5 @@ def __init__(self, hparams) -> None: coreset_sampling_ratio=hparams.model.coreset_sampling_ratio, num_neighbors=hparams.model.num_neighbors, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) diff --git a/anomalib/models/patchcore/torch_model.py b/anomalib/models/patchcore/torch_model.py index 819fd83945..7f4f11f5fc 100644 --- a/anomalib/models/patchcore/torch_model.py +++ b/anomalib/models/patchcore/torch_model.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List, Optional, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -23,14 +23,14 @@ class PatchcoreModel(DynamicBufferModule, nn.Module): def __init__( self, - input_size: Tuple[int, int], - layers: List[str], + input_size: tuple[int, int], + layers: list[str], backbone: str = "wide_resnet50_2", pre_trained: bool = True, num_neighbors: int = 9, ) -> None: super().__init__() - self.tiler: Optional[Tiler] = None + self.tiler: Tiler | None = None self.backbone = backbone self.layers = layers @@ -44,7 +44,7 @@ def __init__( self.register_buffer("memory_bank", Tensor()) self.memory_bank: Tensor - def forward(self, input_tensor: Tensor) -> Union[Tensor, Tuple[Tensor, Tensor]]: + def forward(self, input_tensor: Tensor) -> Tensor | tuple[Tensor, Tensor]: """Return Embedding during training, or a tuple of anomaly map and anomaly score during testing. Steps performed: @@ -56,7 +56,7 @@ def forward(self, input_tensor: Tensor) -> Union[Tensor, Tuple[Tensor, Tensor]]: input_tensor (Tensor): Input tensor Returns: - Union[Tensor, Tuple[Tensor, Tensor]]: Embedding for training, + Tensor | tuple[Tensor, Tensor]: Embedding for training, anomaly map and anomaly score for testing. """ if self.tiler: @@ -93,12 +93,12 @@ def forward(self, input_tensor: Tensor) -> Union[Tensor, Tuple[Tensor, Tensor]]: return output - def generate_embedding(self, features: Dict[str, Tensor]) -> Tensor: + def generate_embedding(self, features: dict[str, Tensor]) -> Tensor: """Generate embedding from hierarchical feature map. Args: features: Hierarchical feature map from a CNN (ResNet18 or WideResnet) - features: Dict[str:Tensor]: + features: dict[str:Tensor]: Returns: Embedding vector @@ -142,7 +142,7 @@ def subsample_embedding(self, embedding: Tensor, sampling_ratio: float) -> None: coreset = sampler.sample_coreset() self.memory_bank = coreset - def nearest_neighbors(self, embedding: Tensor, n_neighbors: int) -> Tuple[Tensor, Tensor]: + def nearest_neighbors(self, embedding: Tensor, n_neighbors: int) -> tuple[Tensor, Tensor]: """Nearest Neighbours using brute force method and euclidean norm. Args: diff --git a/anomalib/models/reverse_distillation/anomaly_map.py b/anomalib/models/reverse_distillation/anomaly_map.py index c245981cba..52e2778feb 100644 --- a/anomalib/models/reverse_distillation/anomaly_map.py +++ b/anomalib/models/reverse_distillation/anomaly_map.py @@ -9,7 +9,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -22,7 +22,7 @@ class AnomalyMapGenerator(nn.Module): """Generate Anomaly Heatmap. Args: - image_size (Union[ListConfig, Tuple]): Size of original image used for upscaling the anomaly map. + image_size (ListConfig, tuple): Size of original image used for upscaling the anomaly map. sigma (int): Standard deviation of the gaussian kernel used to smooth anomaly map. mode (str, optional): Operation used to generate anomaly map. Options are `add` and `multiply`. Defaults to "multiply". @@ -31,7 +31,7 @@ class AnomalyMapGenerator(nn.Module): ValueError: In case modes other than multiply and add are passed. """ - def __init__(self, image_size: Union[ListConfig, Tuple], sigma: int = 4, mode: str = "multiply"): + def __init__(self, image_size: ListConfig | tuple, sigma: int = 4, mode: str = "multiply") -> None: super().__init__() self.image_size = image_size if isinstance(image_size, tuple) else tuple(image_size) self.sigma = sigma @@ -41,12 +41,12 @@ def __init__(self, image_size: Union[ListConfig, Tuple], sigma: int = 4, mode: s raise ValueError(f"Found mode {mode}. Only multiply and add are supported.") self.mode = mode - def forward(self, student_features: List[Tensor], teacher_features: List[Tensor]) -> Tensor: + def forward(self, student_features: list[Tensor], teacher_features: list[Tensor]) -> Tensor: """Computes anomaly map given encoder and decoder features. Args: - student_features (List[Tensor]): List of encoder features - teacher_features (List[Tensor]): List of decoder features + student_features (list[Tensor]): List of encoder features + teacher_features (list[Tensor]): List of decoder features Returns: Tensor: Anomaly maps of length batch. diff --git a/anomalib/models/reverse_distillation/components/bottleneck.py b/anomalib/models/reverse_distillation/components/bottleneck.py index 8000473e03..a04f6a8174 100644 --- a/anomalib/models/reverse_distillation/components/bottleneck.py +++ b/anomalib/models/reverse_distillation/components/bottleneck.py @@ -10,7 +10,9 @@ # SPDX-License-Identifier: Apache-2.0 -from typing import Callable, List, Optional, Type, Union +from __future__ import annotations + +from typing import Callable import torch from torch import Tensor, nn @@ -45,17 +47,17 @@ class OCBE(nn.Module): groups (int, optional): Number of blocked connections from input channels to output channels. Defaults to 1. width_per_group (int, optional): Number of layers in each intermediate convolution layer. Defaults to 64. - norm_layer (Optional[Callable[..., nn.Module]], optional): Batch norm layer to use. Defaults to None. + norm_layer (Callable[..., nn.Module] | None, optional): Batch norm layer to use. Defaults to None. """ def __init__( self, - block: Type[Union[Bottleneck, BasicBlock]], + block: Bottleneck | BasicBlock, layers: int, groups: int = 1, width_per_group: int = 64, - norm_layer: Optional[Callable[..., nn.Module]] = None, - ): + norm_layer: Callable[..., nn.Module] | None = None, + ) -> None: super().__init__() if norm_layer is None: norm_layer = nn.BatchNorm2d @@ -88,7 +90,7 @@ def __init__( def _make_layer( self, - block: Type[Union[Bottleneck, BasicBlock]], + block: type[Bottleneck | BasicBlock], planes: int, blocks: int, stride: int = 1, @@ -134,11 +136,11 @@ def _make_layer( return nn.Sequential(*layers) - def forward(self, features: List[Tensor]) -> Tensor: + def forward(self, features: list[Tensor]) -> Tensor: """Forward-pass of Bottleneck layer. Args: - features (List[Tensor]): List of features extracted from the encoder. + features (list[Tensor]): List of features extracted from the encoder. Returns: Tensor: Output of the bottleneck layer diff --git a/anomalib/models/reverse_distillation/components/de_resnet.py b/anomalib/models/reverse_distillation/components/de_resnet.py index 6d0072a766..9baa64e6e8 100644 --- a/anomalib/models/reverse_distillation/components/de_resnet.py +++ b/anomalib/models/reverse_distillation/components/de_resnet.py @@ -9,7 +9,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Callable, List, Optional, Type, Union +from __future__ import annotations + +from typing import Any, Callable from torch import Tensor, nn from torchvision.models.resnet import conv1x1, conv3x3 @@ -22,12 +24,12 @@ class DecoderBasicBlock(nn.Module): inplanes (int): Number of input channels. planes (int): Number of output channels. stride (int, optional): Stride for convolution and de-convolution layers. Defaults to 1. - upsample (Optional[nn.Module], optional): Module used for upsampling output. Defaults to None. + upsample (nn.Module | None, optional): Module used for upsampling output. Defaults to None. groups (int, optional): Number of blocked connections from input channels to output channels. Defaults to 1. base_width (int, optional): Number of layers in each intermediate convolution layer. Defaults to 64. dilation (int, optional): Spacing between kernel elements. Defaults to 1. - norm_layer (Optional[Callable[..., nn.Module]], optional): Batch norm layer to use.Defaults to None. + norm_layer (Callable[..., nn.Module] | None, optional): Batch norm layer to use.Defaults to None. Raises: ValueError: If groups are not equal to 1 and base width is not 64. @@ -41,11 +43,11 @@ def __init__( inplanes: int, planes: int, stride: int = 1, - upsample: Optional[nn.Module] = None, + upsample: nn.Module | None = None, groups: int = 1, base_width: int = 64, dilation: int = 1, - norm_layer: Optional[Callable[..., nn.Module]] = None, + norm_layer: Callable[..., nn.Module] | None = None, ) -> None: super().__init__() if norm_layer is None: @@ -95,12 +97,12 @@ class DecoderBottleneck(nn.Module): inplanes (int): Number of input channels. planes (int): Number of output channels. stride (int, optional): Stride for convolution and de-convolution layers. Defaults to 1. - upsample (Optional[nn.Module], optional): Module used for upsampling output. Defaults to None. + upsample (nn.Module | None, optional): Module used for upsampling output. Defaults to None. groups (int, optional): Number of blocked connections from input channels to output channels. Defaults to 1. base_width (int, optional): Number of layers in each intermediate convolution layer. Defaults to 64. dilation (int, optional): Spacing between kernel elements. Defaults to 1. - norm_layer (Optional[Callable[..., nn.Module]], optional): Batch norm layer to use.Defaults to None. + norm_layer (Callable[..., nn.Module] | None, optional): Batch norm layer to use.Defaults to None. """ expansion: int = 4 @@ -110,11 +112,11 @@ def __init__( inplanes: int, planes: int, stride: int = 1, - upsample: Optional[nn.Module] = None, + upsample: nn.Module | None = None, groups: int = 1, base_width: int = 64, dilation: int = 1, - norm_layer: Optional[Callable[..., nn.Module]] = None, + norm_layer: Callable[..., nn.Module] | None = None, ) -> None: super().__init__() if norm_layer is None: @@ -164,24 +166,24 @@ class ResNet(nn.Module): """ResNet model for decoder. Args: - block (Type[Union[DecoderBasicBlock, DecoderBottleneck]]): Type of block to use in a layer. - layers (List[int]): List to specify number for blocks per layer. + block (Type[DecoderBasicBlock | DecoderBottleneck]): Type of block to use in a layer. + layers (list[int]): List to specify number for blocks per layer. zero_init_residual (bool, optional): If true, initializes the last batch norm in each layer to zero. Defaults to False. groups (int, optional): Number of blocked connections per layer from input channels to output channels. Defaults to 1. width_per_group (int, optional): Number of layers in each intermediate convolution layer.. Defaults to 64. - norm_layer (Optional[Callable[..., nn.Module]], optional): Batch norm layer to use. Defaults to None. + norm_layer (Callable[..., nn.Module] | None, optional): Batch norm layer to use. Defaults to None. """ def __init__( self, - block: Type[Union[DecoderBasicBlock, DecoderBottleneck]], - layers: List[int], + block: type[DecoderBasicBlock | DecoderBottleneck], + layers: list[int], zero_init_residual: bool = False, groups: int = 1, width_per_group: int = 64, - norm_layer: Optional[Callable[..., nn.Module]] = None, + norm_layer: Callable[..., nn.Module] | None = None, ) -> None: super().__init__() if norm_layer is None: @@ -215,7 +217,7 @@ def __init__( def _make_layer( self, - block: Type[Union[DecoderBasicBlock, DecoderBottleneck]], + block: type[DecoderBasicBlock | DecoderBottleneck], planes: int, blocks: int, stride: int = 1, @@ -256,7 +258,7 @@ def _make_layer( return nn.Sequential(*layers) - def forward(self, batch: Tensor) -> List[Tensor]: + def forward(self, batch: Tensor) -> list[Tensor]: """Forward pass for Decoder ResNet. Returns list of features.""" feature_a = self.layer1(batch) # 512*8*8->256*16*16 feature_b = self.layer2(feature_a) # 256*16*16->128*32*32 @@ -265,7 +267,7 @@ def forward(self, batch: Tensor) -> List[Tensor]: return [feature_c, feature_b, feature_a] -def _resnet(block: Type[Union[DecoderBasicBlock, DecoderBottleneck]], layers: List[int], **kwargs: Any) -> ResNet: +def _resnet(block: type[DecoderBasicBlock | DecoderBottleneck], layers: list[int], **kwargs: Any) -> ResNet: model = ResNet(block, layers, **kwargs) return model diff --git a/anomalib/models/reverse_distillation/lightning_model.py b/anomalib/models/reverse_distillation/lightning_model.py index 209b9ff13a..732c27185b 100644 --- a/anomalib/models/reverse_distillation/lightning_model.py +++ b/anomalib/models/reverse_distillation/lightning_model.py @@ -6,11 +6,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List, Tuple, Union +from __future__ import annotations from omegaconf import DictConfig, ListConfig from pytorch_lightning.callbacks import EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor, optim from anomalib.models.components import AnomalyModule @@ -24,23 +25,23 @@ class ReverseDistillation(AnomalyModule): """PL Lightning Module for Reverse Distillation Algorithm. Args: - input_size (Tuple[int, int]): Size of model input + input_size (tuple[int, int]): Size of model input backbone (str): Backbone of CNN network - layers (List[str]): Layers to extract features from the backbone CNN + layers (list[str]): Layers to extract features from the backbone CNN pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. """ def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, - layers: List[str], + layers: list[str], anomaly_map_mode: str, lr: float, beta1: float, beta2: float, pre_trained: bool = True, - ): + ) -> None: super().__init__() self.model = ReverseDistillationModel( backbone=backbone, @@ -56,7 +57,7 @@ def __init__( self.beta1 = beta1 self.beta2 = beta2 - def configure_optimizers(self): + def configure_optimizers(self) -> optim.Adam: """Configures optimizers for decoder and bottleneck. Note: @@ -74,7 +75,7 @@ def configure_optimizers(self): betas=(self.beta1, self.beta2), ) - def training_step(self, batch, _) -> Dict[str, Tensor]: # type: ignore + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training Step of Reverse Distillation Model. Features are extracted from three layers of the Encoder model. These are passed to the bottleneck layer @@ -82,8 +83,7 @@ def training_step(self, batch, _) -> Dict[str, Tensor]: # type: ignore encoder and decoder features. Args: - batch (Tensor): Input batch - _: Index of the batch. + batch (batch: dict[str, str | Tensor]): Input batch Returns: Feature Map @@ -92,15 +92,14 @@ def training_step(self, batch, _) -> Dict[str, Tensor]: # type: ignore self.log("train_loss", loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": loss} - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of Reverse Distillation Model. Similar to the training step, encoder/decoder features are extracted from the CNN for each batch, and anomaly map is computed. Args: - batch (Tensor): Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: Dictionary containing images, anomaly maps, true labels and masks. @@ -114,10 +113,10 @@ class ReverseDistillationLightning(ReverseDistillation): """PL Lightning Module for Reverse Distillation Algorithm. Args: - hparams(Union[DictConfig, ListConfig]): Model parameters + hparams(DictConfig | ListConfig): Model parameters """ - def __init__(self, hparams: Union[DictConfig, ListConfig]): + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, @@ -128,10 +127,10 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]): beta1=hparams.model.beta1, beta2=hparams.model.beta2, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[EarlyStopping]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/reverse_distillation/loss.py b/anomalib/models/reverse_distillation/loss.py index 3fd6d270a1..e58955f175 100644 --- a/anomalib/models/reverse_distillation/loss.py +++ b/anomalib/models/reverse_distillation/loss.py @@ -3,21 +3,21 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List +from __future__ import annotations import torch -from torch import Tensor +from torch import Tensor, nn -class ReverseDistillationLoss: +class ReverseDistillationLoss(nn.Module): """Loss function for Reverse Distillation.""" - def __call__(self, encoder_features: List[Tensor], decoder_features: List[Tensor]) -> Tensor: + def forward(self, encoder_features: list[Tensor], decoder_features: list[Tensor]) -> Tensor: """Computes cosine similarity loss based on features from encoder and decoder. Args: - encoder_features (List[Tensor]): List of features extracted from encoder - decoder_features (List[Tensor]): List of features extracted from decoder + encoder_features (list[Tensor]): List of features extracted from encoder + decoder_features (list[Tensor]): List of features extracted from decoder Returns: Tensor: Cosine similarity loss diff --git a/anomalib/models/reverse_distillation/torch_model.py b/anomalib/models/reverse_distillation/torch_model.py index 312d4fd2ab..576bf56a86 100644 --- a/anomalib/models/reverse_distillation/torch_model.py +++ b/anomalib/models/reverse_distillation/torch_model.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Optional, Tuple, Union +from __future__ import annotations from torch import Tensor, nn @@ -21,8 +21,8 @@ class ReverseDistillationModel(nn.Module): Args: backbone (str): Name of the backbone used for encoder and decoder - input_size (Tuple[int, int]): Size of input image - layers (List[str]): Name of layers from which the features are extracted. + input_size (tuple[int, int]): Size of input image + layers (list[str]): Name of layers from which the features are extracted. anomaly_map_mode (str): Mode used to generate anomaly map. Options are between ``multiply`` and ``add``. pre_trained (bool, optional): Boolean to check whether to use a pre_trained backbone. """ @@ -30,13 +30,13 @@ class ReverseDistillationModel(nn.Module): def __init__( self, backbone: str, - input_size: Tuple[int, int], - layers: List[str], + input_size: tuple[int, int], + layers: list[str], anomaly_map_mode: str, pre_trained: bool = True, - ): + ) -> None: super().__init__() - self.tiler: Optional[Tiler] = None + self.tiler: Tiler | None = None encoder_backbone = backbone self.encoder = FeatureExtractor(backbone=encoder_backbone, pre_trained=pre_trained, layers=layers) @@ -48,9 +48,9 @@ def __init__( else: image_size = input_size - self.anomaly_map_generator = AnomalyMapGenerator(image_size=tuple(image_size), mode=anomaly_map_mode) + self.anomaly_map_generator = AnomalyMapGenerator(image_size=image_size, mode=anomaly_map_mode) - def forward(self, images: Tensor) -> Union[Tensor, Tuple[List[Tensor], List[Tensor]]]: + def forward(self, images: Tensor) -> Tensor | list[Tensor] | tuple[list[Tensor]]: """Forward-pass images to the network. During the training mode the model extracts features from encoder and decoder networks. @@ -60,7 +60,7 @@ def forward(self, images: Tensor) -> Union[Tensor, Tuple[List[Tensor], List[Tens images (Tensor): Batch of images Returns: - Union[Tensor, Tuple[List[Tensor],List[Tensor]]]: Encoder and decoder features in training mode, + Tensor | list[Tensor] | tuple[list[Tensor]]: Encoder and decoder features in training mode, else anomaly maps. """ self.encoder.eval() diff --git a/anomalib/models/rkde/lightning_model.py b/anomalib/models/rkde/lightning_model.py index 3082eb526e..f0a721f4de 100644 --- a/anomalib/models/rkde/lightning_model.py +++ b/anomalib/models/rkde/lightning_model.py @@ -3,12 +3,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Union import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.utilities.cli import MODEL_REGISTRY +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch import Tensor from anomalib.models.components import AnomalyModule @@ -47,7 +49,7 @@ def __init__( n_pca_components: int = 16, feature_scaling_method: FeatureScalingMethod = FeatureScalingMethod.SCALE, max_training_points: int = 40000, - ): + ) -> None: super().__init__() self.model: RkdeModel = RkdeModel( @@ -60,19 +62,18 @@ def __init__( feature_scaling_method=feature_scaling_method, max_training_points=max_training_points, ) - self.embeddings: List[Tensor] = [] + self.embeddings: list[Tensor] = [] @staticmethod - def configure_optimizers(): + def configure_optimizers() -> None: """RKDE doesn't require optimization, therefore returns no optimizers.""" return None - def training_step(self, batch, _batch_idx): + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> None: """Training Step of RKDE. For each batch, features are extracted from the CNN. Args: - batch (Dict[str, Any]): Batch containing image filename, image, label and mask - _batch_idx: Index of the batch. + batch (dict[str, str | Tensor]): Batch containing image filename, image, label and mask Returns: Deep CNN features. @@ -87,13 +88,13 @@ def on_validation_start(self) -> None: logger.info("Fitting a KDE model to the embedding collected from the training set.") self.model.fit(embeddings) - def validation_step(self, batch, _): + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of RKde. Similar to the training step, features are extracted from the CNN for each batch. Args: - batch: Input batch + batch (dict[str, str | Tensor]): Batch containing image filename, image, label and mask Returns: Dictionary containing probability, prediction and ground truth values. @@ -103,7 +104,8 @@ def validation_step(self, batch, _): boxes, scores = self.model(batch["image"]) # convert batched predictions to list format - batch_size = batch["image"].shape[0] + image: Tensor = batch["image"] + batch_size = image.shape[0] indices = boxes[:, 0] batch["pred_boxes"] = [boxes[indices == i, 1:] for i in range(batch_size)] batch["box_scores"] = [scores[indices == i] for i in range(batch_size)] @@ -115,10 +117,10 @@ class RkdeLightning(Rkde): """Rkde: Deep Feature Kernel Density Estimation. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( roi_stage=RoiStage(hparams.model.roi_stage), roi_score_threshold=hparams.model.roi_score_threshold, @@ -129,5 +131,5 @@ def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: feature_scaling_method=FeatureScalingMethod(hparams.model.feature_scaling_method), max_training_points=hparams.model.max_training_points, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) diff --git a/anomalib/models/rkde/region_extractor.py b/anomalib/models/rkde/region_extractor.py index ab37377dc4..2a61695678 100644 --- a/anomalib/models/rkde/region_extractor.py +++ b/anomalib/models/rkde/region_extractor.py @@ -6,8 +6,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from enum import Enum -from typing import List import torch from torch import Tensor, nn @@ -106,7 +107,7 @@ def forward(self, batch: Tensor) -> Tensor: regions = torch.cat([indices.unsqueeze(1).to(batch.device), torch.cat(regions)], dim=1) return regions - def post_process_box_predictions(self, pred_boxes: Tensor, pred_scores: Tensor) -> List[Tensor]: + def post_process_box_predictions(self, pred_boxes: Tensor, pred_scores: Tensor) -> list[Tensor]: """Post-processes the box predictions. The post-processing consists of removing small boxes, applying nms, and @@ -117,10 +118,10 @@ def post_process_box_predictions(self, pred_boxes: Tensor, pred_scores: Tensor) pred_scores (Tensor): Tensor of shape () with a confidence score for each box prediction. Returns: - List[Tensor]: Post-processed box predictions of shape (N, 4). + list[Tensor]: Post-processed box predictions of shape (N, 4). """ - processed_boxes: List[Tensor] = [] + processed_boxes: list[Tensor] = [] for boxes, scores in zip(pred_boxes, pred_scores): # remove small boxes diff --git a/anomalib/models/rkde/torch_model.py b/anomalib/models/rkde/torch_model.py index 711858bbb0..237c6d15c7 100644 --- a/anomalib/models/rkde/torch_model.py +++ b/anomalib/models/rkde/torch_model.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Tuple, Union import torch from torch import Tensor, nn @@ -47,7 +48,7 @@ def __init__( n_pca_components: int = 16, feature_scaling_method: FeatureScalingMethod = FeatureScalingMethod.SCALE, max_training_points: int = 40000, - ): + ) -> None: super().__init__() self.region_extractor = RegionExtractor( @@ -77,14 +78,14 @@ def fit(self, embeddings: Tensor) -> bool: """ return self.classifier.fit(embeddings) - def forward(self, batch: Tensor) -> Union[Tensor, Tuple[Tensor, Tensor]]: + def forward(self, batch: Tensor) -> Tensor | tuple[Tensor, Tensor]: """Prediction by normality model. Args: input (Tensor): Input images. Returns: - Union[Tensor, Tuple[Tensor, Tensor]]: The extracted features (when in training mode), or the predicted rois + Tensor | tuple[Tensor, Tensor]: The extracted features (when in training mode), or the predicted rois and corresponding anomaly scores. """ self.region_extractor.eval() diff --git a/anomalib/models/stfpm/anomaly_map.py b/anomalib/models/stfpm/anomaly_map.py index 53dd0fa32a..41b43f3663 100644 --- a/anomalib/models/stfpm/anomaly_map.py +++ b/anomalib/models/stfpm/anomaly_map.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, Tuple, Union +from __future__ import annotations import torch import torch.nn.functional as F @@ -14,10 +14,7 @@ class AnomalyMapGenerator(nn.Module): """Generate Anomaly Heatmap.""" - def __init__( - self, - image_size: Union[ListConfig, Tuple], - ): + def __init__(self, image_size: ListConfig | tuple) -> None: super().__init__() self.distance = torch.nn.PairwiseDistance(p=2, keepdim=True) self.image_size = image_size if isinstance(image_size, tuple) else tuple(image_size) @@ -40,13 +37,13 @@ def compute_layer_map(self, teacher_features: Tensor, student_features: Tensor) return layer_map def compute_anomaly_map( - self, teacher_features: Dict[str, Tensor], student_features: Dict[str, Tensor] + self, teacher_features: dict[str, Tensor], student_features: dict[str, Tensor] ) -> torch.Tensor: """Compute the overall anomaly map via element-wise production the interpolated anomaly maps. Args: - teacher_features (Dict[str, Tensor]): Teacher features - student_features (Dict[str, Tensor]): Student features + teacher_features (dict[str, Tensor]): Teacher features + student_features (dict[str, Tensor]): Student features Returns: Final anomaly map @@ -60,7 +57,7 @@ def compute_anomaly_map( return anomaly_map - def forward(self, **kwargs: Dict[str, Tensor]) -> torch.Tensor: + def forward(self, **kwargs: dict[str, Tensor]) -> torch.Tensor: """Returns anomaly map. Expects `teach_features` and `student_features` keywords to be passed explicitly. @@ -82,7 +79,7 @@ def forward(self, **kwargs: Dict[str, Tensor]) -> torch.Tensor: if not ("teacher_features" in kwargs and "student_features" in kwargs): raise ValueError(f"Expected keys `teacher_features` and `student_features. Found {kwargs.keys()}") - teacher_features: Dict[str, Tensor] = kwargs["teacher_features"] - student_features: Dict[str, Tensor] = kwargs["student_features"] + teacher_features: dict[str, Tensor] = kwargs["teacher_features"] + student_features: dict[str, Tensor] = kwargs["student_features"] return self.compute_anomaly_map(teacher_features, student_features) diff --git a/anomalib/models/stfpm/lightning_model.py b/anomalib/models/stfpm/lightning_model.py index 545a6e45b9..ad847987da 100644 --- a/anomalib/models/stfpm/lightning_model.py +++ b/anomalib/models/stfpm/lightning_model.py @@ -6,13 +6,14 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List, Tuple, Union +from __future__ import annotations import torch from omegaconf import DictConfig, ListConfig from pytorch_lightning.callbacks import EarlyStopping from pytorch_lightning.utilities.cli import MODEL_REGISTRY -from torch import optim +from pytorch_lightning.utilities.types import STEP_OUTPUT +from torch import Tensor, optim from anomalib.models.components import AnomalyModule from anomalib.models.stfpm.loss import STFPMLoss @@ -26,17 +27,17 @@ class Stfpm(AnomalyModule): """PL Lightning Module for the STFPM algorithm. Args: - input_size (Tuple[int, int]): Size of the model input. + input_size (tuple[int, int]): Size of the model input. backbone (str): Backbone CNN network - layers (List[str]): Layers to extract features from the backbone CNN + layers (list[str]): Layers to extract features from the backbone CNN """ def __init__( self, - input_size: Tuple[int, int], + input_size: tuple[int, int], backbone: str, - layers: List[str], - ): + layers: list[str], + ) -> None: super().__init__() self.model = STFPMModel( @@ -46,14 +47,13 @@ def __init__( ) self.loss = STFPMLoss() - def training_step(self, batch, _): # pylint: disable=arguments-differ + def training_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Training Step of STFPM. For each batch, teacher and student and teacher features are extracted from the CNN. Args: - batch (Tensor): Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: Loss value @@ -64,15 +64,14 @@ def training_step(self, batch, _): # pylint: disable=arguments-differ self.log("train_loss", loss.item(), on_epoch=True, prog_bar=True, logger=True) return {"loss": loss} - def validation_step(self, batch, _): # pylint: disable=arguments-differ + def validation_step(self, batch: dict[str, str | Tensor], *args, **kwargs) -> STEP_OUTPUT: """Validation Step of STFPM. Similar to the training step, student/teacher features are extracted from the CNN for each batch, and anomaly map is computed. Args: - batch (Tensor): Input batch - _: Index of the batch. + batch (dict[str, str | Tensor]): Input batch Returns: Dictionary containing images, anomaly maps, true labels and masks. @@ -87,19 +86,19 @@ class StfpmLightning(Stfpm): """PL Lightning Module for the STFPM algorithm. Args: - hparams (Union[DictConfig, ListConfig]): Model params + hparams (DictConfig | ListConfig): Model params """ - def __init__(self, hparams: Union[DictConfig, ListConfig]) -> None: + def __init__(self, hparams: DictConfig | ListConfig) -> None: super().__init__( input_size=hparams.model.input_size, backbone=hparams.model.backbone, layers=hparams.model.layers, ) - self.hparams: Union[DictConfig, ListConfig] # type: ignore + self.hparams: DictConfig | ListConfig # type: ignore self.save_hyperparameters(hparams) - def configure_callbacks(self): + def configure_callbacks(self) -> list[EarlyStopping]: """Configure model-specific callbacks. Note: diff --git a/anomalib/models/stfpm/loss.py b/anomalib/models/stfpm/loss.py index c675baad5c..21172e694c 100644 --- a/anomalib/models/stfpm/loss.py +++ b/anomalib/models/stfpm/loss.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List +from __future__ import annotations import torch import torch.nn.functional as F @@ -30,7 +30,7 @@ class STFPMLoss(nn.Module): tensor(51.2015, grad_fn=) """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.mse_loss = nn.MSELoss(reduction="sum") @@ -53,18 +53,18 @@ def compute_layer_loss(self, teacher_feats: Tensor, student_feats: Tensor) -> Te return layer_loss - def forward(self, teacher_features: Dict[str, Tensor], student_features: Dict[str, Tensor]) -> Tensor: + def forward(self, teacher_features: dict[str, Tensor], student_features: dict[str, Tensor]) -> Tensor: """Compute the overall loss via the weighted average of the layer losses computed by the cosine similarity. Args: - teacher_features (Dict[str, Tensor]): Teacher features - student_features (Dict[str, Tensor]): Student features + teacher_features (dict[str, Tensor]): Teacher features + student_features (dict[str, Tensor]): Student features Returns: Total loss, which is the weighted average of the layer losses. """ - layer_losses: List[Tensor] = [] + layer_losses: list[Tensor] = [] for layer in teacher_features.keys(): loss = self.compute_layer_loss(teacher_features[layer], student_features[layer]) layer_losses.append(loss) diff --git a/anomalib/models/stfpm/torch_model.py b/anomalib/models/stfpm/torch_model.py index 8a7da966e0..a730c1fe50 100644 --- a/anomalib/models/stfpm/torch_model.py +++ b/anomalib/models/stfpm/torch_model.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List, Optional, Tuple +from __future__ import annotations from torch import Tensor, nn @@ -16,19 +16,19 @@ class STFPMModel(nn.Module): """STFPM: Student-Teacher Feature Pyramid Matching for Unsupervised Anomaly Detection. Args: - layers (List[str]): Layers used for feature extraction - input_size (Tuple[int, int]): Input size for the model. + layers (list[str]): Layers used for feature extraction + input_size (tuple[int, int]): Input size for the model. backbone (str, optional): Pre-trained model backbone. Defaults to "resnet18". """ def __init__( self, - layers: List[str], - input_size: Tuple[int, int], + layers: list[str], + input_size: tuple[int, int], backbone: str = "resnet18", - ): + ) -> None: super().__init__() - self.tiler: Optional[Tiler] = None + self.tiler: Tiler | None = None self.backbone = backbone self.teacher_model = FeatureExtractor(backbone=self.backbone, pre_trained=True, layers=layers) @@ -46,9 +46,9 @@ def __init__( image_size = (self.tiler.tile_size_h, self.tiler.tile_size_w) else: image_size = input_size - self.anomaly_map_generator = AnomalyMapGenerator(image_size=tuple(image_size)) + self.anomaly_map_generator = AnomalyMapGenerator(image_size=image_size) - def forward(self, images): + def forward(self, images: Tensor) -> Tensor | dict[str, Tensor] | tuple[dict[str, Tensor]]: """Forward-pass images into the network. During the training mode the model extracts the features from the teacher and student networks. @@ -62,8 +62,8 @@ def forward(self, images): """ if self.tiler: images = self.tiler.tile(images) - teacher_features: Dict[str, Tensor] = self.teacher_model(images) - student_features: Dict[str, Tensor] = self.student_model(images) + teacher_features: dict[str, Tensor] = self.teacher_model(images) + student_features: dict[str, Tensor] = self.student_model(images) if self.training: output = teacher_features, student_features else: diff --git a/anomalib/post_processing/normalization/cdf.py b/anomalib/post_processing/normalization/cdf.py index e76425169c..00a3602ccb 100644 --- a/anomalib/post_processing/normalization/cdf.py +++ b/anomalib/post_processing/normalization/cdf.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Optional, Union +from __future__ import annotations import numpy as np import torch @@ -13,11 +13,11 @@ def standardize( - targets: Union[np.ndarray, Tensor], - mean: Union[np.ndarray, Tensor, float], - std: Union[np.ndarray, Tensor, float], - center_at: Optional[float] = None, -) -> Union[np.ndarray, Tensor]: + targets: np.ndarray | Tensor, + mean: float | np.ndarray | Tensor, + std: float | np.ndarray | Tensor, + center_at: float | None = None, +) -> np.ndarray | Tensor: """Standardize the targets to the z-domain.""" if isinstance(targets, np.ndarray): targets = np.log(targets) @@ -31,9 +31,7 @@ def standardize( return standardized -def normalize( - targets: Union[np.ndarray, Tensor], threshold: Union[np.ndarray, Tensor, float] -) -> Union[np.ndarray, Tensor]: +def normalize(targets: np.ndarray | Tensor, threshold: float | np.ndarray | Tensor) -> np.ndarray | Tensor: """Normalize the targets by using the cumulative density function.""" if isinstance(targets, Tensor): return normalize_torch(targets, threshold) @@ -52,6 +50,6 @@ def normalize_torch(targets: Tensor, threshold: Tensor) -> Tensor: return normalized -def normalize_numpy(targets: np.ndarray, threshold: Union[np.ndarray, float]) -> np.ndarray: +def normalize_numpy(targets: np.ndarray, threshold: float | np.ndarray) -> np.ndarray: """Normalize the targets by using the cumulative density function, Numpy version.""" return norm.cdf(targets - threshold) diff --git a/anomalib/post_processing/normalization/min_max.py b/anomalib/post_processing/normalization/min_max.py index 7fe751433e..bab3ce9108 100644 --- a/anomalib/post_processing/normalization/min_max.py +++ b/anomalib/post_processing/normalization/min_max.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Union +from __future__ import annotations import numpy as np import torch @@ -11,11 +11,11 @@ def normalize( - targets: Union[np.ndarray, Tensor, np.float32], - threshold: Union[np.ndarray, Tensor, float], - min_val: Union[np.ndarray, Tensor, float], - max_val: Union[np.ndarray, Tensor, float], -) -> Union[np.ndarray, Tensor]: + targets: np.ndarray | np.float32 | Tensor, + threshold: float | np.ndarray | Tensor, + min_val: float | np.ndarray | Tensor, + max_val: float | np.ndarray | Tensor, +) -> np.ndarray | Tensor: """Apply min-max normalization and shift the values such that the threshold value is centered at 0.5.""" normalized = ((targets - threshold) / (max_val - min_val)) + 0.5 if isinstance(targets, (np.ndarray, np.float32, np.float64)): diff --git a/anomalib/post_processing/post_process.py b/anomalib/post_processing/post_process.py index 226cb21f23..ba24f76de5 100644 --- a/anomalib/post_processing/post_process.py +++ b/anomalib/post_processing/post_process.py @@ -4,9 +4,10 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math from enum import Enum -from typing import Optional, Tuple import cv2 import numpy as np @@ -23,18 +24,18 @@ class ThresholdMethod(str, Enum): def add_label( image: np.ndarray, label_name: str, - color: Tuple[int, int, int], - confidence: Optional[float] = None, + color: tuple[int, int, int], + confidence: float | None = None, font_scale: float = 5e-3, thickness_scale=1e-3, -): +) -> np.ndarray: """Adds a label to an image. Args: image (np.ndarray): Input image. label_name (str): Name of the label that will be displayed on the image. - color (Tuple[int, int, int]): RGB values for background color of label. - confidence (Optional[float]): confidence score of the label. + color (tuple[int, int, int]): RGB values for background color of label. + confidence (float | None): confidence score of the label. font_scale (float): scale of the font size relative to image size. Increase for bigger font. thickness_scale (float): scale of the font thickness. Increase for thicker font. @@ -71,12 +72,12 @@ def add_label( return image -def add_normal_label(image: np.ndarray, confidence: Optional[float] = None): +def add_normal_label(image: np.ndarray, confidence: float | None = None) -> np.ndarray: """Adds the normal label to the image.""" return add_label(image, "normal", (225, 252, 134), confidence) -def add_anomalous_label(image: np.ndarray, confidence: Optional[float] = None): +def add_anomalous_label(image: np.ndarray, confidence: float | None = None) -> np.ndarray: """Adds the anomalous label to the image.""" return add_label(image, "anomalous", (255, 100, 100), confidence) @@ -153,13 +154,13 @@ def compute_mask(anomaly_map: np.ndarray, threshold: float, kernel_size: int = 4 return mask -def draw_boxes(image: np.ndarray, boxes: np.ndarray, color: Tuple[int, int, int]) -> np.ndarray: +def draw_boxes(image: np.ndarray, boxes: np.ndarray, color: tuple[int, int, int]) -> np.ndarray: """Draw bounding boxes on an image. Args: image (np.ndarray): Source image. boxes (np.nparray): 2D array of shape (N, 4) where each row contains the xyxy coordinates of a bounding box. - color (Tuple[int, int, int]): Color of the drawn boxes in RGB format. + color (tuple[int, int, int]): Color of the drawn boxes in RGB format. Returns: np.ndarray: Image showing the bounding boxes drawn on top of the source image. diff --git a/anomalib/post_processing/visualizer.py b/anomalib/post_processing/visualizer.py index d14e51ef58..4086ddbdfe 100644 --- a/anomalib/post_processing/visualizer.py +++ b/anomalib/post_processing/visualizer.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, Iterator, List, Optional +from typing import Iterator import cv2 import matplotlib.figure @@ -30,12 +32,12 @@ class ImageResult: image: np.ndarray pred_score: float pred_label: str - anomaly_map: Optional[np.ndarray] = None - gt_mask: Optional[np.ndarray] = None - pred_mask: Optional[np.ndarray] = None - gt_boxes: Optional[np.ndarray] = None - pred_boxes: Optional[np.ndarray] = None - box_labels: Optional[np.ndarray] = None + anomaly_map: np.ndarray | None = None + gt_mask: np.ndarray | None = None + pred_mask: np.ndarray | None = None + gt_boxes: np.ndarray | None = None + pred_boxes: np.ndarray | None = None + box_labels: np.ndarray | None = None heat_map: np.ndarray = field(init=False) segmentations: np.ndarray = field(init=False) @@ -68,29 +70,29 @@ class Visualizer: """ def __init__(self, mode: str, task: TaskType) -> None: - if mode not in ["full", "simple"]: + if mode not in ("full", "simple"): raise ValueError(f"Unknown visualization mode: {mode}. Please choose one of ['full', 'simple']") self.mode = mode - if task not in [TaskType.CLASSIFICATION, TaskType.DETECTION, TaskType.SEGMENTATION]: + if task not in (TaskType.CLASSIFICATION, TaskType.DETECTION, TaskType.SEGMENTATION): raise ValueError( f"Unknown task type: {mode}. Please choose one of ['classification', 'detection', 'segmentation']" ) self.task = task - def visualize_batch(self, batch: Dict) -> Iterator[np.ndarray]: + def visualize_batch(self, batch: dict) -> Iterator[np.ndarray]: """Generator that yields a visualization result for each item in the batch. Args: - batch (Dict): Dictionary containing the ground truth and predictions of a batch of images. + batch (dict): Dictionary containing the ground truth and predictions of a batch of images. Returns: Generator that yields a display-ready visualization for each image. """ batch_size, _num_channels, height, width = batch["image"].size() for i in range(batch_size): - if "image_path" in batch.keys(): + if "image_path" in batch: image = read_image(path=batch["image_path"][i], image_size=(height, width)) - elif "video_path" in batch.keys(): + elif "video_path" in batch: image = batch["original_image"][i].squeeze().numpy() image = cv2.resize(image, dsize=(width, height), interpolation=cv2.INTER_AREA) else: @@ -233,18 +235,18 @@ class ImageGrid: must be called to compile the image grid and obtain the final visualization. """ - def __init__(self): - self.images: List[Dict] = [] + def __init__(self) -> None: + self.images: list[dict] = [] self.figure: matplotlib.figure.Figure self.axis: np.ndarray - def add_image(self, image: np.ndarray, title: Optional[str] = None, color_map: Optional[str] = None) -> None: + def add_image(self, image: np.ndarray, title: str | None = None, color_map: str | None = None) -> None: """Add an image to the grid. Args: image (np.ndarray): Image which should be added to the figure. title (str): Image title shown on the plot. - color_map (Optional[str]): Name of matplotlib color map used to map scalar data to colours. Defaults to None. + color_map (str | None): Name of matplotlib color map used to map scalar data to colours. Defaults to None. """ image_data = dict(image=image, title=title, color_map=color_map) self.images.append(image_data) diff --git a/anomalib/pre_processing/pre_process.py b/anomalib/pre_processing/pre_process.py index 4b023199ba..fac77a427f 100644 --- a/anomalib/pre_processing/pre_process.py +++ b/anomalib/pre_processing/pre_process.py @@ -7,9 +7,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import warnings -from typing import Optional, Tuple, Union +from typing import Any import albumentations as A from albumentations.pytorch import ToTensorV2 @@ -17,11 +19,11 @@ logger = logging.getLogger(__name__) -def get_image_height_and_width(image_size: Optional[Union[int, Tuple]] = None) -> Tuple[Optional[int], Optional[int]]: +def get_image_height_and_width(image_size: int | tuple | None = None) -> tuple[int | None, int | None]: """Get image height and width from ``image_size`` variable. Args: - image_size (Optional[Union[int, Tuple[int, int]]], optional): Input image size. + image_size (int | tuple | None, optional): Input image size. Raises: ValueError: Image size not None, int or tuple. @@ -40,12 +42,12 @@ def get_image_height_and_width(image_size: Optional[Union[int, Tuple]] = None) - Traceback (most recent call last): File "", line 1, in File "", line 18, in get_image_height_and_width - ValueError: ``image_size`` could be either int or Tuple[int, int] + ValueError: ``image_size`` could be either int or tuple[int, int] Returns: - Tuple[Optional[int], Optional[int]]: A tuple containing image height and width values. + tuple[int | None, int | None]: A tuple containing image height and width values. """ - height_and_width: Tuple[Optional[int], Optional[int]] + height_and_width: tuple[int | None, int | None] if isinstance(image_size, int): height_and_width = (image_size, image_size) elif isinstance(image_size, tuple): @@ -53,22 +55,22 @@ def get_image_height_and_width(image_size: Optional[Union[int, Tuple]] = None) - elif image_size is None: height_and_width = (None, None) else: - raise ValueError("``image_size`` could be either int or Tuple[int, int]") + raise ValueError("``image_size`` could be either int or tuple[int, int]") return height_and_width def get_transforms( - config: Optional[Union[str, A.Compose]] = None, - image_size: Optional[Union[int, Tuple]] = None, + config: str | A.Compose | None = None, + image_size: int | tuple | None = None, to_tensor: bool = True, ) -> A.Compose: """Get transforms from config or image size. Args: - config (Optional[Union[str, A.Compose]], optional): Albumentations transforms. + config (str | A.Compose | None, optional): Albumentations transforms. Either config or albumentations ``Compose`` object. Defaults to None. - image_size (Optional[Union[int, Tuple]], optional): Image size to transform. Defaults to None. + image_size (int | tuple | None, optional): Image size to transform. Defaults to None. to_tensor (bool, optional): Boolean to convert the final transforms into Torch tensor. Defaults to True. Raises: @@ -119,7 +121,7 @@ def get_transforms( ) ) - if config is None and image_size is None: + if config is None is image_size: raise ValueError( "Both config and image_size cannot be `None`. " "Provide either config file to de-serialize transforms " @@ -168,10 +170,10 @@ class PreProcessor: For the inference it returns a numpy array. Args: - config (Optional[Union[str, A.Compose]], optional): Transformation configurations. + config (str | A.Compose | None, optional): Transformation configurations. When it is ``None``, ``PreProcessor`` only applies resizing. When it is ``str`` it loads the config via ``albumentations`` deserialisation methos . Defaults to None. - image_size (Optional[Union[int, Tuple[int, int]]], optional): When there is no config, + image_size (int | tuple | None, optional): When there is no config, ``image_size`` resizes the image. Defaults to None. to_tensor (bool, optional): Boolean to check whether the augmented image is transformed into a tensor or not. Defaults to True. @@ -213,8 +215,8 @@ class PreProcessor: def __init__( self, - config: Optional[Union[str, A.Compose]] = None, - image_size: Optional[Union[int, Tuple]] = None, + config: str | A.Compose | None = None, + image_size: int | tuple | None = None, to_tensor: bool = True, ) -> None: warnings.warn( @@ -229,6 +231,6 @@ def __init__( self.transforms = get_transforms(config, image_size, to_tensor) - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> dict[str, Any]: """Return transformed arguments.""" return self.transforms(*args, **kwargs) diff --git a/anomalib/pre_processing/tiler.py b/anomalib/pre_processing/tiler.py index 08f4687f40..43b148d048 100644 --- a/anomalib/pre_processing/tiler.py +++ b/anomalib/pre_processing/tiler.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from itertools import product from math import ceil -from typing import Optional, Sequence, Tuple, Union +from typing import Sequence import torch import torchvision.transforms as T @@ -17,15 +19,15 @@ class StrideSizeError(Exception): """StrideSizeError to raise exception when stride size is greater than the tile size.""" -def compute_new_image_size(image_size: Tuple, tile_size: Tuple, stride: Tuple) -> Tuple: +def compute_new_image_size(image_size: tuple, tile_size: tuple, stride: tuple) -> tuple: """This function checks if image size is divisible by tile size and stride. If not divisible, it resizes the image size to make it divisible. Args: - image_size (Tuple): Original image size - tile_size (Tuple): Tile size - stride (Tuple): Stride + image_size (tuple): Original image size + tile_size (tuple): Tile size + stride (tuple): Stride Examples: >>> compute_new_image_size(image_size=(512, 512), tile_size=(256, 256), stride=(128, 128)) @@ -35,7 +37,7 @@ def compute_new_image_size(image_size: Tuple, tile_size: Tuple, stride: Tuple) - (555, 555) Returns: - Tuple: Updated image size that is divisible by tile size and stride. + tuple: Updated image size that is divisible by tile size and stride. """ def __compute_new_edge_size(edge_size: int, tile_size: int, stride: int) -> int: @@ -51,12 +53,12 @@ def __compute_new_edge_size(edge_size: int, tile_size: int, stride: int) -> int: return resized_h, resized_w -def upscale_image(image: Tensor, size: Tuple, mode: str = "padding") -> Tensor: +def upscale_image(image: Tensor, size: tuple, mode: str = "padding") -> Tensor: """Upscale image to the desired size via either padding or interpolation. Args: image (Tensor): Image - size (Tuple): Tuple to which image is upscaled. + size (tuple): tuple to which image is upscaled. mode (str, optional): Upscaling mode. Defaults to "padding". Examples: @@ -90,12 +92,12 @@ def upscale_image(image: Tensor, size: Tuple, mode: str = "padding") -> Tensor: return image -def downscale_image(image: Tensor, size: Tuple, mode: str = "padding") -> Tensor: +def downscale_image(image: Tensor, size: tuple, mode: str = "padding") -> Tensor: """Opposite of upscaling. This image downscales image to a desired size. Args: image (Tensor): Input image - size (Tuple): Size to which image is down scaled. + size (tuple): Size to which image is down scaled. mode (str, optional): Downscaling mode. Defaults to "padding". Examples: @@ -146,8 +148,8 @@ class Tiler: def __init__( self, - tile_size: Union[int, Sequence], - stride: Optional[Union[int, Sequence]] = None, + tile_size: int | Sequence, + stride: int | Sequence | None = None, remove_border_count: int = 0, mode: str = "padding", tile_count: int = 4, @@ -159,7 +161,7 @@ def __init__( if stride is not None: self.stride_h, self.stride_w = self.__validate_size_type(stride) - self.remove_border_count = int(remove_border_count) + self.remove_border_count = remove_border_count self.overlapping = not (self.stride_h == self.tile_size_h and self.stride_w == self.tile_size_w) self.mode = mode @@ -169,7 +171,7 @@ def __init__( "Please ensure stride size is less than or equal than tiling size." ) - if self.mode not in ["padding", "interpolation"]: + if self.mode not in ("padding", "interpolation"): raise ValueError(f"Unknown tiling mode {self.mode}. Available modes are padding and interpolation") self.batch_size: int @@ -188,7 +190,7 @@ def __init__( self.num_patches_w: int @staticmethod - def __validate_size_type(parameter: Union[int, Sequence]) -> Tuple[int, ...]: + def __validate_size_type(parameter: int | Sequence) -> tuple[int, ...]: if isinstance(parameter, int): output = (parameter, parameter) elif isinstance(parameter, Sequence): @@ -330,7 +332,7 @@ def __fold(self, tiles: Tensor) -> Tensor: return img - def tile(self, image: Tensor, use_random_tiling: Optional[bool] = False) -> Tensor: + def tile(self, image: Tensor, use_random_tiling: bool | None = False) -> Tensor: """Tiles an input image to either overlapping, non-overlapping or random patches. Args: diff --git a/anomalib/pre_processing/transforms/custom.py b/anomalib/pre_processing/transforms/custom.py index 6ca4a67d34..083efc5572 100644 --- a/anomalib/pre_processing/transforms/custom.py +++ b/anomalib/pre_processing/transforms/custom.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import warnings -from typing import List, Optional, Tuple import numpy as np from torch import Tensor @@ -13,7 +14,7 @@ class Denormalize: """Denormalize Torch Tensor into np image format.""" - def __init__(self, mean: Optional[List[float]] = None, std: Optional[List[float]] = None): + def __init__(self, mean: list[float] | None = None, std: list[float] | None = None) -> None: """Denormalize Torch Tensor into np image format. Args: @@ -52,7 +53,7 @@ def __call__(self, tensor: Tensor) -> np.ndarray: array = (tensor * 255).permute(1, 2, 0).cpu().numpy().astype(np.uint8) return array - def __repr__(self): + def __repr__(self) -> str: """Representational string.""" return self.__class__.__name__ + "()" @@ -60,12 +61,12 @@ def __repr__(self): class ToNumpy: """Convert Tensor into Numpy Array.""" - def __call__(self, tensor: Tensor, dims: Optional[Tuple[int, ...]] = None) -> np.ndarray: + def __call__(self, tensor: Tensor, dims: tuple[int, ...] | None = None) -> np.ndarray: """Convert Tensor into Numpy Array. Args: tensor (Tensor): Tensor to convert. Input tensor in range 0-1. - dims (Optional[Tuple[int, ...]], optional): Convert dimensions from torch to numpy format. + dims (tuple[int, ...] | None, optional): Convert dimensions from torch to numpy format. Tuple corresponding to axis permutation from torch tensor to numpy array. Defaults to None. Returns: diff --git a/anomalib/utils/callbacks/__init__.py b/anomalib/utils/callbacks/__init__.py index e5d5549b2e..5ee1bfdefc 100644 --- a/anomalib/utils/callbacks/__init__.py +++ b/anomalib/utils/callbacks/__init__.py @@ -3,11 +3,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import os import warnings from importlib import import_module -from typing import List, Union import yaml from jsonargparse.namespace import Namespace @@ -43,18 +44,18 @@ logger = logging.getLogger(__name__) -def get_callbacks(config: Union[ListConfig, DictConfig]) -> List[Callback]: +def get_callbacks(config: DictConfig | ListConfig) -> list[Callback]: """Return base callbacks for all the lightning models. Args: config (DictConfig): Model config Return: - (List[Callback]): List of callbacks. + (list[Callback]): List of callbacks. """ logger.info("Loading the callbacks") - callbacks: List[Callback] = [] + callbacks: list[Callback] = [] monitor_metric = None if "early_stopping" not in config.model.keys() else config.model.early_stopping.metric monitor_mode = "max" if "early_stopping" not in config.model.keys() else config.model.early_stopping.mode @@ -97,7 +98,7 @@ def get_callbacks(config: Union[ListConfig, DictConfig]) -> List[Callback]: if "normalization_method" in config.model.keys() and not config.model.normalization_method == "none": if config.model.normalization_method == "cdf": - if config.model.name in ["padim", "stfpm"]: + if config.model.name in ("padim", "stfpm"): if "nncf" in config.optimization and config.optimization.nncf.apply: raise NotImplementedError("CDF Score Normalization is currently not compatible with NNCF.") callbacks.append(CdfNormalizationCallback()) @@ -141,18 +142,18 @@ def get_callbacks(config: Union[ListConfig, DictConfig]) -> List[Callback]: warnings.warn(f"Export option: {config.optimization.export_mode} not found. Defaulting to no model export") # Add callback to log graph to loggers - if config.logging.log_graph not in [None, False]: + if config.logging.log_graph not in (None, False): callbacks.append(GraphLogger()) return callbacks -def add_visualizer_callback(callbacks: List[Callback], config: Union[DictConfig, ListConfig]): +def add_visualizer_callback(callbacks: list[Callback], config: DictConfig | ListConfig) -> None: """Configure the visualizer callback based on the config and add it to the list of callbacks. Args: - callbacks (List[Callback]): Current list of callbacks. - config (Union[DictConfig, ListConfig]): The config object. + callbacks (list[Callback]): Current list of callbacks. + config (DictConfig | ListConfig): The config object. """ # visualization settings assert isinstance(config, (DictConfig, Namespace)) @@ -183,11 +184,7 @@ def add_visualizer_callback(callbacks: List[Callback], config: Union[DictConfig, config.visualization.inputs_are_normalized = not config.post_processing.normalization_method == "none" if config.visualization.log_images or config.visualization.save_images or config.visualization.show_images: - image_save_path = ( - config.visualization.image_save_path - if config.visualization.image_save_path - else config.project.path + "/images" - ) + image_save_path = config.visualization.image_save_path or config.project.path + "/images" for callback in (ImageVisualizerCallback, MetricVisualizerCallback): callbacks.append( callback( diff --git a/anomalib/utils/callbacks/cdf_normalization.py b/anomalib/utils/callbacks/cdf_normalization.py index 5b426fc8a5..52ec6af226 100644 --- a/anomalib/utils/callbacks/cdf_normalization.py +++ b/anomalib/utils/callbacks/cdf_normalization.py @@ -3,8 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any import pytorch_lightning as pl from pytorch_lightning import Callback, Trainer @@ -24,13 +26,14 @@ class CdfNormalizationCallback(Callback): """Callback that standardizes the image-level and pixel-level anomaly scores.""" - def __init__(self): - self.image_dist: Optional[LogNormal] = None - self.pixel_dist: Optional[LogNormal] = None + def __init__(self) -> None: + self.image_dist: LogNormal | None = None + self.pixel_dist: LogNormal | None = None - # pylint: disable=unused-argument - def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: str | None = None) -> None: """Adds training_distribution metrics to normalization metrics.""" + del trainer, stage # These variabels are not used. + if not hasattr(pl_module, "normalization_metrics"): pl_module.normalization_metrics = AnomalyScoreDistribution().cpu() elif not isinstance(pl_module.normalization_metrics, AnomalyScoreDistribution): @@ -39,15 +42,16 @@ def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: Optional[s f" got {type(pl_module.normalization_metrics)}" ) - # pylint: disable=unused-argument def on_test_start(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Called when the test begins.""" + del trainer # `trainer` variable is not used. + if pl_module.image_metrics is not None: pl_module.image_metrics.set_threshold(0.5) if pl_module.pixel_metrics is not None: pl_module.pixel_metrics.set_threshold(0.5) - def on_validation_epoch_start(self, trainer: "pl.Trainer", pl_module: AnomalyModule) -> None: + def on_validation_epoch_start(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Called when the validation starts after training. Use the current model to compute the anomaly score distributions @@ -59,44 +63,50 @@ def on_validation_epoch_start(self, trainer: "pl.Trainer", pl_module: AnomalyMod def on_validation_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: Optional[STEP_OUTPUT], - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the validation batch ends, standardizes the predicted scores and anomaly maps.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + self._standardize_batch(outputs, pl_module) def on_test_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: Optional[STEP_OUTPUT], - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the test batch ends, normalizes the predicted scores and anomaly maps.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + self._standardize_batch(outputs, pl_module) self._normalize_batch(outputs, pl_module) def on_predict_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: Dict, - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: dict, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the predict batch ends, normalizes the predicted scores and anomaly maps.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + self._standardize_batch(outputs, pl_module) self._normalize_batch(outputs, pl_module) outputs["pred_labels"] = outputs["pred_scores"] >= 0.5 - def _collect_stats(self, trainer, pl_module): + def _collect_stats(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Collect the statistics of the normal training data. Create a trainer and use it to predict the anomaly maps and scores of the normal training data. Then @@ -136,3 +146,4 @@ def _normalize_batch(outputs: STEP_OUTPUT, pl_module: AnomalyModule) -> None: outputs["pred_scores"] = normalize(outputs["pred_scores"], pl_module.image_threshold.value) if "anomaly_maps" in outputs.keys(): outputs["anomaly_maps"] = normalize(outputs["anomaly_maps"], pl_module.pixel_threshold.value) + outputs["anomaly_maps"] = normalize(outputs["anomaly_maps"], pl_module.pixel_threshold.value) diff --git a/anomalib/utils/callbacks/export.py b/anomalib/utils/callbacks/export.py index c81eccbb72..65ae1ff53a 100644 --- a/anomalib/utils/callbacks/export.py +++ b/anomalib/utils/callbacks/export.py @@ -3,10 +3,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -import os -from typing import Tuple +from pathlib import Path +import pytorch_lightning as pl from pytorch_lightning import Callback from pytorch_lightning.utilities.cli import CALLBACK_REGISTRY @@ -23,25 +25,27 @@ class ExportCallback(Callback): Model is first exported to ``.onnx`` format, and then converted to OpenVINO IR. Args: - input_size (Tuple[int, int]): Tuple of image height, width + input_size (tuple[int, int]): Tuple of image height, width dirpath (str): Path for model output filename (str): Name of output model """ - def __init__(self, input_size: Tuple[int, int], dirpath: str, filename: str, export_mode: ExportMode): + def __init__(self, input_size: tuple[int, int], dirpath: str, filename: str, export_mode: ExportMode) -> None: self.input_size = input_size self.dirpath = dirpath self.filename = filename self.export_mode = export_mode - def on_train_end(self, trainer, pl_module: AnomalyModule) -> None: # pylint: disable=W0613 + def on_train_end(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Call when the train ends. Converts the model to ``onnx`` format and then calls OpenVINO's model optimizer to get the ``.xml`` and ``.bin`` IR files. """ + del trainer # `trainer` variable is not used. + logger.info("Exporting the model") - os.makedirs(self.dirpath, exist_ok=True) + Path(self.dirpath).mkdir(parents=True, exist_ok=True) export( model=pl_module, input_size=self.input_size, diff --git a/anomalib/utils/callbacks/metrics_configuration.py b/anomalib/utils/callbacks/metrics_configuration.py index d35bc6a295..812d48e757 100644 --- a/anomalib/utils/callbacks/metrics_configuration.py +++ b/anomalib/utils/callbacks/metrics_configuration.py @@ -4,8 +4,9 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import List, Optional import pytorch_lightning as pl from pytorch_lightning.callbacks import Callback @@ -27,9 +28,9 @@ class MetricsConfigurationCallback(Callback): def __init__( self, task: TaskType = TaskType.SEGMENTATION, - image_metrics: Optional[List[str]] = None, - pixel_metrics: Optional[List[str]] = None, - ): + image_metrics: list[str] | None = None, + pixel_metrics: list[str] | None = None, + ) -> None: """Create image and pixel-level AnomalibMetricsCollection. This callback creates AnomalibMetricsCollection based on the @@ -39,8 +40,8 @@ def __init__( Args: task (TaskType): Task type of the current run. - image_metrics (Optional[List[str]]): List of image-level metrics. - pixel_metrics (Optional[List[str]]): List of pixel-level metrics. + image_metrics (list[str] | None): List of image-level metrics. + pixel_metrics (list[str] | None): List of pixel-level metrics. """ self.task = task self.image_metric_names = image_metrics @@ -48,20 +49,22 @@ def __init__( def setup( self, - _trainer: pl.Trainer, - pl_module: pl.LightningModule, - stage: Optional[str] = None, # pylint: disable=unused-argument + trainer: pl.Trainer, + pl_module: AnomalyModule, + stage: str | None = None, ) -> None: """Setup image and pixel-level AnomalibMetricsCollection within Anomalib Model. Args: - _trainer (pl.Trainer): PyTorch Lightning Trainer - pl_module (pl.LightningModule): Anomalib Model that inherits pl LightningModule. - stage (Optional[str], optional): fit, validate, test or predict. Defaults to None. + trainer (pl.Trainer): PyTorch Lightning Trainer + pl_module (AnomalyModule): Anomalib Model that inherits pl LightningModule. + stage (str | None, optional): fit, validate, test or predict. Defaults to None. """ + del trainer, stage # These variables are not used. + image_metric_names = [] if self.image_metric_names is None else self.image_metric_names - pixel_metric_names: List[str] + pixel_metric_names: list[str] if self.pixel_metric_names is None: pixel_metric_names = [] elif self.task == TaskType.CLASSIFICATION: diff --git a/anomalib/utils/callbacks/min_max_normalization.py b/anomalib/utils/callbacks/min_max_normalization.py index 6d24aeb57a..632ed8b872 100644 --- a/anomalib/utils/callbacks/min_max_normalization.py +++ b/anomalib/utils/callbacks/min_max_normalization.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any import pytorch_lightning as pl import torch @@ -20,9 +22,10 @@ class MinMaxNormalizationCallback(Callback): """Callback that normalizes the image-level and pixel-level anomaly scores using min-max normalization.""" - # pylint: disable=unused-argument - def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: str | None = None) -> None: """Adds min_max metrics to normalization metrics.""" + del trainer, stage # These variables are not used. + if not hasattr(pl_module, "normalization_metrics"): pl_module.normalization_metrics = MinMax().cpu() elif not isinstance(pl_module.normalization_metrics, MinMax): @@ -30,23 +33,26 @@ def setup(self, trainer: pl.Trainer, pl_module: AnomalyModule, stage: Optional[s f"Expected normalization_metrics to be of type MinMax, got {type(pl_module.normalization_metrics)}" ) - # pylint: disable=unused-argument def on_test_start(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Called when the test begins.""" + del trainer # `trainer` variable is not used. + for metric in (pl_module.image_metrics, pl_module.pixel_metrics): if metric is not None: metric.set_threshold(0.5) def on_validation_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, outputs: STEP_OUTPUT, - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the validation batch ends, update the min and max observed values.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + if "anomaly_maps" in outputs: pl_module.normalization_metrics(outputs["anomaly_maps"]) elif "box_scores" in outputs: @@ -58,30 +64,34 @@ def on_validation_batch_end( def on_test_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: STEP_OUTPUT, - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the test batch ends, normalizes the predicted scores and anomaly maps.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + self._normalize_batch(outputs, pl_module) def on_predict_batch_end( self, - _trainer: pl.Trainer, + trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: Dict, - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: Any, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Called when the predict batch ends, normalizes the predicted scores and anomaly maps.""" + del trainer, batch, batch_idx, dataloader_idx # These variables are not used. + self._normalize_batch(outputs, pl_module) @staticmethod - def _normalize_batch(outputs, pl_module): + def _normalize_batch(outputs, pl_module) -> None: """Normalize a batch of predictions.""" image_threshold = pl_module.image_threshold.value.cpu() pixel_threshold = pl_module.pixel_threshold.value.cpu() diff --git a/anomalib/utils/callbacks/model_loader.py b/anomalib/utils/callbacks/model_loader.py index 7621d87a1d..a32384a3ee 100644 --- a/anomalib/utils/callbacks/model_loader.py +++ b/anomalib/utils/callbacks/model_loader.py @@ -3,8 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Optional import torch from pytorch_lightning import Callback, Trainer @@ -19,14 +20,15 @@ class LoadModelCallback(Callback): """Callback that loads the model weights from the state dict.""" - def __init__(self, weights_path): + def __init__(self, weights_path) -> None: self.weights_path = weights_path - # pylint: disable=unused-argument - def setup(self, trainer: Trainer, pl_module: AnomalyModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: Trainer, pl_module: AnomalyModule, stage: str | None = None) -> None: """Call when inference begins. Loads the model weights from ``weights_path`` into the PyTorch module. """ + del trainer, stage # These variables are not used. + logger.info("Loading the model from %s", self.weights_path) pl_module.load_state_dict(torch.load(self.weights_path, map_location=pl_module.device)["state_dict"]) diff --git a/anomalib/utils/callbacks/nncf/callback.py b/anomalib/utils/callbacks/nncf/callback.py index 5c1485eddf..51ca1de386 100644 --- a/anomalib/utils/callbacks/nncf/callback.py +++ b/anomalib/utils/callbacks/nncf/callback.py @@ -3,8 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import os -from typing import Any, Dict, Optional +from pathlib import Path +from typing import Any import pytorch_lightning as pl from nncf import NNCFConfig @@ -24,23 +27,24 @@ class NNCFCallback(Callback): the PyTorch module that must be compressed. Args: - config (Dict): NNCF Configuration + config (dict): NNCF Configuration export_dir (Str): Path where the export `onnx` and the OpenVINO `xml` and `bin` IR are saved. If None model will not be exported. """ - def __init__(self, config: Dict, export_dir: str = None): + def __init__(self, config: dict, export_dir: str | None = None) -> None: self.export_dir = export_dir self.config = NNCFConfig(config) - self.nncf_ctrl: Optional[CompressionAlgorithmController] = None + self.nncf_ctrl: CompressionAlgorithmController | None = None - # pylint: disable=unused-argument - def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: str | None = None) -> None: """Call when fit or test begins. Takes the pytorch model and wraps it using the compression controller so that it is ready for nncf fine-tuning. """ + del stage # `stage` variable is not used. + if self.nncf_ctrl is not None: return @@ -54,37 +58,38 @@ def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: Optio ) def on_train_batch_start( - self, - trainer: pl.Trainer, - _pl_module: pl.LightningModule, - _batch: Any, - _batch_idx: int, - _unused: Optional[int] = 0, + self, trainer: pl.Trainer, pl_module: pl.LightningModule, batch: Any, batch_idx: int, unused: int = 0 ) -> None: """Call when the train batch begins. Prepare compression method to continue training the model in the next step. """ + del trainer, pl_module, batch, batch_idx, unused # These variables are not used. + if self.nncf_ctrl: self.nncf_ctrl.scheduler.step() - def on_train_epoch_start(self, _trainer: pl.Trainer, _pl_module: pl.LightningModule) -> None: + def on_train_epoch_start(self, trainer: pl.Trainer, pl_module: pl.LightningModule) -> None: """Call when the train epoch starts. Prepare compression method to continue training the model in the next epoch. """ + del trainer, pl_module # `trainer` and `pl_module` variables are not used. + if self.nncf_ctrl: self.nncf_ctrl.scheduler.epoch_step() - def on_train_end(self, _trainer: pl.Trainer, _pl_module: pl.LightningModule) -> None: + def on_train_end(self, trainer: pl.Trainer, pl_module: pl.LightningModule) -> None: """Call when the train ends. Exports onnx model and if compression controller is not None, uses the onnx model to generate the OpenVINO IR. """ + del trainer, pl_module # `trainer` and `pl_module` variables are not used. + if self.export_dir is None or self.nncf_ctrl is None: return - os.makedirs(self.export_dir, exist_ok=True) + Path(self.export_dir).mkdir(parents=True, exist_ok=True) onnx_path = os.path.join(self.export_dir, "model_nncf.onnx") self.nncf_ctrl.export_model(onnx_path) optimize_command = "mo --input_model " + onnx_path + " --output_dir " + self.export_dir diff --git a/anomalib/utils/callbacks/nncf/utils.py b/anomalib/utils/callbacks/nncf/utils.py index b387034a67..a1aafd759f 100644 --- a/anomalib/utils/callbacks/nncf/utils.py +++ b/anomalib/utils/callbacks/nncf/utils.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from copy import copy -from typing import Any, Dict, Iterator, List, Tuple +from typing import Any, Iterator from nncf import NNCFConfig from nncf.api.compression import CompressionAlgorithmController @@ -35,11 +37,11 @@ def __next__(self) -> Any: loaded_item = next(self._data_loader_iter) return loaded_item["image"] - def get_inputs(self, dataloader_output) -> Tuple[Tuple, Dict]: + def get_inputs(self, dataloader_output) -> tuple[tuple, dict]: """Get input to model. Returns: - (dataloader_output,), {}: Tuple[Tuple, Dict]: The current model call to be made during + (dataloader_output,), {}: tuple[tuple, dict]: The current model call to be made during the initialization process """ return (dataloader_output,), {} @@ -56,8 +58,8 @@ def get_target(self, _): def wrap_nncf_model( - model: nn.Module, config: Dict, dataloader: DataLoader = None, init_state_dict: Dict = None -) -> Tuple[CompressionAlgorithmController, NNCFNetwork]: + model: nn.Module, config: dict, dataloader: DataLoader = None, init_state_dict: dict = None +) -> tuple[CompressionAlgorithmController, NNCFNetwork]: """Wrap model by NNCF. :param model: Anomalib model. @@ -95,12 +97,12 @@ def wrap_nncf_model( return nncf_ctrl, nncf_model -def is_state_nncf(state: Dict) -> bool: +def is_state_nncf(state: dict) -> bool: """The function to check if sate is the result of NNCF-compressed model.""" return bool(state.get("meta", {}).get("nncf_enable_compression", False)) -def compose_nncf_config(nncf_config: Dict, enabled_options: List[str]) -> Dict: +def compose_nncf_config(nncf_config: dict, enabled_options: list[str]) -> dict: """Compose NNCf config by selected options. :param nncf_config: diff --git a/anomalib/utils/callbacks/post_processing_configuration.py b/anomalib/utils/callbacks/post_processing_configuration.py index 9437d3ba2e..d097277687 100644 --- a/anomalib/utils/callbacks/post_processing_configuration.py +++ b/anomalib/utils/callbacks/post_processing_configuration.py @@ -4,8 +4,9 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging -from typing import Optional import torch from pytorch_lightning import Callback, LightningModule, Trainer @@ -26,22 +27,22 @@ class PostProcessingConfigurationCallback(Callback): Args: normalization_method(NormalizationMethod): Normalization method. threshold_method (ThresholdMethod): Flag indicating whether threshold should be manual or adaptive. - manual_image_threshold (Optional[float]): Default manual image threshold value. - manual_pixel_threshold (Optional[float]): Default manual pixel threshold value. + manual_image_threshold (float | None): Default manual image threshold value. + manual_pixel_threshold (float | None): Default manual pixel threshold value. """ def __init__( self, normalization_method: NormalizationMethod = NormalizationMethod.MIN_MAX, threshold_method: ThresholdMethod = ThresholdMethod.ADAPTIVE, - manual_image_threshold: Optional[float] = None, - manual_pixel_threshold: Optional[float] = None, + manual_image_threshold: float | None = None, + manual_pixel_threshold: float | None = None, ) -> None: super().__init__() self.normalization_method = normalization_method if threshold_method == ThresholdMethod.ADAPTIVE and all( - i is not None for i in [manual_image_threshold, manual_pixel_threshold] + i is not None for i in (manual_image_threshold, manual_pixel_threshold) ): raise ValueError( "When `threshold_method` is set to `adaptive`, `manual_image_threshold` and `manual_pixel_threshold` " @@ -49,7 +50,7 @@ def __init__( ) if threshold_method == ThresholdMethod.MANUAL and all( - i is None for i in [manual_image_threshold, manual_pixel_threshold] + i is None for i in (manual_image_threshold, manual_pixel_threshold) ): raise ValueError( "When `threshold_method` is set to `manual`, `manual_image_threshold` and `manual_pixel_threshold` " @@ -60,15 +61,16 @@ def __init__( self.manual_image_threshold = manual_image_threshold self.manual_pixel_threshold = manual_pixel_threshold - # pylint: disable=unused-argument - def setup(self, trainer: Trainer, pl_module: LightningModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: Trainer, pl_module: LightningModule, stage: str | None = None) -> None: """Setup post-processing configuration within Anomalib Model. Args: trainer (Trainer): PyTorch Lightning Trainer pl_module (LightningModule): Anomalib Model that inherits pl LightningModule. - stage (Optional[str], optional): fit, validate, test or predict. Defaults to None. + stage (str | None, optional): fit, validate, test or predict. Defaults to None. """ + del trainer, stage # These variables are not used. + if isinstance(pl_module, AnomalyModule): pl_module.threshold_method = self.threshold_method if pl_module.threshold_method == ThresholdMethod.MANUAL: diff --git a/anomalib/utils/callbacks/tiler_configuration.py b/anomalib/utils/callbacks/tiler_configuration.py index 227be27539..db5c5b710a 100644 --- a/anomalib/utils/callbacks/tiler_configuration.py +++ b/anomalib/utils/callbacks/tiler_configuration.py @@ -4,7 +4,9 @@ # SPDX-License-Identifier: Apache-2.0 -from typing import Optional, Sequence, Union +from __future__ import annotations + +from typing import Sequence import pytorch_lightning as pl from pytorch_lightning.callbacks import Callback @@ -23,20 +25,20 @@ class TilerConfigurationCallback(Callback): def __init__( self, enable: bool = False, - tile_size: Union[int, Sequence] = 256, - stride: Optional[Union[int, Sequence]] = None, + tile_size: int | Sequence = 256, + stride: int | Sequence | None = None, remove_border_count: int = 0, mode: str = "padding", tile_count: int = 4, - ): + ) -> None: """Sets tiling configuration from the command line. Args: enable (bool): Boolean to enable tiling operation. Defaults to False. - tile_size ([Union[int, Sequence]]): Tile size. + tile_size ([int | Sequence]): Tile size. Defaults to 256. - stride ([Union[int, Sequence]]): Stride to move tiles on the image. + stride ([int | Sequence]): Stride to move tiles on the image. remove_border_count (int, optional): Number of pixels to remove from the image before tiling. Defaults to 0. mode (str, optional): Up-scaling mode when untiling overlapping tiles. @@ -51,18 +53,20 @@ def __init__( self.mode = mode self.tile_count = tile_count - def setup(self, _trainer: pl.Trainer, pl_module: pl.LightningModule, stage: Optional[str] = None) -> None: + def setup(self, trainer: pl.Trainer, pl_module: pl.LightningModule, stage: str | None = None) -> None: """Setup Tiler object within Anomalib Model. Args: - _trainer (pl.Trainer): PyTorch Lightning Trainer + trainer (pl.Trainer): PyTorch Lightning Trainer pl_module (pl.LightningModule): Anomalib Model that inherits pl LightningModule. - stage (Optional[str], optional): fit, validate, test or predict. Defaults to None. + stage (str | None, optional): fit, validate, test or predict. Defaults to None. Raises: ValueError: When Anomalib Model doesn't contain ``Tiler`` object, it means the model doesn not support tiling operation. """ + del trainer, stage # These variables are not used. + if self.enable: if isinstance(pl_module, AnomalyModule) and hasattr(pl_module.model, "tiler"): pl_module.model.tiler = Tiler( diff --git a/anomalib/utils/callbacks/timer.py b/anomalib/utils/callbacks/timer.py index 99360ad981..17f132a43f 100644 --- a/anomalib/utils/callbacks/timer.py +++ b/anomalib/utils/callbacks/timer.py @@ -16,12 +16,11 @@ class TimerCallback(Callback): """Callback that measures the training and testing time of a PyTorch Lightning module.""" - # pylint: disable=unused-argument - def __init__(self): + def __init__(self) -> None: self.start: float self.num_images: int = 0 - def on_fit_start(self, trainer: Trainer, pl_module: LightningModule) -> None: # pylint: disable=W0613 + def on_fit_start(self, trainer: Trainer, pl_module: LightningModule) -> None: """Call when fit begins. Sets the start time to the time training started. @@ -33,6 +32,8 @@ def on_fit_start(self, trainer: Trainer, pl_module: LightningModule) -> None: # Returns: None """ + del trainer, pl_module # These variables are not used. + self.start = time.time() def on_fit_end(self, trainer: Trainer, pl_module: LightningModule) -> None: # pylint: disable=W0613 diff --git a/anomalib/utils/callbacks/visualizer/visualizer_base.py b/anomalib/utils/callbacks/visualizer/visualizer_base.py index da8df5de50..aae8329851 100644 --- a/anomalib/utils/callbacks/visualizer/visualizer_base.py +++ b/anomalib/utils/callbacks/visualizer/visualizer_base.py @@ -3,8 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path -from typing import Union, cast +from typing import cast import numpy as np import pytorch_lightning as pl @@ -33,12 +35,12 @@ def __init__( show_images: bool = False, log_images: bool = True, save_images: bool = True, - ): + ) -> None: """Visualizer callback.""" - if mode not in ["full", "simple"]: + if mode not in ("full", "simple"): raise ValueError(f"Unknown visualization mode: {mode}. Please choose one of ['full', 'simple']") self.mode = mode - if task not in [TaskType.CLASSIFICATION, TaskType.DETECTION, TaskType.SEGMENTATION]: + if task not in (TaskType.CLASSIFICATION, TaskType.DETECTION, TaskType.SEGMENTATION): raise ValueError( f"Unknown task type: {mode}. Please choose one of ['classification', 'detection', 'segmentation']" ) @@ -56,8 +58,8 @@ def _add_to_logger( image: np.ndarray, module: AnomalyModule, trainer: pl.Trainer, - filename: Union[Path, str], - ): + filename: str | Path, + ) -> None: """Log image from a visualizer to each of the available loggers in the project. Args: @@ -96,6 +98,8 @@ def on_test_end(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: trainer (pl.Trainer): Pytorch Lightning trainer pl_module (AnomalyModule): Anomaly module (unused) """ + del pl_module # `pl_module` is not used. + for logger in trainer.loggers: if isinstance(logger, AnomalibWandbLogger): logger.save() diff --git a/anomalib/utils/callbacks/visualizer/visualizer_image.py b/anomalib/utils/callbacks/visualizer/visualizer_image.py index d1d52b31ca..8ae244a066 100644 --- a/anomalib/utils/callbacks/visualizer/visualizer_image.py +++ b/anomalib/utils/callbacks/visualizer/visualizer_image.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import math from pathlib import Path -from typing import Any, Optional +from typing import Any import pytorch_lightning as pl from pytorch_lightning.utilities.cli import CALLBACK_REGISTRY @@ -29,25 +31,27 @@ class ImageVisualizerCallback(BaseVisualizerCallback): def on_predict_batch_end( self, - _trainer: pl.Trainer, - _pl_module: AnomalyModule, - outputs: Optional[STEP_OUTPUT], - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + trainer: pl.Trainer, + pl_module: AnomalyModule, + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Show images at the end of every batch. Args: - _trainer (Trainer): Pytorch lightning trainer object (unused). - _pl_module (LightningModule): Lightning modules derived from BaseAnomalyLightning object as + trainer (Trainer): Pytorch lightning trainer object (unused). + pl_module (AnomalyModule): Lightning modules derived from BaseAnomalyLightning object as currently only they support logging images. - outputs (Dict[str, Any]): Outputs of the current test step. - _batch (Any): Input batch of the current test step (unused). - _batch_idx (int): Index of the current test batch (unused). - _dataloader_idx (int): Index of the dataloader that yielded the current batch (unused). + outputs (STEP_OUTPUT | None): Outputs of the current test step. + batch (Any): Input batch of the current test step (unused). + batch_idx (int): Index of the current test batch (unused). + dataloader_idx (int): Index of the dataloader that yielded the current batch (unused). """ + del trainer, pl_module, batch, batch_idx, dataloader_idx # These variables are not used. assert outputs is not None + for i, image in enumerate(self.visualizer.visualize_batch(outputs)): filename = Path(outputs["image_path"][i]) if self.save_images: @@ -60,23 +64,25 @@ def on_test_batch_end( self, trainer: pl.Trainer, pl_module: AnomalyModule, - outputs: Optional[STEP_OUTPUT], - _batch: Any, - _batch_idx: int, - _dataloader_idx: int, + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, ) -> None: """Log images at the end of every batch. Args: trainer (Trainer): Pytorch lightning trainer object (unused). - pl_module (LightningModule): Lightning modules derived from BaseAnomalyLightning object as - currently only they support logging images. - outputs (Dict[str, Any]): Outputs of the current test step. - _batch (Any): Input batch of the current test step (unused). - _batch_idx (int): Index of the current test batch (unused). - _dataloader_idx (int): Index of the dataloader that yielded the current batch (unused). + pl_module (AnomalyModule): Lightning modules derived from BaseAnomalyLightning object as + currently only they support logging images. + outputs (STEP_OUTPUT | None): Outputs of the current test step. + batch (Any): Input batch of the current test step (unused). + batch_idx (int): Index of the current test batch (unused). + dataloader_idx (int): Index of the dataloader that yielded the current batch (unused). """ + del batch, batch_idx, dataloader_idx # These variables are not used. assert outputs is not None + for i, image in enumerate(self.visualizer.visualize_batch(outputs)): if "image_path" in outputs.keys(): filename = Path(outputs["image_path"][i]) diff --git a/anomalib/utils/callbacks/visualizer/visualizer_metric.py b/anomalib/utils/callbacks/visualizer/visualizer_metric.py index 0350cde878..428fc07d32 100644 --- a/anomalib/utils/callbacks/visualizer/visualizer_metric.py +++ b/anomalib/utils/callbacks/visualizer/visualizer_metric.py @@ -3,6 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path import numpy as np @@ -27,7 +29,7 @@ def on_test_end(self, trainer: pl.Trainer, pl_module: AnomalyModule) -> None: """Log images of the metrics contained in pl_module. In order to also plot custom metrics, they need to have implemented a `generate_figure` function that returns - Tuple[matplotlib.figure.Figure, str]. + tuple[matplotlib.figure.Figure, str]. Args: trainer (pl.Trainer): pytorch lightning trainer. diff --git a/anomalib/utils/cli/cli.py b/anomalib/utils/cli/cli.py index 14a0bc8470..86bc21f4cd 100644 --- a/anomalib/utils/cli/cli.py +++ b/anomalib/utils/cli/cli.py @@ -3,13 +3,15 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import os import warnings from datetime import datetime from importlib import import_module from pathlib import Path -from typing import Any, Callable, Dict, Optional, Type, Union +from typing import Any, Callable from omegaconf.omegaconf import OmegaConf from pytorch_lightning import LightningDataModule, LightningModule, Trainer @@ -47,19 +49,19 @@ class AnomalibCLI(LightningCLI): def __init__( # pylint: disable=too-many-function-args self, - model_class: Optional[Union[Type[LightningModule], Callable[..., LightningModule]]] = None, - datamodule_class: Optional[Union[Type[LightningDataModule], Callable[..., LightningDataModule]]] = None, - save_config_callback: Optional[Type[SaveConfigCallback]] = SaveConfigCallback, + model_class: type[LightningModule] | Callable[..., LightningModule] | None = None, + datamodule_class: type[LightningDataModule] | Callable[..., LightningDataModule] | None = None, + save_config_callback: type[SaveConfigCallback] | None = SaveConfigCallback, save_config_filename: str = "config.yaml", save_config_overwrite: bool = False, save_config_multifile: bool = False, - trainer_class: Union[Type[Trainer], Callable[..., Trainer]] = Trainer, - trainer_defaults: Optional[Dict[str, Any]] = None, - seed_everything_default: Optional[int] = None, + trainer_class: type[Trainer] | Callable[..., Trainer] = Trainer, + trainer_defaults: dict[str, Any] | None = None, + seed_everything_default: int | None = None, description: str = "Anomalib trainer command line tool", env_prefix: str = "Anomalib", env_parse: bool = False, - parser_kwargs: Optional[Union[Dict[str, Any], Dict[str, Dict[str, Any]]]] = None, + parser_kwargs: dict[str, Any] | dict[str, dict[str, Any]] | None = None, subclass_mode_model: bool = False, subclass_mode_data: bool = False, run: bool = True, @@ -147,7 +149,7 @@ def __set_default_root_dir(self) -> None: # If `resume_from_checkpoint` is not specified, it means that the project has not been created before. # Therefore, we need to create the project directory first. if config.trainer.resume_from_checkpoint is None: - root_dir = config.trainer.default_root_dir if config.trainer.default_root_dir else "./results" + root_dir = config.trainer.default_root_dir or "./results" model_name = config.model.class_path.split(".")[-1].lower() data_name = config.data.class_path.split(".")[-1].lower() category = config.data.init_args.category if "category" in config.data.init_args else "" diff --git a/anomalib/utils/hpo/config.py b/anomalib/utils/hpo/config.py index b01a831e4f..859ac17e1a 100644 --- a/anomalib/utils/hpo/config.py +++ b/anomalib/utils/hpo/config.py @@ -3,7 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List + +from __future__ import annotations from omegaconf import DictConfig @@ -18,7 +19,7 @@ def flatten_hpo_params(params_dict: DictConfig) -> DictConfig: flattened version of the parameter dictionary. """ - def process_params(nested_params: DictConfig, keys: List[str], flattened_params: DictConfig): + def process_params(nested_params: DictConfig, keys: list[str], flattened_params: DictConfig) -> None: """Flatten nested dictionary till the time it reaches the hpo params. Recursive helper function that traverses the nested config object and stores the leaf nodes in a flattened @@ -26,7 +27,7 @@ def process_params(nested_params: DictConfig, keys: List[str], flattened_params: Args: nested_params: DictConfig: config object containing the original parameters. - keys: List[str]: list of keys leading to the current location in the config. + keys: list[str]: list of keys leading to the current location in the config. flattened_params: DictConfig: Dictionary in which the flattened parameters are stored. """ if len({"values", "min", "max"}.intersection(nested_params.keys())) > 0: diff --git a/anomalib/utils/hpo/runners.py b/anomalib/utils/hpo/runners.py index d726756224..3760b2180f 100644 --- a/anomalib/utils/hpo/runners.py +++ b/anomalib/utils/hpo/runners.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Optional, Union +from __future__ import annotations import pytorch_lightning as pl from comet_ml import Optimizer @@ -34,9 +34,9 @@ class WandbSweep: def __init__( self, - config: Union[DictConfig, ListConfig], - sweep_config: Union[DictConfig, ListConfig], - entity: Optional[str] = None, + config: DictConfig | ListConfig, + sweep_config: DictConfig | ListConfig, + entity: str | None = None, ) -> None: self.config = config self.sweep_config = sweep_config @@ -47,7 +47,7 @@ def __init__( if isinstance(self.sweep_config, DictConfig): self.sweep_config.pop("observation_budget") - def run(self): + def run(self) -> None: """Run the sweep.""" flattened_hpo_params = flatten_hpo_params(self.sweep_config.parameters) self.sweep_config.parameters = flattened_hpo_params @@ -58,7 +58,7 @@ def run(self): ) wandb.agent(sweep_id, function=self.sweep, count=self.observation_budget) - def sweep(self): + def sweep(self) -> None: """Method to load the model, update config and call fit. The metrics are logged to ```wandb``` dashboard.""" wandb_logger = WandbLogger(config=flatten_sweep_params(self.sweep_config), log_model=False) sweep_config = wandb_logger.experiment.config @@ -89,15 +89,15 @@ class CometSweep: def __init__( self, - config: Union[DictConfig, ListConfig], - sweep_config: Union[DictConfig, ListConfig], - entity: Optional[str] = None, + config: DictConfig | ListConfig, + sweep_config: DictConfig | ListConfig, + entity: str | None = None, ) -> None: self.config = config self.sweep_config = sweep_config self.entity = entity - def run(self): + def run(self) -> None: """Run the sweep.""" flattened_hpo_params = flatten_hpo_params(self.sweep_config.parameters) self.sweep_config.parameters = flattened_hpo_params diff --git a/anomalib/utils/loggers/__init__.py b/anomalib/utils/loggers/__init__.py index e43218088c..98a4caa0af 100644 --- a/anomalib/utils/loggers/__init__.py +++ b/anomalib/utils/loggers/__init__.py @@ -3,10 +3,13 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging import os import warnings -from typing import Iterable, List, Union +from pathlib import Path +from typing import Iterable from omegaconf.dictconfig import DictConfig from omegaconf.listconfig import ListConfig @@ -35,11 +38,11 @@ class UnknownLogger(Exception): """This is raised when the logger option in `config.yaml` file is set incorrectly.""" -def configure_logger(level: Union[int, str] = logging.INFO): +def configure_logger(level: int | str = logging.INFO) -> None: """Get console logger by name. Args: - level (Union[int, str], optional): Logger Level. Defaults to logging.INFO. + level (int | str, optional): Logger Level. Defaults to logging.INFO. Returns: Logger: The expected logger. @@ -58,8 +61,8 @@ def configure_logger(level: Union[int, str] = logging.INFO): def get_experiment_logger( - config: Union[DictConfig, ListConfig] -) -> Union[LightningLoggerBase, Iterable[LightningLoggerBase], bool]: + config: DictConfig | ListConfig, +) -> LightningLoggerBase | Iterable[LightningLoggerBase] | bool: """Return a logger based on the choice of logger in the config file. Args: @@ -69,7 +72,7 @@ def get_experiment_logger( ValueError: for any logger types apart from false and tensorboard Returns: - Union[LightningLoggerBase, Iterable[LightningLoggerBase], bool]: Logger + LightningLoggerBase | Iterable[LightningLoggerBase] | bool]: Logger """ logger.info("Loading the experiment logger(s)") @@ -85,10 +88,10 @@ def get_experiment_logger( else: config.logging.logger = config.project.logger - if config.logging.logger in [None, False]: + if config.logging.logger in (None, False): return False - logger_list: List[LightningLoggerBase] = [] + logger_list: list[LightningLoggerBase] = [] if isinstance(config.logging.logger, str): config.logging.logger = [config.logging.logger] @@ -103,7 +106,7 @@ def get_experiment_logger( ) elif experiment_logger == "wandb": wandb_logdir = os.path.join(config.project.path, "logs") - os.makedirs(wandb_logdir, exist_ok=True) + Path(wandb_logdir).mkdir(parents=True, exist_ok=True) name = ( config.model.name if "category" not in config.dataset.keys() @@ -118,7 +121,7 @@ def get_experiment_logger( ) elif experiment_logger == "comet": comet_logdir = os.path.join(config.project.path, "logs") - os.makedirs(comet_logdir, exist_ok=True) + Path(comet_logdir).mkdir(parents=True, exist_ok=True) run_name = ( config.model.name if "category" not in config.dataset.keys() diff --git a/anomalib/utils/loggers/base.py b/anomalib/utils/loggers/base.py index 7bbc756de0..91f3607a89 100644 --- a/anomalib/utils/loggers/base.py +++ b/anomalib/utils/loggers/base.py @@ -3,8 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from abc import abstractmethod -from typing import Any, Optional, Union +from typing import Any import numpy as np from matplotlib.figure import Figure @@ -14,6 +16,6 @@ class ImageLoggerBase: """Adds a common interface for logging the images.""" @abstractmethod - def add_image(self, image: Union[np.ndarray, Figure], name: Optional[str] = None, **kwargs: Any) -> None: + def add_image(self, image: np.ndarray | Figure, name: str | None = None, **kwargs: Any) -> None: """Interface to log images in the respective loggers.""" raise NotImplementedError() diff --git a/anomalib/utils/loggers/comet.py b/anomalib/utils/loggers/comet.py index 6ca449309f..8daf40cd6b 100644 --- a/anomalib/utils/loggers/comet.py +++ b/anomalib/utils/loggers/comet.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Optional, Union +from __future__ import annotations + +from typing import Any import numpy as np from matplotlib.figure import Figure @@ -69,15 +71,15 @@ class AnomalibCometLogger(ImageLoggerBase, CometLogger): def __init__( self, - api_key: Optional[str] = None, - save_dir: Optional[str] = None, - project_name: Optional[str] = None, - rest_api_key: Optional[str] = None, - experiment_name: Optional[str] = None, - experiment_key: Optional[str] = None, + api_key: str | None = None, + save_dir: str | None = None, + project_name: str | None = None, + rest_api_key: str | None = None, + experiment_name: str | None = None, + experiment_key: str | None = None, offline: bool = False, prefix: str = "", - **kwargs + **kwargs, ) -> None: super().__init__( api_key=api_key, @@ -88,17 +90,17 @@ def __init__( experiment_key=experiment_key, offline=offline, prefix=prefix, - **kwargs + **kwargs, ) self.experiment.log_other("Created from", "Anomalib") @rank_zero_only - def add_image(self, image: Union[np.ndarray, Figure], name: Optional[str] = None, **kwargs: Any): + def add_image(self, image: np.ndarray | Figure, name: str | None = None, **kwargs: Any) -> None: """Interface to add image to comet logger. Args: - image (Union[np.ndarray, Figure]): Image to log - name (Optional[str]): The tag of the image + image (np.ndarray | Figure): Image to log + name (str | None): The tag of the image kwargs: Accepts only `global_step` (int). The step at which to log the image. """ if "global_step" not in kwargs: diff --git a/anomalib/utils/loggers/tensorboard.py b/anomalib/utils/loggers/tensorboard.py index bdf620b3fc..74137e19bb 100644 --- a/anomalib/utils/loggers/tensorboard.py +++ b/anomalib/utils/loggers/tensorboard.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Optional, Union +from __future__ import annotations + +from typing import Any import numpy as np from matplotlib.figure import Figure @@ -52,12 +54,12 @@ class AnomalibTensorBoardLogger(ImageLoggerBase, TensorBoardLogger): def __init__( self, save_dir: str, - name: Optional[str] = "default", - version: Optional[Union[int, str]] = None, + name: str | None = "default", + version: int | str | None = None, log_graph: bool = False, default_hp_metric: bool = True, prefix: str = "", - **kwargs + **kwargs, ): super().__init__( save_dir, @@ -66,16 +68,16 @@ def __init__( log_graph=log_graph, default_hp_metric=default_hp_metric, prefix=prefix, - **kwargs + **kwargs, ) @rank_zero_only - def add_image(self, image: Union[np.ndarray, Figure], name: Optional[str] = None, **kwargs: Any): + def add_image(self, image: np.ndarray | Figure, name: str | None = None, **kwargs: Any): """Interface to add image to tensorboard logger. Args: - image (Union[np.ndarray, Figure]): Image to log - name (Optional[str]): The tag of the image + image (np.ndarray | Figure): Image to log + name (str | None): The tag of the image kwargs: Accepts only `global_step` (int). The step at which to log the image. """ if "global_step" not in kwargs: diff --git a/anomalib/utils/loggers/wandb.py b/anomalib/utils/loggers/wandb.py index 416a3a5c15..6260337adb 100644 --- a/anomalib/utils/loggers/wandb.py +++ b/anomalib/utils/loggers/wandb.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, List, Optional, Union +from __future__ import annotations + +from typing import Any import numpy as np from matplotlib.figure import Figure @@ -68,17 +70,17 @@ class AnomalibWandbLogger(ImageLoggerBase, WandbLogger): def __init__( self, - name: Optional[str] = None, - save_dir: Optional[str] = None, - offline: Optional[bool] = False, - id: Optional[str] = None, # kept to match wandb init pylint: disable=redefined-builtin - anonymous: Optional[bool] = None, - version: Optional[str] = None, - project: Optional[str] = None, - log_model: Union[str, bool] = False, + name: str | None = None, + save_dir: str | None = None, + offline: bool | None = False, + id: str | None = None, # kept to match wandb init pylint: disable=redefined-builtin + anonymous: bool | None = None, + version: str | None = None, + project: str | None = None, + log_model: str | bool = False, experiment=None, - prefix: Optional[str] = "", - **kwargs + prefix: str | None = "", + **kwargs, ) -> None: super().__init__( name=name, @@ -91,17 +93,17 @@ def __init__( log_model=log_model, experiment=experiment, prefix=prefix, - **kwargs + **kwargs, ) - self.image_list: List[wandb.Image] = [] # Cache images + self.image_list: list[wandb.Image] = [] # Cache images @rank_zero_only - def add_image(self, image: Union[np.ndarray, Figure], name: Optional[str] = None, **kwargs: Any): + def add_image(self, image: np.ndarray | Figure, name: str | None = None, **kwargs: Any): """Interface to add image to wandb logger. Args: - image (Union[np.ndarray, Figure]): Image to log - name (Optional[str]): The tag of the image + image (np.ndarray | Figure): Image to log + name (str | None): The tag of the image """ image = wandb.Image(image, caption=name) self.image_list.append(image) diff --git a/anomalib/utils/metrics/__init__.py b/anomalib/utils/metrics/__init__.py index e1fd7541fb..c8b7e84c3a 100644 --- a/anomalib/utils/metrics/__init__.py +++ b/anomalib/utils/metrics/__init__.py @@ -3,9 +3,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import importlib import warnings -from typing import Any, Dict, List, Optional, Union +from typing import Any import torchmetrics from omegaconf import DictConfig, ListConfig @@ -23,15 +25,15 @@ __all__ = ["AUROC", "AUPR", "AUPRO", "OptimalF1", "AnomalyScoreThreshold", "AnomalyScoreDistribution", "MinMax", "PRO"] -def metric_collection_from_names(metric_names: List[str], prefix: Optional[str]) -> AnomalibMetricCollection: +def metric_collection_from_names(metric_names: list[str], prefix: str | None) -> AnomalibMetricCollection: """Create a metric collection from a list of metric names. The function will first try to retrieve the metric from the metrics defined in Anomalib metrics module, then in TorchMetrics package. Args: - metric_names (List[str]): List of metric names to be included in the collection. - prefix (Optional[str]): prefix to assign to the metrics in the collection. + metric_names (list[str]): List of metric names to be included in the collection. + prefix (str | None): prefix to assign to the metrics in the collection. Returns: AnomalibMetricCollection: Collection of metrics. @@ -53,7 +55,7 @@ def metric_collection_from_names(metric_names: List[str], prefix: Optional[str]) return metrics -def _validate_metrics_dict(metrics: Dict[str, Dict[str, Any]]) -> None: +def _validate_metrics_dict(metrics: dict[str, dict[str, Any]]) -> None: """Check the assumptions about metrics config dict. - Keys are metric names @@ -90,7 +92,7 @@ def _get_class_from_path(class_path: str) -> Any: return cls -def metric_collection_from_dicts(metrics: Dict[str, Dict[str, Any]], prefix: Optional[str]) -> AnomalibMetricCollection: +def metric_collection_from_dicts(metrics: dict[str, dict[str, Any]], prefix: str | None) -> AnomalibMetricCollection: """Create a metric collection from a dict of "metric name" -> "metric specifications". Example: @@ -123,11 +125,11 @@ def metric_collection_from_dicts(metrics: Dict[str, Dict[str, Any]], prefix: Opt ``` Args: - metrics (Dict[str, Dict[str, Any]]): keys are metric names, values are dictionaries. - Internal Dict[str, Any] keys are "class_path" (value is string) and "init_args" (value is dict), + metrics (dict[str, dict[str, Any]]): keys are metric names, values are dictionaries. + Internal dict[str, Any] keys are "class_path" (value is string) and "init_args" (value is dict), following the convention in Pytorch Lightning CLI. - prefix (Optional[str]): prefix to assign to the metrics in the collection. + prefix (str | None): prefix to assign to the metrics in the collection. Returns: AnomalibMetricCollection: Collection of metrics. @@ -143,21 +145,21 @@ def metric_collection_from_dicts(metrics: Dict[str, Dict[str, Any]], prefix: Opt def create_metric_collection( - metrics: Union[List[str], Dict[str, Dict[str, Any]]], prefix: Optional[str] + metrics: list[str] | dict[str, dict[str, Any]], prefix: str | None ) -> AnomalibMetricCollection: """Create a metric collection from a list of metric names or dictionaries. This function will dispatch the actual creation to the appropriate function depending on the input type: - - if List[str] (names of metrics): see `metric_collection_from_names` - - if Dict[str, Dict[str, Any]] (path and init args of a class): see `metric_collection_from_dicts` + - if list[str] (names of metrics): see `metric_collection_from_names` + - if dict[str, dict[str, Any]] (path and init args of a class): see `metric_collection_from_dicts` The function will first try to retrieve the metric from the metrics defined in Anomalib metrics module, then in TorchMetrics package. Args: - metrics (Union[List[str], Dict[str, Dict[str, Any]]]). - prefix (Optional[str]): prefix to assign to the metrics in the collection. + metrics (list[str] | dict[str, dict[str, Any]]). + prefix (str | None): prefix to assign to the metrics in the collection. Returns: AnomalibMetricCollection: Collection of metrics. diff --git a/anomalib/utils/metrics/anomaly_score_distribution.py b/anomalib/utils/metrics/anomaly_score_distribution.py index eb1af5a905..4aed1e5bf7 100644 --- a/anomalib/utils/metrics/anomaly_score_distribution.py +++ b/anomalib/utils/metrics/anomaly_score_distribution.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Optional, Tuple +from __future__ import annotations import torch from torch import Tensor @@ -13,10 +13,10 @@ class AnomalyScoreDistribution(Metric): """Mean and standard deviation of the anomaly scores of normal training data.""" - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.anomaly_maps = [] - self.anomaly_scores = [] + self.anomaly_maps: list[Tensor] = [] + self.anomaly_scores: list[Tensor] = [] self.add_state("image_mean", torch.empty(0), persistent=True) self.add_state("image_std", torch.empty(0), persistent=True) @@ -28,17 +28,16 @@ def __init__(self, **kwargs): self.pixel_mean = torch.empty(0) self.pixel_std = torch.empty(0) - # pylint: disable=arguments-differ - def update( # type: ignore - self, anomaly_scores: Optional[Tensor] = None, anomaly_maps: Optional[Tensor] = None - ) -> None: + def update(self, *args, anomaly_scores: Tensor | None = None, anomaly_maps: Tensor | None = None, **kwargs) -> None: """Update the precision-recall curve metric.""" + del args, kwargs # These variables are not used. + if anomaly_maps is not None: self.anomaly_maps.append(anomaly_maps) if anomaly_scores is not None: self.anomaly_scores.append(anomaly_scores) - def compute(self) -> Tuple[Tensor, Tensor, Tensor, Tensor]: + def compute(self) -> tuple[Tensor, Tensor, Tensor, Tensor]: """Compute stats.""" anomaly_scores = torch.hstack(self.anomaly_scores) anomaly_scores = torch.log(anomaly_scores) diff --git a/anomalib/utils/metrics/anomaly_score_threshold.py b/anomalib/utils/metrics/anomaly_score_threshold.py index 0a16c35f00..118ace3711 100644 --- a/anomalib/utils/metrics/anomaly_score_threshold.py +++ b/anomalib/utils/metrics/anomaly_score_threshold.py @@ -3,9 +3,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import warnings import torch +from torch import Tensor from torchmetrics import PrecisionRecallCurve @@ -21,13 +24,13 @@ class AnomalyScoreThreshold(PrecisionRecallCurve): adaptive threshold value. """ - def __init__(self, default_value: float = 0.5, **kwargs): + def __init__(self, default_value: float = 0.5, **kwargs) -> None: super().__init__(num_classes=1, **kwargs) self.add_state("value", default=torch.tensor(default_value), persistent=True) # pylint: disable=not-callable self.value = torch.tensor(default_value) # pylint: disable=not-callable - def compute(self) -> torch.Tensor: + def compute(self) -> Tensor: """Compute the threshold that yields the optimal F1 score. Compute the F1 scores while varying the threshold. Store the optimal @@ -36,9 +39,9 @@ def compute(self) -> torch.Tensor: Returns: Value of the F1 score at the optimal threshold. """ - precision: torch.Tensor - recall: torch.Tensor - thresholds: torch.Tensor + precision: Tensor + recall: Tensor + thresholds: Tensor if not any(1 in batch for batch in self.target): warnings.warn( diff --git a/anomalib/utils/metrics/aupr.py b/anomalib/utils/metrics/aupr.py index f567444b10..813aab6115 100644 --- a/anomalib/utils/metrics/aupr.py +++ b/anomalib/utils/metrics/aupr.py @@ -3,7 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple + +from __future__ import annotations import torch from matplotlib.figure import Figure @@ -44,7 +45,7 @@ def update(self, preds: Tensor, target: Tensor) -> None: # type: ignore """ super().update(preds.flatten(), target.flatten()) - def _compute(self) -> Tuple[Tensor, Tensor]: + def _compute(self) -> tuple[Tensor, Tensor]: """Compute prec/rec value pairs. Returns: @@ -55,11 +56,11 @@ def _compute(self) -> Tuple[Tensor, Tensor]: prec, rec, _ = super().compute() return (prec, rec) - def generate_figure(self) -> Tuple[Figure, str]: + def generate_figure(self) -> tuple[Figure, str]: """Generate a figure containing the PR curve as well as the random baseline and the AUC. Returns: - Tuple[Figure, str]: Tuple containing both the PR curve and the figure title to be used for logging + tuple[Figure, str]: Tuple containing both the PR curve and the figure title to be used for logging """ prec, rec = self._compute() aupr = self.compute() diff --git a/anomalib/utils/metrics/aupro.py b/anomalib/utils/metrics/aupro.py index 58f69d4e3d..7b7fa17d04 100644 --- a/anomalib/utils/metrics/aupro.py +++ b/anomalib/utils/metrics/aupro.py @@ -3,7 +3,9 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Callable, List, Optional, Tuple +from __future__ import annotations + +from typing import Any, Callable import torch from matplotlib.figure import Figure @@ -24,16 +26,16 @@ class AUPRO(Metric): """Area under per region overlap (AUPRO) Metric.""" is_differentiable: bool = False - higher_is_better: Optional[bool] = None + higher_is_better: bool | None = None full_state_update: bool = False - preds: List[Tensor] - target: List[Tensor] + preds: list[Tensor] + target: list[Tensor] def __init__( self, compute_on_step: bool = True, dist_sync_on_step: bool = False, - process_group: Optional[Any] = None, + process_group: Any | None = None, dist_sync_fn: Callable = None, fpr_limit: float = 0.3, ) -> None: @@ -58,7 +60,7 @@ def update(self, preds: Tensor, target: Tensor) -> None: # type: ignore self.target.append(target) self.preds.append(preds) - def _compute(self) -> Tuple[Tensor, Tensor]: + def _compute(self) -> tuple[Tensor, Tensor]: """Compute the pro/fpr value-pairs until the fpr specified by self.fpr_limit. It leverages the fact that the overlap corresponds to the tpr, and thus computes the overall @@ -69,7 +71,7 @@ def _compute(self) -> Tuple[Tensor, Tensor]: connected component analysis. Returns: - Tuple[Tensor, Tensor]: tuple containing final fpr and tpr values. + tuple[Tensor, Tensor]: tuple containing final fpr and tpr values. """ target = dim_zero_cat(self.target) preds = dim_zero_cat(self.preds) @@ -77,10 +79,8 @@ def _compute(self) -> Tuple[Tensor, Tensor]: # check and prepare target for labeling via kornia if target.min() < 0 or target.max() > 1: raise ValueError( - ( - f"kornia.contrib.connected_components expects input to lie in the interval [0, 1], but found " - f"interval was [{target.min()}, {target.max()}]." - ) + f"kornia.contrib.connected_components expects input to lie in the interval [0, 1], but found " + f"interval was [{target.min()}, {target.max()}]." ) target = target.unsqueeze(1) # kornia expects N1HW format target = target.type(torch.float) # kornia expects FloatTensor @@ -167,11 +167,11 @@ def compute(self) -> Tensor: return aupro - def generate_figure(self) -> Tuple[Figure, str]: + def generate_figure(self) -> tuple[Figure, str]: """Generate a figure containing the PRO curve and the AUPRO. Returns: - Tuple[Figure, str]: Tuple containing both the figure and the figure title to be used for logging + tuple[Figure, str]: Tuple containing both the figure and the figure title to be used for logging """ fpr, tpr = self._compute() aupro = self.compute() diff --git a/anomalib/utils/metrics/auroc.py b/anomalib/utils/metrics/auroc.py index 60889d6be9..7cbc62976a 100644 --- a/anomalib/utils/metrics/auroc.py +++ b/anomalib/utils/metrics/auroc.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple +from __future__ import annotations import torch from matplotlib.figure import Figure @@ -43,7 +43,7 @@ def update(self, preds: Tensor, target: Tensor) -> None: # type: ignore """ super().update(preds.flatten(), target.flatten()) - def _compute(self) -> Tuple[Tensor, Tensor]: + def _compute(self) -> tuple[Tensor, Tensor]: """Compute fpr/tpr value pairs. Returns: @@ -54,11 +54,11 @@ def _compute(self) -> Tuple[Tensor, Tensor]: fpr, tpr, _thresholds = super().compute() return (fpr, tpr) - def generate_figure(self) -> Tuple[Figure, str]: + def generate_figure(self) -> tuple[Figure, str]: """Generate a figure containing the ROC curve, the baseline and the AUROC. Returns: - Tuple[Figure, str]: Tuple containing both the figure and the figure title to be used for logging + tuple[Figure, str]: Tuple containing both the figure and the figure title to be used for logging """ fpr, tpr = self._compute() auroc = self.compute() diff --git a/anomalib/utils/metrics/collection.py b/anomalib/utils/metrics/collection.py index ecf2aa779f..7b86bd6fbd 100644 --- a/anomalib/utils/metrics/collection.py +++ b/anomalib/utils/metrics/collection.py @@ -9,12 +9,12 @@ class AnomalibMetricCollection(MetricCollection): """Extends the MetricCollection class for use in the Anomalib pipeline.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._update_called = False self._threshold = 0.5 - def set_threshold(self, threshold_value): + def set_threshold(self, threshold_value) -> None: """Update the threshold value for all metrics that have the threshold attribute.""" self._threshold = threshold_value for metric in self.values(): diff --git a/anomalib/utils/metrics/min_max.py b/anomalib/utils/metrics/min_max.py index 2cf0fd09b8..ce0b98e1df 100644 --- a/anomalib/utils/metrics/min_max.py +++ b/anomalib/utils/metrics/min_max.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple +from __future__ import annotations import torch from torch import Tensor @@ -15,7 +15,7 @@ class MinMax(Metric): full_state_update: bool = True - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.add_state("min", torch.tensor(float("inf")), persistent=True) # pylint: disable=not-callable self.add_state("max", torch.tensor(float("-inf")), persistent=True) # pylint: disable=not-callable @@ -23,12 +23,13 @@ def __init__(self, **kwargs): self.min = torch.tensor(float("inf")) # pylint: disable=not-callable self.max = torch.tensor(float("-inf")) # pylint: disable=not-callable - # pylint: disable=arguments-differ - def update(self, predictions: Tensor) -> None: # type: ignore + def update(self, predictions: Tensor, *args, **kwargs) -> None: """Update the min and max values.""" + del args, kwargs # These variables are not used. + self.max = torch.max(self.max, torch.max(predictions)) self.min = torch.min(self.min, torch.min(predictions)) - def compute(self) -> Tuple[Tensor, Tensor]: + def compute(self) -> tuple[Tensor, Tensor]: """Return min and max values.""" return self.min, self.max diff --git a/anomalib/utils/metrics/optimal_f1.py b/anomalib/utils/metrics/optimal_f1.py index c091fc1894..18af32b892 100644 --- a/anomalib/utils/metrics/optimal_f1.py +++ b/anomalib/utils/metrics/optimal_f1.py @@ -6,6 +6,7 @@ import warnings import torch +from torch import Tensor from torchmetrics import Metric, PrecisionRecallCurve @@ -18,7 +19,7 @@ class OptimalF1(Metric): full_state_update: bool = False - def __init__(self, num_classes: int, **kwargs): + def __init__(self, num_classes: int, **kwargs) -> None: warnings.warn( DeprecationWarning( "OptimalF1 metric is deprecated and will be removed in a future release. The optimal F1 score for " @@ -30,14 +31,15 @@ def __init__(self, num_classes: int, **kwargs): self.precision_recall_curve = PrecisionRecallCurve(num_classes=num_classes) - self.threshold: torch.Tensor + self.threshold: Tensor - # pylint: disable=arguments-differ - def update(self, preds: torch.Tensor, target: torch.Tensor) -> None: # type: ignore + def update(self, preds: Tensor, target: Tensor, *args, **kwargs) -> None: """Update the precision-recall curve metric.""" + del args, kwargs # These variables are not used. + self.precision_recall_curve.update(preds, target) - def compute(self) -> torch.Tensor: + def compute(self) -> Tensor: """Compute the value of the optimal F1 score. Compute the F1 scores while varying the threshold. Store the optimal @@ -46,9 +48,9 @@ def compute(self) -> torch.Tensor: Returns: Value of the F1 score at the optimal threshold. """ - precision: torch.Tensor - recall: torch.Tensor - thresholds: torch.Tensor + precision: Tensor + recall: Tensor + thresholds: Tensor precision, recall, thresholds = self.precision_recall_curve.compute() f1_score = (2 * precision * recall) / (precision + recall + 1e-10) diff --git a/anomalib/utils/metrics/plotting_utils.py b/anomalib/utils/metrics/plotting_utils.py index 7162c55808..f6ec537ecc 100644 --- a/anomalib/utils/metrics/plotting_utils.py +++ b/anomalib/utils/metrics/plotting_utils.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import Tuple +from __future__ import annotations import torch from matplotlib import pyplot as plt @@ -16,14 +16,14 @@ def plot_figure( x_vals: Tensor, y_vals: Tensor, auc: Tensor, - xlim: Tuple[float, float], - ylim: Tuple[float, float], + xlim: tuple[float, float], + ylim: tuple[float, float], xlabel: str, ylabel: str, loc: str, title: str, sample_points: int = 1000, -) -> Tuple[Figure, Axis]: +) -> tuple[Figure, Axis]: """Generate a simple, ROC-style plot, where x_vals is plotted against y_vals. Note that a subsampling is applied if > sample_points are present in x/y, as matplotlib plotting draws @@ -33,8 +33,8 @@ def plot_figure( x_vals (Tensor): x values to plot y_vals (Tensor): y values to plot auc (Tensor): normalized area under the curve spanned by x_vals, y_vals - xlim (Tuple[float, float]): displayed range for x-axis - ylim (Tuple[float, float]): displayed range for y-axis + xlim (tuple[float, float]): displayed range for x-axis + ylim (tuple[float, float]): displayed range for y-axis xlabel (str): label of x axis ylabel (str): label of y axis loc (str): string-based legend location, for details see @@ -43,7 +43,7 @@ def plot_figure( sample_points (int): number of sampling points to subsample x_vals/y_vals with Returns: - Tuple[Figure, Axis]: Figure and the contained Axis + tuple[Figure, Axis]: Figure and the contained Axis """ fig, axis = plt.subplots() diff --git a/anomalib/utils/metrics/pro.py b/anomalib/utils/metrics/pro.py index a3f8922c4f..18c9069414 100644 --- a/anomalib/utils/metrics/pro.py +++ b/anomalib/utils/metrics/pro.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -from typing import List +from __future__ import annotations import torch from torch import Tensor @@ -17,8 +17,8 @@ class PRO(Metric): """Per-Region Overlap (PRO) Score.""" - target: List[Tensor] - preds: List[Tensor] + target: list[Tensor] + preds: list[Tensor] def __init__(self, threshold: float = 0.5, **kwargs) -> None: super().__init__(**kwargs) diff --git a/anomalib/utils/sweep/config.py b/anomalib/utils/sweep/config.py index b3a1a369a3..d8a0fdf7ae 100644 --- a/anomalib/utils/sweep/config.py +++ b/anomalib/utils/sweep/config.py @@ -3,16 +3,18 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import itertools import operator from collections.abc import Iterable, ValuesView from functools import reduce -from typing import Any, Generator, List, Tuple +from typing import Any, Generator from omegaconf import DictConfig -def convert_to_tuple(values: ValuesView) -> List[Tuple]: +def convert_to_tuple(values: ValuesView) -> list[tuple]: """Converts a ValuesView object to a list of tuples. This is useful to get list of possible values for each parameter in the config and a tuple for values that are @@ -36,7 +38,7 @@ def convert_to_tuple(values: ValuesView) -> List[Tuple]: values: ValuesView: ValuesView object to be converted to a list of tuples. Returns: - List[Tuple]: List of tuples. + list[Tuple]: List of tuples. """ return_list = [] for value in values: @@ -63,7 +65,7 @@ def flatten_sweep_params(params_dict: DictConfig) -> DictConfig: flattened version of the parameter dictionary. """ - def flatten_nested_dict(nested_params: DictConfig, keys: List[str], flattened_params: DictConfig): + def flatten_nested_dict(nested_params: DictConfig, keys: list[str], flattened_params: DictConfig) -> None: """Flatten nested dictionary. Recursive helper function that traverses the nested config object and stores the leaf nodes in a flattened @@ -71,7 +73,7 @@ def flatten_nested_dict(nested_params: DictConfig, keys: List[str], flattened_pa Args: nested_params: DictConfig: config object containing the original parameters. - keys: List[str]: list of keys leading to the current location in the config. + keys: list[str]: list of keys leading to the current location in the config. flattened_params: DictConfig: Dictionary in which the flattened parameters are stored. """ for name, cfg in nested_params.items(): @@ -123,22 +125,22 @@ def get_run_config(params_dict: DictConfig) -> Generator[DictConfig, None, None] yield run_config -def get_from_nested_config(config: DictConfig, keymap: List) -> Any: +def get_from_nested_config(config: DictConfig, keymap: list) -> Any: """Retrieves an item from a nested config object using a list of keys. Args: config: DictConfig: nested DictConfig object - keymap: List[str]: list of keys corresponding to item that should be retrieved. + keymap: list[str]: list of keys corresponding to item that should be retrieved. """ return reduce(operator.getitem, keymap, config) -def set_in_nested_config(config: DictConfig, keymap: List, value: Any): +def set_in_nested_config(config: DictConfig, keymap: list, value: Any) -> None: """Set an item in a nested config object using a list of keys. Args: config: DictConfig: nested DictConfig object - keymap: List[str]: list of keys corresponding to item that should be set. + keymap: list[str]: list of keys corresponding to item that should be set. value: Any: Value that should be assigned to the dictionary item at the specified location. Example: diff --git a/anomalib/utils/sweep/helpers/callbacks.py b/anomalib/utils/sweep/helpers/callbacks.py index 62dacda2d1..fd26d7fb6e 100644 --- a/anomalib/utils/sweep/helpers/callbacks.py +++ b/anomalib/utils/sweep/helpers/callbacks.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 -from typing import List, Union +from __future__ import annotations from omegaconf import DictConfig, ListConfig from pytorch_lightning import Callback @@ -16,16 +16,16 @@ from anomalib.utils.callbacks.timer import TimerCallback -def get_sweep_callbacks(config: Union[ListConfig, DictConfig]) -> List[Callback]: +def get_sweep_callbacks(config: DictConfig | ListConfig) -> list[Callback]: """Gets callbacks relevant to sweep. Args: - config (Union[DictConfig, ListConfig]): Model config loaded from anomalib + config (DictConfig | ListConfig): Model config loaded from anomalib Returns: - List[Callback]: List of callbacks + list[Callback]: List of callbacks """ - callbacks: List[Callback] = [TimerCallback()] + callbacks: list[Callback] = [TimerCallback()] # Add metric configuration to the model via MetricsConfigurationCallback # TODO: Remove this once the old CLI is deprecated. diff --git a/anomalib/utils/sweep/helpers/inference.py b/anomalib/utils/sweep/helpers/inference.py index 78906a8880..b53eb35906 100644 --- a/anomalib/utils/sweep/helpers/inference.py +++ b/anomalib/utils/sweep/helpers/inference.py @@ -3,9 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import time from pathlib import Path -from typing import Union import torch from omegaconf import DictConfig, ListConfig @@ -15,13 +16,11 @@ from anomalib.models.components import AnomalyModule -def get_torch_throughput( - config: Union[DictConfig, ListConfig], model: AnomalyModule, test_dataset: DataLoader -) -> float: +def get_torch_throughput(config: DictConfig | ListConfig, model: AnomalyModule, test_dataset: DataLoader) -> float: """Tests the model on dummy data. Images are passed sequentially to make the comparision with OpenVINO model fair. Args: - config (Union[DictConfig, ListConfig]): Model config. + config (DictConfig | ListConfig): Model config. model (Path): Model on which inference is called. test_dataset (DataLoader): The test dataset used as a reference for the mock dataset. @@ -48,11 +47,11 @@ def get_torch_throughput( return throughput -def get_openvino_throughput(config: Union[DictConfig, ListConfig], model_path: Path, test_dataset: DataLoader) -> float: +def get_openvino_throughput(config: DictConfig | ListConfig, model_path: Path, test_dataset: DataLoader) -> float: """Runs the generated OpenVINO model on a dummy dataset to get throughput. Args: - config (Union[DictConfig, ListConfig]): Model config. + config (DictConfig | ListConfig): Model config. model_path (Path): Path to folder containing the OpenVINO models. It then searches `model.xml` in the folder. test_dataset (DataLoader): The test dataset used as a reference for the mock dataset. diff --git a/notebooks/000_getting_started/001_getting_started.ipynb b/notebooks/000_getting_started/001_getting_started.ipynb index f30fddff2d..3a8f817cd6 100644 --- a/notebooks/000_getting_started/001_getting_started.ipynb +++ b/notebooks/000_getting_started/001_getting_started.ipynb @@ -106,8 +106,10 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", "from pathlib import Path\n", - "from typing import Any, Dict\n", + "from typing import Any\n", "\n", "import numpy as np\n", "from IPython.display import display\n", @@ -241,7 +243,7 @@ "metadata": {}, "outputs": [], "source": [ - "def show_image_and_mask(sample: Dict[str, Any], index: int) -> Image:\n", + "def show_image_and_mask(sample: dict[str, Any], index: int) -> Image:\n", " img = ToPILImage()(Denormalize()(sample[\"image\"][index].clone()))\n", " msk = ToPILImage()(sample[\"mask\"][index]).convert(\"RGB\")\n", "\n", @@ -365,7 +367,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.8.13 (default, Oct 21 2022, 23:50:54) \n[GCC 11.2.0]" }, "orig_nbformat": 4, "vscode": { diff --git a/setup.py b/setup.py index 881015130c..1d93d3058a 100644 --- a/setup.py +++ b/setup.py @@ -3,14 +3,16 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from typing import List +from types import ModuleType from setuptools import find_packages, setup -def load_module(name: str = "anomalib/__init__.py"): +def load_module(name: str = "anomalib/__init__.py") -> ModuleType: """Load Python Module. Args: @@ -48,13 +50,13 @@ def get_version() -> str: return version -def get_required_packages(requirement_files: List[str]) -> List[str]: +def get_required_packages(requirement_files: list[str]) -> list[str]: """Get packages from requirements.txt file. This function returns list of required packages from requirement files. Args: - requirement_files (List[str]): txt files that contains list of required + requirement_files (list[str]): txt files that contains list of required packages. Example: @@ -62,13 +64,13 @@ def get_required_packages(requirement_files: List[str]) -> List[str]: ['onnx>=1.8.1', 'networkx~=2.5', 'openvino-dev==2021.4.1', ...] Returns: - List[str]: List of required packages + list[str]: List of required packages """ - required_packages: List[str] = [] + required_packages: list[str] = [] for requirement_file in requirement_files: - with open(f"requirements/{requirement_file}.txt", "r", encoding="utf8") as file: + with open(f"requirements/{requirement_file}.txt", encoding="utf8") as file: for line in file: package = line.strip() if package and not package.startswith(("#", "-f")): diff --git a/tests/helpers/detection.py b/tests/helpers/detection.py index 10de219e11..7f16b38d7b 100644 --- a/tests/helpers/detection.py +++ b/tests/helpers/detection.py @@ -1,6 +1,6 @@ """Helpers for detection tests.""" import os -import xml.etree.cElementTree as ET # nosec +import xml.etree.ElementTree as ET # nosec from glob import glob from typing import List, Tuple diff --git a/tests/pre_merge/utils/callbacks/export_callback/dummy_lightning_model.py b/tests/pre_merge/utils/callbacks/export_callback/dummy_lightning_model.py index fe7a338565..075a4327b5 100644 --- a/tests/pre_merge/utils/callbacks/export_callback/dummy_lightning_model.py +++ b/tests/pre_merge/utils/callbacks/export_callback/dummy_lightning_model.py @@ -18,7 +18,7 @@ class FakeDataModule(pl.LightningDataModule): def __init__(self, batch_size: int = 32): - super(FakeDataModule, self).__init__() + super().__init__() self.batch_size = batch_size self.pre_process = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]) diff --git a/tools/benchmarking/benchmark.py b/tools/benchmarking/benchmark.py index e236f3445e..20dc24c2fc 100644 --- a/tools/benchmarking/benchmark.py +++ b/tools/benchmarking/benchmark.py @@ -3,6 +3,8 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import functools import io import logging @@ -16,7 +18,7 @@ from datetime import datetime from pathlib import Path from tempfile import TemporaryDirectory -from typing import Dict, List, Optional, Union, cast +from typing import cast import torch from omegaconf import DictConfig, ListConfig, OmegaConf @@ -74,7 +76,7 @@ def wrapper(*args, **kwargs): @hide_output -def get_single_model_metrics(model_config: Union[DictConfig, ListConfig], openvino_metrics: bool = False) -> Dict: +def get_single_model_metrics(model_config: DictConfig | ListConfig, openvino_metrics: bool = False) -> dict: """Collects metrics for `model_name` and returns a dict of results. Args: @@ -82,7 +84,7 @@ def get_single_model_metrics(model_config: Union[DictConfig, ListConfig], openvi openvino_metrics (bool): If True, converts the model to OpenVINO format and gathers inference metrics. Returns: - Dict: Collection of all the metrics such as time taken, throughput and performance scores. + dict: Collection of all the metrics such as time taken, throughput and performance scores. """ with TemporaryDirectory() as project_path: @@ -135,7 +137,7 @@ def get_single_model_metrics(model_config: Union[DictConfig, ListConfig], openvi return data -def compute_on_cpu(sweep_config: Union[DictConfig, ListConfig], folder: Optional[str] = None): +def compute_on_cpu(sweep_config: DictConfig | ListConfig, folder: str | None = None): """Compute all run configurations over a sigle CPU.""" for run_config in get_run_config(sweep_config.grid_search): model_metrics = sweep(run_config, 0, sweep_config.seed, False) @@ -143,20 +145,20 @@ def compute_on_cpu(sweep_config: Union[DictConfig, ListConfig], folder: Optional def compute_on_gpu( - run_configs: List[DictConfig], + run_configs: list[DictConfig], device: int, seed: int, - writers: List[str], - folder: Optional[str] = None, + writers: list[str], + folder: str | None = None, compute_openvino: bool = False, ): """Go over each run config and collect the result. Args: - run_configs (Union[DictConfig, ListConfig]): List of run configurations. + run_configs (DictConfig | ListConfig): List of run configurations. device (int): The GPU id used for running the sweep. seed (int): Fix a seed. - writers (List[str]): Destinations to write to. + writers (list[str]): Destinations to write to. folder (optional, str): Sub-directory to which runs are written to. Defaults to None. If none writes to root. compute_openvino (bool, optional): Compute OpenVINO throughput. Defaults to False. """ @@ -170,7 +172,7 @@ def compute_on_gpu( ) -def distribute_over_gpus(sweep_config: Union[DictConfig, ListConfig], folder: Optional[str] = None): +def distribute_over_gpus(sweep_config: DictConfig | ListConfig, folder: str | None = None): """Distribute metric collection over all available GPUs. This is done by splitting the list of configurations.""" with ProcessPoolExecutor( max_workers=torch.cuda.device_count(), mp_context=multiprocessing.get_context("spawn") @@ -198,11 +200,11 @@ def distribute_over_gpus(sweep_config: Union[DictConfig, ListConfig], folder: Op raise Exception(f"Error occurred while computing benchmark on GPU {job}") from exc -def distribute(config: Union[DictConfig, ListConfig]): +def distribute(config: DictConfig | ListConfig): """Run all cpu experiments on a single process. Distribute gpu experiments over all available gpus. Args: - config: (Union[DictConfig, ListConfig]): Sweep configuration. + config: (DictConfig | ListConfig): Sweep configuration. """ runs_folder = datetime.strftime(datetime.now(), "%Y_%m_%d-%H_%M_%S") @@ -232,17 +234,17 @@ def distribute(config: Union[DictConfig, ListConfig]): def sweep( - run_config: Union[DictConfig, ListConfig], device: int = 0, seed: int = 42, convert_openvino: bool = False -) -> Dict[str, Union[float, str]]: + run_config: DictConfig | ListConfig, device: int = 0, seed: int = 42, convert_openvino: bool = False +) -> dict[str, str | float]: """Go over all the values mentioned in `grid_search` parameter of the benchmarking config. Args: - run_config: (Union[DictConfig, ListConfig], optional): Configuration for current run. + run_config: (DictConfig | ListConfig, optional): Configuration for current run. device (int, optional): Name of the device on which the model is trained. Defaults to 0 "cpu". convert_openvino (bool, optional): Whether to convert the model to openvino format. Defaults to False. Returns: - Dict[str, Union[float, str]]: Dictionary containing the metrics gathered from the sweep. + dict[str, str | float]: Dictionary containing the metrics gathered from the sweep. """ seed_everything(seed, workers=True) # This assumes that `model_name` is always present in the sweep config. diff --git a/tools/benchmarking/utils/metrics.py b/tools/benchmarking/utils/metrics.py index 04bc3dff1f..8b34e93082 100644 --- a/tools/benchmarking/utils/metrics.py +++ b/tools/benchmarking/utils/metrics.py @@ -3,11 +3,12 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import random import string from glob import glob from pathlib import Path -from typing import Dict, List, Optional, Union import pandas as pd from comet_ml import Experiment @@ -17,15 +18,15 @@ def write_metrics( - model_metrics: Dict[str, Union[str, float]], - writers: List[str], - folder: Optional[str] = None, + model_metrics: dict[str, str | float], + writers: list[str], + folder: str | None = None, ): """Writes metrics to destination provided in the sweep config. Args: - model_metrics (Dict): Dictionary to be written - writers (List[str]): List of destinations. + model_metrics (dict): Dictionary to be written + writers (list[str]): List of destinations. folder (optional, str): Sub-directory to which runs are written to. Defaults to None. If none writes to root. """ # Write to file as each run is computed @@ -47,15 +48,15 @@ def write_metrics( def write_to_tensorboard( - model_metrics: Dict[str, Union[str, float]], + model_metrics: dict[str, str | float], ): """Write model_metrics to tensorboard. Args: - model_metrics (Dict[str, Union[str, float]]): Dictionary containing collected results. + model_metrics (dict[str, str | float]): Dictionary containing collected results. """ scalar_metrics = {} - scalar_prefixes: List[str] = [] + scalar_prefixes: list[str] = [] string_metrics = {} for key, metric in model_metrics.items(): if isinstance(metric, (int, float, bool)): @@ -92,7 +93,7 @@ def get_unique_key(str_len: int) -> str: def upload_to_wandb( team: str = "anomalib", - folder: Optional[str] = None, + folder: str | None = None, ): """Upload the data in csv files to wandb. @@ -120,7 +121,7 @@ def upload_to_wandb( def upload_to_comet( - folder: Optional[str] = None, + folder: str | None = None, ): """Upload the data in csv files to comet. diff --git a/tools/hpo/sweep.py b/tools/hpo/sweep.py index d14d43c1d3..6f2a2f7abf 100644 --- a/tools/hpo/sweep.py +++ b/tools/hpo/sweep.py @@ -3,9 +3,10 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from argparse import ArgumentParser from pathlib import Path -from typing import Union from omegaconf import OmegaConf from pytorch_lightning import seed_everything @@ -39,7 +40,7 @@ def get_args(): seed_everything(model_config.project.seed) # check hpo config structure to see whether it adheres to comet or wandb format - sweep: Union[CometSweep, WandbSweep] + sweep: CometSweep | WandbSweep if "spec" in hpo_config.keys(): sweep = CometSweep(model_config, hpo_config, entity=args.entity) else: diff --git a/tools/inference/gradio_inference.py b/tools/inference/gradio_inference.py index 3fd670bce1..deb0d7f494 100644 --- a/tools/inference/gradio_inference.py +++ b/tools/inference/gradio_inference.py @@ -6,10 +6,11 @@ # Copyright (C) 2022 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from argparse import ArgumentParser, Namespace from importlib import import_module from pathlib import Path -from typing import Optional, Tuple import gradio as gr import gradio.inputs @@ -41,13 +42,13 @@ def get_args() -> Namespace: return parser.parse_args() -def get_inferencer(config_path: Path, weight_path: Path, meta_data_path: Optional[Path] = None) -> Inferencer: +def get_inferencer(config_path: Path, weight_path: Path, meta_data_path: Path | None = None) -> Inferencer: """Parse args and open inferencer. Args: config_path (Path): Path to model configuration file or the name of the model. weight_path (Path): Path to model weights. - meta_data_path (Optional[Path], optional): Metadata is required for OpenVINO models. Defaults to None. + meta_data_path (Path | None, optional): Metadata is required for OpenVINO models. Defaults to None. Raises: ValueError: If unsupported model weight is passed. @@ -78,7 +79,7 @@ def get_inferencer(config_path: Path, weight_path: Path, meta_data_path: Optiona return inferencer -def infer(image: np.ndarray, inferencer: Inferencer) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def infer(image: np.ndarray, inferencer: Inferencer) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Inference function, return anomaly map, score, heat map, prediction mask ans visualisation. Args: @@ -86,7 +87,7 @@ def infer(image: np.ndarray, inferencer: Inferencer) -> Tuple[np.ndarray, np.nda inferencer (Inferencer): model inferencer Returns: - Tuple[np.ndarray, float, np.ndarray, np.ndarray, np.ndarray]: + tuple[np.ndarray, float, np.ndarray, np.ndarray, np.ndarray]: heat_map, pred_mask, segmentation result. """ # Perform inference for the given image.