From e35b5743718a784275e8252b33be7387142a6ede Mon Sep 17 00:00:00 2001 From: Samet Akcay Date: Wed, 27 Mar 2024 12:04:45 +0000 Subject: [PATCH] Merge v1.0.1 to main (#1905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update CHANGELOG.md (#1899) * Update __init__.py * Fix metric bug (#1900) * check metrics by name instead of key * remove prefix from temp metric collection * πŸ”’ Bump onnx version as the previous version caused security issues (#1907) * πŸ“š Add action to check broken links (#1838) * Fix broken links * Add link checker * Fix link in template * linter * nightly check --------- Co-authored-by: Samet Akcay * Use LabelName ENUM for label assignment (#1881) * Use LabelName ENUM for label assignment * Update src/anomalib/deploy/inferencers/torch_inferencer.py Co-authored-by: Samet Akcay --------- Co-authored-by: Samet Akcay * Changed prediction label assignment for OpenVINO inferencer (#1872) * Update openvino_inferencer.py Fixed incorrect label assignment. Was True/False, now it's "Anomalous/Normal" as for the PyTorch inferencer in torch_inferencer.py * Fix ruff --------- Co-authored-by: Samet * Bump onnx version as the previous version caused security issues --------- Co-authored-by: Alexander Dokuchaev Co-authored-by: TurboJonte * Add: fix image transform to float (#1902) * πŸ“š Add action to check broken links (#1838) * Fix broken links * Add link checker * Fix link in template * linter * nightly check --------- Co-authored-by: Samet Akcay * Use LabelName ENUM for label assignment (#1881) * Use LabelName ENUM for label assignment * Update src/anomalib/deploy/inferencers/torch_inferencer.py Co-authored-by: Samet Akcay --------- Co-authored-by: Samet Akcay * Changed prediction label assignment for OpenVINO inferencer (#1872) * Update openvino_inferencer.py Fixed incorrect label assignment. Was True/False, now it's "Anomalous/Normal" as for the PyTorch inferencer in torch_inferencer.py * Fix ruff --------- Co-authored-by: Samet * Add: fix image transform to float Signed-off-by: Bepitic * upd: video dtpe transform from torchvision Signed-off-by: Bepitic * Added the scale. Signed-off-by: Bepitic * Update src/anomalib/data/base/video.py Co-authored-by: Dick Ameln --------- Signed-off-by: Bepitic Co-authored-by: Alexander Dokuchaev Co-authored-by: Samet Akcay Co-authored-by: TurboJonte Co-authored-by: Dick Ameln * Fix video bug and add tests (#1910) * πŸ“š Add action to check broken links (#1838) * Fix broken links * Add link checker * Fix link in template * linter * nightly check --------- Co-authored-by: Samet Akcay * squeeze temporal dimension when clip length is 1 * add unit test for video input tensor dtype * add test for single frame clips * update changelog * add author --------- Co-authored-by: Alexander Dokuchaev Co-authored-by: Samet Akcay * Temporarily set devices to 1 * 🐞 Add greyscale conversion (#1911) Add greyscale conversion Signed-off-by: Ashwin Vaidya * 🐞 Fix publish workflow (#1912) Disable tty prompt for gpg import Signed-off-by: Ashwin Vaidya --------- Signed-off-by: Bepitic Signed-off-by: Ashwin Vaidya Co-authored-by: Dick Ameln Co-authored-by: Alexander Dokuchaev Co-authored-by: TurboJonte Co-authored-by: Paco Co-authored-by: Dick Ameln Co-authored-by: Ashwin Vaidya --- .github/workflows/publish.yml | 6 ++-- CHANGELOG.md | 42 +++++++++++++++++++++- pyproject.toml | 2 +- requirements/openvino.txt | 2 +- src/anomalib/__init__.py | 2 +- src/anomalib/callbacks/metrics.py | 9 ++--- src/anomalib/data/base/video.py | 5 +++ src/anomalib/data/utils/image.py | 2 +- src/anomalib/engine/engine.py | 3 ++ src/anomalib/metrics/__init__.py | 2 +- tests/unit/data/base/video.py | 26 +++++++++++++- tests/unit/data/video/test_avenue.py | 8 ++++- tests/unit/data/video/test_shanghaitech.py | 8 ++++- tests/unit/data/video/test_ucsdped.py | 8 ++++- 14 files changed, 106 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cd0a46153f..04a60de9b6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,9 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -17,7 +19,7 @@ jobs: - name: Import GPG Key run: | - echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --import + echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --import --batch --yes --no-tty - name: Build and sign distribution run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f64f3fdd..ce0778deff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed -- Use right interpolation method in WinCLIP resize () +### New Contributors + +**Full Changelog**: + +## [v1.0.1] - Unreleased + +### Added + +- Add requirements into `pyproject.toml` & Refactor anomalib install `get_requirements` by @harimkang in https://github.com/openvinotoolkit/anomalib/pull/1808 + +### Changed + +- πŸ“š Update the getting started notebook by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/1800 +- πŸ”¨Refactored-assert-statements-with-explicit-error-handling by @sahusiddharth in https://github.com/openvinotoolkit/anomalib/pull/1825 +- πŸ”¨Made-imagenette-path-configurable-in-config by @sahusiddharth in https://github.com/openvinotoolkit/anomalib/pull/1833 +- πŸ› οΈ Update changelog by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/1842 +- Remove input_size argument from models #1827 by @Shakib-IO in https://github.com/openvinotoolkit/anomalib/pull/1856 +- πŸš€ Allow validation splits from training data by @davnn in https://github.com/openvinotoolkit/anomalib/pull/1865 +- πŸ› οΈ Ensure images are loaded in RGB format by @davnn in https://github.com/openvinotoolkit/anomalib/pull/1866 +- πŸ”¨ Update OpenVINO predict to handle normalization inside the method. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/1875 +- ✨ Upgrade TorchMetrics by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/1879 +- Address minor WinCLIP issues by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/1889 + +### Deprecated + +### Fixed + +- 🐞 Fix single-frame video input size by [@djdameln](https://github.com/djdameln) () +- 🐞 Fix dobot notebook by @djdameln in https://github.com/openvinotoolkit/anomalib/pull/1852 +- 🐞 Fix CLI config and update the docs. by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/1886 +- 🐞 Fix the error if the device in masks_to_boxes is not both CPU and CUDA by @danylo-boiko in https://github.com/openvinotoolkit/anomalib/pull/1839 +- 🐞 Hot-fix wrong requirement for setup.py by @harimkang in https://github.com/openvinotoolkit/anomalib/pull/1823 +- 🐞 Use right interpolation method in WinCLIP resize () - 🐞 Fix the error if the device in masks_to_boxes is not both CPU and CUDA by @danylo-boiko in https://github.com/openvinotoolkit/anomalib/pull/1839 +### New Contributors + +- @sahusiddharth made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/1825 +- @Shakib-IO made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/1856 +- @davnn made their first contribution in https://github.com/openvinotoolkit/anomalib/pull/1866 + +**Full Changelog**: https://github.com/openvinotoolkit/anomalib/compare/v1.0.0...v1.0.1 + ## [v1.0.0] - 2024-02-29 ### Added diff --git a/pyproject.toml b/pyproject.toml index 8cd34a9d13..bbff67ad3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ core = [ "torchmetrics==0.10.3", "open-clip-torch>=2.23.0", ] -openvino = ["openvino-dev>=2023.0", "nncf>=2.5.0", "onnx>=1.13.1"] +openvino = ["openvino-dev>=2023.0", "nncf>=2.5.0", "onnx>=1.16.0"] loggers = [ "comet-ml>=3.31.7", "gradio>=4", diff --git a/requirements/openvino.txt b/requirements/openvino.txt index cb68a34a59..3fb653fa17 100644 --- a/requirements/openvino.txt +++ b/requirements/openvino.txt @@ -1,3 +1,3 @@ openvino-dev>=2023.0 nncf>=2.5.0 -onnx>=1.13.1 +onnx>=1.16.0 diff --git a/src/anomalib/__init__.py b/src/anomalib/__init__.py index 711eb023e9..6b5e4318e5 100644 --- a/src/anomalib/__init__.py +++ b/src/anomalib/__init__.py @@ -5,7 +5,7 @@ from enum import Enum -__version__ = "1.1.0dev" +__version__ = "1.0.1" class LearningType(str, Enum): diff --git a/src/anomalib/callbacks/metrics.py b/src/anomalib/callbacks/metrics.py index f9ab286e0d..6a7b173272 100644 --- a/src/anomalib/callbacks/metrics.py +++ b/src/anomalib/callbacks/metrics.py @@ -91,14 +91,9 @@ def setup( if isinstance(pl_module, AnomalyModule): pl_module.image_metrics = create_metric_collection(image_metric_names, "image_") if hasattr(pl_module, "pixel_metrics"): # incase metrics are loaded from model checkpoint - new_metrics = create_metric_collection(pixel_metric_names, "pixel_") + new_metrics = create_metric_collection(pixel_metric_names) for name in new_metrics: - # Ruff is ignored as - # >>> name not in pl_module.pixel_metrics - # True - # >>> name not in pl_module.pixel_metrics.keys() - # False - if name not in pl_module.pixel_metrics.keys(): # noqa: SIM118 + if name not in pl_module.pixel_metrics: pl_module.pixel_metrics.add_metrics(new_metrics[name]) else: pl_module.pixel_metrics = create_metric_collection(pixel_metric_names, "pixel_") diff --git a/src/anomalib/data/base/video.py b/src/anomalib/data/base/video.py index 7e9461a0a9..5f04ebfe3b 100644 --- a/src/anomalib/data/base/video.py +++ b/src/anomalib/data/base/video.py @@ -10,6 +10,7 @@ import torch from pandas import DataFrame from torchvision.transforms.v2 import Transform +from torchvision.transforms.v2.functional import to_dtype_video from torchvision.tv_tensors import Mask from anomalib import TaskType @@ -153,6 +154,7 @@ def __getitem__(self, index: int) -> dict[str, str | torch.Tensor]: msg = "self.indexer must be an instance of ClipsIndexer." raise TypeError(msg) item = self.indexer.get_item(index) + item["image"] = to_dtype_video(video=item["image"], scale=True) # include the untransformed image for visualization item["original_image"] = item["image"].to(torch.uint8) @@ -167,6 +169,9 @@ def __getitem__(self, index: int) -> dict[str, str | torch.Tensor]: elif self.transform: item["image"] = self.transform(item["image"]) + # squeeze temporal dimensions in case clip length is 1 + item["image"] = item["image"].squeeze(0) + # include only target frame in gt if self.clip_length_in_frames > 1 and self.target_frame != VideoTargetFrame.ALL: item = self._select_targets(item) diff --git a/src/anomalib/data/utils/image.py b/src/anomalib/data/utils/image.py index 91a4e975f3..2063baa6e4 100644 --- a/src/anomalib/data/utils/image.py +++ b/src/anomalib/data/utils/image.py @@ -371,7 +371,7 @@ def read_mask(path: str | Path, as_tensor: bool = False) -> torch.Tensor | np.nd >>> type(mask) """ - image = Image.open(path) + image = Image.open(path).convert("L") return Mask(to_image(image).squeeze() / 255, dtype=torch.uint8) if as_tensor else np.array(image) diff --git a/src/anomalib/engine/engine.py b/src/anomalib/engine/engine.py index 38e3aa5244..a08b78e529 100644 --- a/src/anomalib/engine/engine.py +++ b/src/anomalib/engine/engine.py @@ -301,6 +301,9 @@ def _setup_trainer(self, model: AnomalyModule) -> None: # Setup anomalib callbacks to be used with the trainer self._setup_anomalib_callbacks() + # Temporarily set devices to 1 to avoid issues with multiple processes + self._cache.args["devices"] = 1 + # Instantiate the trainer if it is not already instantiated if self._trainer is None: self._trainer = Trainer(**self._cache.args) diff --git a/src/anomalib/metrics/__init__.py b/src/anomalib/metrics/__init__.py index 74bab19446..2eefb4882d 100644 --- a/src/anomalib/metrics/__init__.py +++ b/src/anomalib/metrics/__init__.py @@ -162,7 +162,7 @@ def metric_collection_from_dicts(metrics: dict[str, dict[str, Any]], prefix: str def create_metric_collection( metrics: list[str] | dict[str, dict[str, Any]], - prefix: str | None, + prefix: str | None = None, ) -> AnomalibMetricCollection: """Create a metric collection from a list of metric names or dictionaries. diff --git a/tests/unit/data/base/video.py b/tests/unit/data/base/video.py index 3e0ef24b1d..d42768bd6c 100644 --- a/tests/unit/data/base/video.py +++ b/tests/unit/data/base/video.py @@ -3,8 +3,8 @@ # Copyright (C) 2023-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 - import pytest +import torch from anomalib.data import AnomalibDataModule @@ -46,3 +46,27 @@ def test_get_item_returns_correct_keys_and_shapes(self, datamodule: AnomalibData assert len(batch["label"]) == 4 assert batch["mask"].shape == (4, 256, 256) assert batch["mask"].shape == (4, 256, 256) + + @pytest.mark.parametrize("subset", ["train", "val", "test"]) + def test_item_dtype(self, datamodule: AnomalibDataModule, subset: str) -> None: + """Test that the input tensor is of float type and scaled between 0-1.""" + # Get the dataloader. + dataloader = getattr(datamodule, f"{subset}_dataloader")() + + # Get the first batch. + batch = next(iter(dataloader)) + clip = batch["image"] + assert clip.dtype == torch.float32 + assert clip.min() >= 0 + assert clip.max() <= 1 + + @pytest.mark.parametrize("clip_length_in_frames", [1]) + def test_single_frame_squeezed(self, datamodule: AnomalibDataModule) -> None: + """Test that the temporal dimension is squeezed when the clip lenght is 1.""" + # Get the dataloader. + dataloader = datamodule.train_dataloader() + + # Get the first batch. + batch = next(iter(dataloader)) + clip = batch["image"] + assert clip.shape == (4, 3, 256, 256) diff --git a/tests/unit/data/video/test_avenue.py b/tests/unit/data/video/test_avenue.py index b4f27e3122..114a6eecff 100644 --- a/tests/unit/data/video/test_avenue.py +++ b/tests/unit/data/video/test_avenue.py @@ -16,11 +16,17 @@ class TestAvenue(_TestAnomalibVideoDatamodule): """Avenue Datamodule Unit Tests.""" @pytest.fixture() - def datamodule(self, dataset_path: Path, task_type: TaskType) -> Avenue: + def clip_length_in_frames(self) -> int: + """Return the number of frames in each clip.""" + return 2 + + @pytest.fixture() + def datamodule(self, dataset_path: Path, task_type: TaskType, clip_length_in_frames: int) -> Avenue: """Create and return a Avenue datamodule.""" _datamodule = Avenue( root=dataset_path / "avenue", gt_dir=dataset_path / "avenue" / "ground_truth_demo", + clip_length_in_frames=clip_length_in_frames, image_size=256, task=task_type, num_workers=0, diff --git a/tests/unit/data/video/test_shanghaitech.py b/tests/unit/data/video/test_shanghaitech.py index 09516c6806..e32b65273d 100644 --- a/tests/unit/data/video/test_shanghaitech.py +++ b/tests/unit/data/video/test_shanghaitech.py @@ -17,11 +17,17 @@ class TestShanghaiTech(_TestAnomalibVideoDatamodule): """ShanghaiTech Datamodule Unit Tests.""" @pytest.fixture() - def datamodule(self, dataset_path: Path, task_type: TaskType) -> ShanghaiTech: + def clip_length_in_frames(self) -> int: + """Return the number of frames in each clip.""" + return 2 + + @pytest.fixture() + def datamodule(self, dataset_path: Path, task_type: TaskType, clip_length_in_frames: int) -> ShanghaiTech: """Create and return a Shanghai datamodule.""" _datamodule = ShanghaiTech( root=dataset_path / "shanghaitech", scene=1, + clip_length_in_frames=clip_length_in_frames, image_size=(256, 256), train_batch_size=4, eval_batch_size=4, diff --git a/tests/unit/data/video/test_ucsdped.py b/tests/unit/data/video/test_ucsdped.py index 67dddf6b76..95260fa300 100644 --- a/tests/unit/data/video/test_ucsdped.py +++ b/tests/unit/data/video/test_ucsdped.py @@ -16,11 +16,17 @@ class TestUCSDped(_TestAnomalibVideoDatamodule): """UCSDped Datamodule Unit Tests.""" @pytest.fixture() - def datamodule(self, dataset_path: Path, task_type: TaskType) -> UCSDped: + def clip_length_in_frames(self) -> int: + """Return the number of frames in each clip.""" + return 2 + + @pytest.fixture() + def datamodule(self, dataset_path: Path, task_type: TaskType, clip_length_in_frames: int) -> UCSDped: """Create and return a UCSDped datamodule.""" _datamodule = UCSDped( root=dataset_path / "ucsdped", category="dummy", + clip_length_in_frames=clip_length_in_frames, task=task_type, image_size=256, train_batch_size=4,