mirror of
https://github.com/hanzoai/anomalib.git
synced 2026-10-30 18:48:11 +00:00
The previous commit was meant to remove .github/workflows and removed every tracked path instead — 892 files, the whole repo. This puts back the 874 that were never in scope; the eighteen workflow files stay gone, for the reasons that commit gives. Content is byte-identical to 9d2aec9 outside .github/workflows.
832 lines
37 KiB
Python
832 lines
37 KiB
Python
"""Test Helpers - Dataset."""
|
|
|
|
# Copyright (C) 2023-2025 Intel Corporation
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from contextlib import ContextDecorator
|
|
from pathlib import Path
|
|
from tempfile import mkdtemp
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from scipy.io import savemat
|
|
from skimage import img_as_ubyte
|
|
from skimage.io import imsave
|
|
|
|
from anomalib.data import DataFormat
|
|
from anomalib.data.utils import LabelName
|
|
from anomalib.data.utils.generators.perlin import PerlinAnomalyGenerator
|
|
|
|
|
|
class DummyImageGenerator:
|
|
"""Dummy image generator.
|
|
|
|
Args:
|
|
image_shape (tuple[int, int], optional): Image shape. Defaults to (256, 256).
|
|
|
|
Examples:
|
|
To generate a normal image, use the ``generate_normal_image`` method.
|
|
>>> generator = DummyImageGenerator()
|
|
>>> image, mask = generator.generate_normal_image()
|
|
|
|
To generate an abnormal image, use the ``generate_abnormal_image`` method.
|
|
>>> generator = DummyImageGenerator()
|
|
>>> image, mask = generator.generate_abnormal_image()
|
|
|
|
To generate an image with a specific label, use the ``generate_image`` method.
|
|
>>> generator = DummyImageGenerator()
|
|
>>> image, mask = generator.generate_image(label=LabelName.ABNORMAL)
|
|
|
|
or,
|
|
>>> generator = DummyImageGenerator()
|
|
>>> image, mask = generator.generate_image(label=LabelName.NORMAL)
|
|
"""
|
|
|
|
def __init__(self, image_shape: tuple[int, int] = (256, 256), rng: np.random.Generator | None = None) -> None:
|
|
self.image_shape = image_shape
|
|
self.augmenter = PerlinAnomalyGenerator()
|
|
self.rng = rng if rng else np.random.default_rng()
|
|
|
|
def generate_normal_image(self) -> tuple[np.ndarray, np.ndarray]:
|
|
"""Generate a normal image."""
|
|
image = np.zeros([*self.image_shape, 3]).astype(np.uint8)
|
|
image[...] = self.rng.integers(low=0, high=255, size=[3])
|
|
mask = np.zeros(self.image_shape).astype(np.uint8)
|
|
|
|
return image, mask
|
|
|
|
def generate_abnormal_image(self, beta: float = 0.2) -> tuple[np.ndarray, np.ndarray]:
|
|
"""Generate an abnormal image.
|
|
|
|
Args:
|
|
beta (float, optional): beta value for superimposing perturbation on image. Defaults to 0.2.
|
|
|
|
Returns:
|
|
tuple[np.ndarray, np.ndarray]: Abnormal image and mask.
|
|
"""
|
|
# Generate a random image.
|
|
image, _ = self.generate_normal_image()
|
|
|
|
# Generate perturbation.
|
|
perturbation, mask = self.augmenter.generate_perturbation(height=self.image_shape[0], width=self.image_shape[1])
|
|
perturbation = perturbation.cpu().numpy()
|
|
mask = mask.cpu().numpy()
|
|
|
|
# Superimpose perturbation on image ``img``.
|
|
abnormal_image = (image * (1 - mask) + (beta) * perturbation + (1 - beta) * image * (mask)).astype(np.uint8)
|
|
|
|
return abnormal_image, mask.squeeze()
|
|
|
|
def generate_image(
|
|
self,
|
|
label: LabelName = LabelName.NORMAL,
|
|
image_filename: Path | str | None = None,
|
|
mask_filename: Path | str | None = None,
|
|
) -> tuple[np.ndarray, np.ndarray]:
|
|
"""Generate a random image.
|
|
|
|
Args:
|
|
label (LabelName, optional): Image label (NORMAL - 0 or ABNORMAL - 1). Defaults to 0.
|
|
image_filename (Path | str | None, optional): Image filename to save to filesytem. Defaults to None.
|
|
mask_filename (Path | str | None, optional): Mask filename to save to filesystem. Defaults to None.
|
|
|
|
Returns:
|
|
tuple[np.ndarray, np.ndarray]: Image and mask.
|
|
"""
|
|
func = self.generate_normal_image if label == LabelName.NORMAL else self.generate_abnormal_image
|
|
image, mask = func()
|
|
|
|
if image_filename:
|
|
self.save_image(filename=image_filename, image=image)
|
|
|
|
if mask_filename:
|
|
self.save_image(filename=mask_filename, image=img_as_ubyte(mask))
|
|
|
|
return image, mask
|
|
|
|
@staticmethod
|
|
def save_image(filename: Path | str, image: np.ndarray, check_contrast: bool = False) -> None:
|
|
"""Save image to filesystem.
|
|
|
|
Args:
|
|
filename (Path | str): Filename to save image to.
|
|
image (np.ndarray): Image to save.
|
|
check_contrast (bool, optional): Check for low contrast and print warning. Defaults to False.
|
|
"""
|
|
filename = Path(filename)
|
|
filename.parent.mkdir(parents=True, exist_ok=True)
|
|
imsave(fname=filename, arr=image, check_contrast=check_contrast)
|
|
|
|
|
|
class DummyVideoGenerator(DummyImageGenerator):
|
|
"""Dummy video generator.
|
|
|
|
Args:
|
|
num_frames (int, optional): Length of the video. Defaults to 32.
|
|
frame_shape (tuple[int, int], optional): Shape of the video frames. Defaults to (256, 256).
|
|
|
|
Examples:
|
|
To generate a video with a random sequence of normal and abnormal frames, use the ``generate_video`` method.
|
|
>>> generator = DummyVideoGenerator()
|
|
>>> frames, masks = generator.generate_video()
|
|
|
|
It is possible to specify the length of the video
|
|
>>> generator = DummyVideoGenerator()
|
|
>>> frames, masks = generator.generate_video(length=64)
|
|
|
|
It is possible to specify the first frame label (normal or abnormal)
|
|
>>> generator = DummyVideoGenerator()
|
|
>>> frames, masks = generator.generate_video(first_label=LabelName.ABNORMAL)
|
|
|
|
It is possible to specify the probability of switching between normal and abnormal frames
|
|
>>> generator = DummyVideoGenerator()
|
|
>>> frames, masks = generator.generate_video(p_state_switch=0.5)
|
|
"""
|
|
|
|
def __init__(self, num_frames: int = 32, frame_shape: tuple[int, int] = (256, 256)) -> None:
|
|
super().__init__(frame_shape)
|
|
self.num_frames = num_frames
|
|
|
|
def generate_video(
|
|
self,
|
|
length: int = 32,
|
|
first_label: LabelName = LabelName.NORMAL,
|
|
p_state_switch: float = 0.2,
|
|
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
|
"""Generate video clip with a random sequence of anomalous frames.
|
|
|
|
Args:
|
|
length (int): Length of the video sequence in number of frames.
|
|
first_label (LabelName): Label of the first frame (normal or abnormal).
|
|
p_state_switch (float): Probability of transitioning between normal and anomalous in consecutive frames.
|
|
|
|
Returns:
|
|
tuple[list[np.ndarray], list[np.ndarray]]: List of frames and list of masks.
|
|
"""
|
|
frames: list[np.ndarray] = []
|
|
masks: list[np.ndarray] = []
|
|
state = 1 if first_label == LabelName.NORMAL else -1
|
|
for _ in range(length):
|
|
state = state * -1 if self.rng.random() < p_state_switch else state
|
|
label = LabelName.NORMAL if state == 1 else LabelName.ABNORMAL
|
|
frame, mask = self.generate_image(label=label)
|
|
frames.append(frame)
|
|
masks.append(mask)
|
|
return frames, masks
|
|
|
|
def save_frame(self, filename: Path | str, frame: np.ndarray, check_contrast: bool = False) -> None:
|
|
"""Save frame to filesystem.
|
|
|
|
Args:
|
|
filename (Path | str): Filename to save image to.
|
|
frame (np.ndarray): Frame to save.
|
|
check_contrast (bool, optional): Check for low contrast and print warning. Defaults to False.
|
|
"""
|
|
self.save_image(filename=filename, image=frame, check_contrast=check_contrast)
|
|
|
|
|
|
class DummyDatasetGenerator(ContextDecorator):
|
|
"""Base dummy dataset generator class to be implemented by Image and Video generators.
|
|
|
|
Args:
|
|
data_format (DataFormat): Data format of the dataset.
|
|
root (Path | str | None, optional): Root directory to save the dataset. Defaults to None.
|
|
dataset_name (str, optional): Name of the dataset. Defaults to None.
|
|
num_train (int, optional): Number of training images to generate. Defaults to 5.
|
|
num_test (int, optional): Number of testing images to generate per category. Defaults to 5.
|
|
seed (int, optional): Fixes seed if any number greater than 0 is provided. 0 means no seed. Defaults to 0.
|
|
|
|
Examples:
|
|
>>> with DummyDatasetGenerator(num_train=10, num_test=10) as dataset_path:
|
|
>>> some_function()
|
|
|
|
Alternatively, you can use it as a standalone class.
|
|
This will create a temporary directory with the dataset.
|
|
|
|
>>> generator = DummyDatasetGenerator(num_train=10, num_test=10)
|
|
>>> generator.generate_dataset()
|
|
|
|
If you want to use a specific directory, you can pass it as an argument.
|
|
|
|
>>> generator = DummyDatasetGenerator(root="./datasets/dummy")
|
|
>>> generator.generate_dataset()
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
data_format: DataFormat | str,
|
|
root: Path | str | None = None,
|
|
num_train: int = 5,
|
|
num_test: int = 5,
|
|
seed: int | None = None,
|
|
) -> None:
|
|
if isinstance(data_format, str):
|
|
data_format = DataFormat(data_format)
|
|
|
|
if data_format not in list(DataFormat):
|
|
message = f"Invalid data format {data_format}. Valid options are {list(DataFormat)}."
|
|
raise ValueError(message)
|
|
|
|
self.data_format = data_format
|
|
self.root = Path(mkdtemp() if root is None else root)
|
|
self.dataset_root = self.root / self.data_format.value
|
|
self.num_train = num_train
|
|
self.num_test = num_test
|
|
self.rng = np.random.default_rng(seed)
|
|
|
|
def generate_dataset(self) -> None:
|
|
"""Generate dataset."""
|
|
# get dataset specific ``generate_dataset`` function based on string.
|
|
|
|
if hasattr(self, f"_generate_dummy_{self.data_format.value}_dataset"):
|
|
method_name = f"_generate_dummy_{self.data_format.value}_dataset"
|
|
method = getattr(self, method_name)
|
|
method()
|
|
else:
|
|
message = f"``generate_dummy_{self.data_format.value}_dataset`` not implemented."
|
|
raise NotImplementedError(message)
|
|
|
|
def __enter__(self) -> Path:
|
|
"""Creates the dataset in temp folder."""
|
|
self.generate_dataset()
|
|
return self.dataset_root
|
|
|
|
def __exit__(self, _exc_type, _exc_value, _exc_traceback) -> None: # noqa: ANN001
|
|
"""Cleanup the directory."""
|
|
shutil.rmtree(self.dataset_root)
|
|
|
|
|
|
class DummyImageDatasetGenerator(DummyDatasetGenerator):
|
|
r"""Context for generating dummy shapes dataset.
|
|
|
|
Args:
|
|
data_format (DataFormat): Data format of the dataset.
|
|
root (Path | str, optional): Path to the root directory. Defaults to None.
|
|
num_train (int, optional): Number of training images to generate. Defaults to 1000.
|
|
num_test (int, optional): Number of testing images to generate per category. Defaults to 100.
|
|
img_height (int, optional): Height of the image. Defaults to 128.
|
|
img_width (int, optional): Width of the image. Defaults to 128.
|
|
max_size (Optional[int], optional): Maximum size of the test shapes. Defaults to 10.
|
|
train_shapes (List[str], optional): List of good shapes. Defaults to ["circle", "rectangle"].
|
|
test_shapes (List[str], optional): List of anomalous shapes. Defaults to ["triangle", "ellipse"].
|
|
seed (int, optional): Fixes seed if any number greater than 0 is provided. 0 means no seed. Defaults to 0.
|
|
|
|
Examples:
|
|
To create an MVTecAD dataset with 10 training images and 10 testing images per category, use the following code.
|
|
>>> dataset_generator = DummyImageDatasetGenerator(data_format="mvtecad", num_train=10, num_test=10)
|
|
>>> dataset_generator.generate_dataset()
|
|
|
|
In order to provide a specific directory to save the dataset, use the ``root`` argument.
|
|
>>> dataset_generator = DummyImageDatasetGenerator(data_format="mvtecad", root="./datasets/dummy")
|
|
>>> dataset_generator.generate_dataset()
|
|
|
|
It is also possible to use the generator as a context manager.
|
|
>>> with DummyImageDatasetGenerator(data_format="mvtecad", num_train=10, num_test=10) as dataset_path:
|
|
>>> some_function()
|
|
|
|
To get the list of available image datasets, use the ``ImageDataFormat`` enum.
|
|
>>> from anomalib.data import ImageDataFormat
|
|
>>> print(list(ImageDataFormat))
|
|
|
|
Then you can use the ``ImageDataFormat`` enum to generate the dataset.
|
|
>>> dataset_generator = DummyImageDatasetGenerator(data_format="btech", num_train=10, num_test=10)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
data_format: DataFormat | str = "mvtecad",
|
|
root: Path | str | None = None,
|
|
normal_category: str = "good",
|
|
abnormal_category: str = "bad",
|
|
num_train: int = 5,
|
|
num_test: int = 5,
|
|
image_shape: tuple[int, int] = (256, 256),
|
|
num_channels: int = 3,
|
|
min_size: int = 64,
|
|
seed: int | None = None,
|
|
) -> None:
|
|
super().__init__(
|
|
data_format=data_format,
|
|
root=root,
|
|
num_train=num_train,
|
|
num_test=num_test,
|
|
seed=seed,
|
|
)
|
|
self.normal_category = normal_category
|
|
self.abnormal_category = abnormal_category
|
|
self.image_shape = image_shape
|
|
self.num_channels = num_channels
|
|
self.min_size = min_size
|
|
self.image_generator = DummyImageGenerator(image_shape=image_shape, rng=self.rng)
|
|
|
|
def _generate_dummy_datumaro_dataset(self) -> None:
|
|
"""Generates dummy Datumaro dataset in a temporary directory."""
|
|
# generate images
|
|
image_root = self.dataset_root / "images" / "default"
|
|
image_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
file_names: list[str] = []
|
|
|
|
# Create normal images
|
|
for i in range(self.num_train + self.num_test):
|
|
label = LabelName.NORMAL
|
|
image_filename = image_root / f"normal_{i:03}.png"
|
|
file_names.append(image_filename)
|
|
self.image_generator.generate_image(label, image_filename)
|
|
|
|
# Create abnormal images
|
|
for i in range(self.num_test):
|
|
label = LabelName.ABNORMAL
|
|
image_filename = image_root / f"abnormal_{i:03}.png"
|
|
file_names.append(image_filename)
|
|
self.image_generator.generate_image(label, image_filename)
|
|
|
|
# create annotation file
|
|
annotation_file = self.dataset_root / "annotations" / "default.json"
|
|
annotation_file.parent.mkdir(parents=True, exist_ok=True)
|
|
annotations = {
|
|
"categories": {"label": {"labels": [{"name": "Normal"}, {"name": "Anomalous"}]}},
|
|
"items": [],
|
|
}
|
|
for file_name in file_names:
|
|
annotations["items"].append({
|
|
"annotations": [{"label_id": 1 if "abnormal" in str(file_name) else 0}],
|
|
"image": {"path": file_name.name},
|
|
})
|
|
with annotation_file.open("w") as f:
|
|
json.dump(annotations, f)
|
|
|
|
def _generate_dummy_mvtecad_dataset(
|
|
self,
|
|
normal_dir: str = "good",
|
|
abnormal_dir: str | None = None,
|
|
image_extension: str = ".png",
|
|
mask_suffix: str = "_mask",
|
|
mask_extension: str = ".png",
|
|
) -> None:
|
|
"""Generates dummy MVTecAD dataset in a temporary directory using the same convention as MVTec AD."""
|
|
# MVTec has multiple subcategories within the dataset.
|
|
dataset_category = "dummy"
|
|
|
|
# Create normal images.
|
|
for split in ("train", "test"):
|
|
path = self.dataset_root / dataset_category / split / normal_dir
|
|
num_images = self.num_train if split == "train" else self.num_test
|
|
for i in range(num_images):
|
|
label = LabelName.NORMAL
|
|
image_filename = path / f"{i:03}{image_extension}"
|
|
self.image_generator.generate_image(label=label, image_filename=image_filename)
|
|
|
|
# Create abnormal test images and masks.
|
|
abnormal_dir = abnormal_dir or self.abnormal_category
|
|
path = self.dataset_root / dataset_category / "test" / abnormal_dir
|
|
mask_path = self.dataset_root / dataset_category / "ground_truth" / abnormal_dir
|
|
|
|
for i in range(self.num_test):
|
|
label = LabelName.ABNORMAL
|
|
image_filename = path / f"{i:03}{image_extension}"
|
|
mask_filename = mask_path / f"{i:03}{mask_suffix}{mask_extension}"
|
|
self.image_generator.generate_image(label, image_filename, mask_filename)
|
|
|
|
def _generate_dummy_folder_dataset(self) -> None:
|
|
"""Generate dummy folder dataset in a temporary directory."""
|
|
# folder names
|
|
normal_dir = self.root / self.normal_category
|
|
abnormal_dir = self.root / self.abnormal_category
|
|
mask_dir = self.root / "masks"
|
|
|
|
# generate images
|
|
for i in range(self.num_train):
|
|
label = LabelName.NORMAL
|
|
image_filename = normal_dir / f"{self.normal_category}_{i:03}.png"
|
|
self.image_generator.generate_image(label, image_filename)
|
|
|
|
for i in range(self.num_test):
|
|
label = LabelName.ABNORMAL
|
|
image_filename = abnormal_dir / f"{self.abnormal_category}_{i:03}.png"
|
|
mask_filename = mask_dir / image_filename.name
|
|
self.image_generator.generate_image(label, image_filename, mask_filename)
|
|
|
|
def _generate_dummy_tabular_dataset(self) -> None:
|
|
"""Generate dummy folder structure for tabular dataset in a temporary directory."""
|
|
self._generate_dummy_folder_dataset()
|
|
|
|
def _generate_dummy_btech_dataset(self) -> None:
|
|
"""Generate dummy BeanTech dataset in directory using the same convention as BeanTech AD."""
|
|
# BeanTech AD follows the same convention as MVTec AD.
|
|
self._generate_dummy_mvtecad_dataset(normal_dir="ok", abnormal_dir="ko", mask_suffix="")
|
|
|
|
def _generate_dummy_mvtec_3d_dataset(self) -> None:
|
|
"""Generate dummy MVTec 3D AD dataset in a temporary directory using the same convention as MVTec AD."""
|
|
# MVTec 3D AD has multiple subcategories within the dataset.
|
|
dataset_category = "dummy"
|
|
|
|
# Create training and validation images.
|
|
for split in ("train", "validation"):
|
|
split_path = self.dataset_root / dataset_category / split / self.normal_category
|
|
for directory in ("rgb", "xyz"):
|
|
extension = ".tiff" if directory == "xyz" else ".png"
|
|
for i in range(self.num_train):
|
|
label = LabelName.NORMAL
|
|
image_filename = split_path / directory / f"{i:03}{extension}"
|
|
self.image_generator.generate_image(label=label, image_filename=image_filename)
|
|
|
|
## Create test images.
|
|
test_path = self.dataset_root / dataset_category / "test"
|
|
for category in (self.normal_category, self.abnormal_category):
|
|
label = LabelName.NORMAL if category == "good" else LabelName.ABNORMAL
|
|
for i in range(self.num_test):
|
|
# Generate image and mask.
|
|
image, mask = self.image_generator.generate_image(label=label)
|
|
|
|
# Create rgb, xyz, and gt filenames.
|
|
for directory in ("rgb", "xyz", "gt"):
|
|
extension = ".png" if directory == "gt" else ".tiff" if directory == "xyz" else ".png"
|
|
filename = test_path / category / directory / f"{i:03}{extension}"
|
|
|
|
# Save image or mask.
|
|
if directory == "gt":
|
|
self.image_generator.save_image(filename=filename, image=img_as_ubyte(mask))
|
|
else:
|
|
self.image_generator.save_image(filename=filename, image=image)
|
|
|
|
def _generate_dummy_mvtec_loco_dataset(self) -> None:
|
|
"""Generates dummy MVTec LOCO AD dataset in a temporary directory using the same convention as MVTec LOCO AD."""
|
|
# MVTec LOCO has multiple subcategories within the dataset.
|
|
dataset_category = "dummy"
|
|
|
|
extension = ".png"
|
|
|
|
# Create normal images.
|
|
for split in ("train", "validation", "test"):
|
|
path = self.dataset_root / dataset_category / split / "good"
|
|
if split == "train":
|
|
num_images = self.num_train
|
|
elif split == "val":
|
|
num_images = self.num_val
|
|
else:
|
|
num_images = self.num_test
|
|
|
|
for i in range(num_images):
|
|
label = LabelName.NORMAL
|
|
image_filename = path / f"{i:03}{extension}"
|
|
self.image_generator.generate_image(label=label, image_filename=image_filename)
|
|
|
|
# Create abnormal test images and masks.
|
|
for abnormal_dir in ("logical_anomalies", "structural_anomalies"):
|
|
path = self.dataset_root / dataset_category / "test" / abnormal_dir
|
|
mask_path = self.dataset_root / dataset_category / "ground_truth" / abnormal_dir
|
|
|
|
for i in range(self.num_test):
|
|
label = LabelName.ABNORMAL
|
|
image_filename = path / f"{i:03}{extension}"
|
|
# Here, only one ground-truth mask for each abnormal image is generated
|
|
# the structure follows the same convention as MVTec LOCO AD, e.g., image_filename/000.png
|
|
mask_filename = mask_path / f"{i:03}/000{extension}"
|
|
self.image_generator.generate_image(label, image_filename, mask_filename)
|
|
|
|
def _generate_dummy_kolektor_dataset(self) -> None:
|
|
"""Generate dummy Kolektor dataset in directory using the same convention as Kolektor AD."""
|
|
# Emulating the first two categories of Kolektor dataset.
|
|
for category in ("kos01", "kos02"):
|
|
for i in range(self.num_train * 2):
|
|
# Half of the images are normal, while the rest are abnormal.
|
|
label = LabelName.NORMAL if i > self.num_train // 2 else LabelName.ABNORMAL
|
|
image_filename = self.dataset_root / category / f"Part{i}.jpg"
|
|
mask_filename = self.dataset_root / category / f"Part{i}_label.bmp"
|
|
self.image_generator.generate_image(label, image_filename, mask_filename)
|
|
|
|
def _generate_dummy_realiad_dataset(self) -> None:
|
|
"""Generate dummy RealIAD dataset in directory using the same convention as RealIAD."""
|
|
import json
|
|
|
|
# Create the resolution directory
|
|
resolution_dir = self.dataset_root / "realiad_256"
|
|
resolution_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create category directory
|
|
category = "audiojack"
|
|
category_dir = resolution_dir / category
|
|
category_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create jsons directory structure
|
|
jsons_dir = self.dataset_root / "realiad_jsons" / "realiad_jsons"
|
|
jsons_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate images and create metadata
|
|
metadata = {"train": [], "test": []}
|
|
image_generator = DummyImageGenerator(image_shape=self.image_shape, rng=self.rng)
|
|
|
|
# Generate normal train images
|
|
for i in range(self.num_train):
|
|
image, _ = image_generator.generate_normal_image()
|
|
filename = f"{category}_{i:04d}_OK_C0_0000.png"
|
|
image_path = category_dir / filename
|
|
image_generator.save_image(image_path, image)
|
|
|
|
# Add to metadata - note: these are relative paths from category dir
|
|
metadata["train"].append({
|
|
"image_path": filename,
|
|
"mask_path": "",
|
|
"anomaly_class": "OK",
|
|
"camera_view": "C0",
|
|
"timestamp": "0000",
|
|
})
|
|
|
|
# Generate abnormal test images with masks
|
|
for i in range(self.num_test):
|
|
image, mask = image_generator.generate_abnormal_image()
|
|
|
|
# Save abnormal images
|
|
filename = f"{category}_{i:04d}_NG_C0_0000.png"
|
|
mask_filename = f"{category}_{i:04d}_NG_C0_0000_mask.png"
|
|
|
|
image_path = category_dir / filename
|
|
mask_path = category_dir / mask_filename
|
|
|
|
image_generator.save_image(image_path, image)
|
|
|
|
# Convert mask to uint8 before saving
|
|
# Ensure mask is in range [0, 255]
|
|
mask = (mask * 255).astype(np.uint8)
|
|
image_generator.save_image(mask_path, mask)
|
|
|
|
# Add to metadata - note: these are relative paths from category dir
|
|
metadata["test"].append({
|
|
"image_path": filename,
|
|
"mask_path": mask_filename,
|
|
"anomaly_class": "NG",
|
|
"camera_view": "C0",
|
|
"timestamp": "0000",
|
|
})
|
|
|
|
# Save metadata JSON file
|
|
json_path = jsons_dir / f"{category}.json"
|
|
with json_path.open("w") as f:
|
|
json.dump(metadata, f, indent=2)
|
|
|
|
def _generate_dummy_mvtecad2_dataset(
|
|
self,
|
|
normal_dir: str = "good",
|
|
abnormal_dir: str = "bad",
|
|
image_extension: str = ".png",
|
|
mask_suffix: str = "_mask",
|
|
mask_extension: str = ".png",
|
|
) -> None:
|
|
"""Generate a dummy MVTec AD 2 dataset.
|
|
|
|
Args:
|
|
normal_dir (str, optional): Name of the normal directory. Defaults to "good".
|
|
abnormal_dir (str, optional): Name of the abnormal directory. Defaults to "bad".
|
|
image_extension (str, optional): Extension of the image files. Defaults to ".png".
|
|
mask_suffix (str, optional): Suffix to append to mask filenames. Defaults to "_mask".
|
|
mask_extension (str, optional): Extension of the mask files. Defaults to ".png".
|
|
"""
|
|
# MVTec AD 2 has multiple subcategories within the dataset
|
|
dataset_category = "dummy"
|
|
category_root = self.dataset_root / dataset_category
|
|
|
|
# Create train directory with normal images
|
|
train_path = category_root / "train" / normal_dir
|
|
for i in range(self.num_train):
|
|
image_path = train_path / f"{i:03d}_regular{image_extension}"
|
|
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_path)
|
|
|
|
# Create validation directory with normal images
|
|
val_path = category_root / "validation" / normal_dir
|
|
for i in range(self.num_test):
|
|
image_path = val_path / f"{i:03d}_regular{image_extension}"
|
|
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_path)
|
|
|
|
# Create public test directory with normal and abnormal images
|
|
test_public_path = category_root / "test_public"
|
|
|
|
# Normal test images
|
|
test_normal_path = test_public_path / normal_dir
|
|
for i in range(self.num_test):
|
|
image_path = test_normal_path / f"{i:03d}_regular{image_extension}"
|
|
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_path)
|
|
|
|
# Abnormal test images with masks
|
|
test_abnormal_path = test_public_path / abnormal_dir
|
|
test_mask_path = test_public_path / "ground_truth" / abnormal_dir
|
|
for i in range(self.num_test):
|
|
image_path = test_abnormal_path / f"{i:03d}_regular{image_extension}"
|
|
mask_path = test_mask_path / f"{i:03d}_regular{mask_suffix}{mask_extension}"
|
|
self.image_generator.generate_image(
|
|
label=LabelName.ABNORMAL,
|
|
image_filename=image_path,
|
|
mask_filename=mask_path,
|
|
)
|
|
|
|
# Create private test directory with unknown images
|
|
test_private_path = category_root / "test_private"
|
|
for i in range(self.num_test):
|
|
image_path = test_private_path / f"{i:03d}_regular{image_extension}"
|
|
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_path)
|
|
|
|
# Create private mixed test directory with unknown images
|
|
test_private_mixed_path = category_root / "test_private_mixed"
|
|
for i in range(self.num_test):
|
|
image_path = test_private_mixed_path / f"{i:03d}_regular{image_extension}"
|
|
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_path)
|
|
|
|
def _generate_dummy_vad_dataset(
|
|
self,
|
|
normal_dir: str = "good",
|
|
abnormal_dir: str = "bad",
|
|
image_extension: str = ".png",
|
|
) -> None:
|
|
"""Generates dummy VAD dataset in a temporary directory."""
|
|
# VAD has a single subcategory within the dataset.
|
|
dataset_category = "vad"
|
|
|
|
# Create normal images.
|
|
for split in ("train", "test"):
|
|
path = self.dataset_root / dataset_category / split / normal_dir
|
|
num_images = self.num_train if split == "train" else self.num_test
|
|
for i in range(num_images):
|
|
label = LabelName.NORMAL
|
|
image_filename = path / f"{i:03}{image_extension}"
|
|
self.image_generator.generate_image(label=label, image_filename=image_filename)
|
|
|
|
# Create abnormal test images and masks.
|
|
abnormal_dir = abnormal_dir or self.abnormal_category
|
|
path = self.dataset_root / dataset_category / "test" / abnormal_dir
|
|
|
|
for i in range(self.num_test):
|
|
label = LabelName.ABNORMAL
|
|
image_filename = path / f"{i:03}{image_extension}"
|
|
self.image_generator.generate_image(label, image_filename)
|
|
|
|
def _generate_dummy_visa_dataset(self) -> None:
|
|
"""Generate dummy Visa dataset in directory using the same convention as Visa AD."""
|
|
# Visa dataset on anomalib follows the same convention as MVTec AD.
|
|
# The only difference is that the root directory has a subdirectory called "visa_pytorch".
|
|
self.dataset_root = self.dataset_root.parent / "visa_pytorch"
|
|
self._generate_dummy_mvtecad_dataset(normal_dir="good", abnormal_dir="bad", image_extension=".jpg")
|
|
|
|
|
|
class DummyVideoDatasetGenerator(DummyDatasetGenerator):
|
|
"""Dummy video dataset generator.
|
|
|
|
Args:
|
|
data_format (DataFormat): Data format of the dataset.
|
|
root (Path | str | None, optional): Root directory to save the dataset. Defaults to None.
|
|
dataset_name (str, optional): Name of the dataset. Defaults to "ucsdped1".
|
|
num_frames (int, optional): Number of frames to generate the video. Defaults to 32.
|
|
frame_shape (tuple[int, int], optional): Shape of individual frames. Defaults to (256, 256).
|
|
num_train (int, optional): Number of training images to generate. Defaults to 5.
|
|
num_test (int, optional): Number of testing images to generate per category. Defaults to 5.
|
|
seed (int, optional): Fixes seed if any number greater than 0 is provided. 0 means no seed. Defaults to 0.
|
|
|
|
Examples:
|
|
To create a UCSDped1 dataset with 10 training videos and 10 testing videos, use the following code.
|
|
>>> dataset_generator = DummyVideoDatasetGenerator(data_format="ucsdped", num_train=10, num_test=10)
|
|
>>> dataset_generator.generate_dataset()
|
|
|
|
In order to provide a specific directory to save the dataset, use the ``root`` argument.
|
|
>>> dataset_generator = DummyVideoDatasetGenerator(data_format="ucsdped", root="./datasets/dummy")
|
|
>>> dataset_generator.generate_dataset()
|
|
|
|
It is also possible to use the generator as a context manager.
|
|
>>> with DummyVideoDatasetGenerator(data_format="ucsdped", num_train=10, num_test=10) as dataset_path:
|
|
>>> some_function()
|
|
|
|
To get the list of available datasets, use the ``VideoDataFormat`` enum.
|
|
>>> from anomalib.data import VideoDataFormat
|
|
>>> print(list(VideoDataFormat))
|
|
|
|
Based on the enum, you can generate the dataset.
|
|
>>> dataset_generator = DummyVideoDatasetGenerator(data_format="avenue", num_train=10, num_test=10)
|
|
>>> dataset_generator.generate_dataset()
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
data_format: DataFormat,
|
|
root: Path | str | None = None,
|
|
num_frames: int = 32,
|
|
frame_shape: tuple[int, int] = (256, 256),
|
|
num_train: int = 5,
|
|
num_test: int = 5,
|
|
seed: int | None = None,
|
|
) -> None:
|
|
super().__init__(
|
|
data_format=data_format,
|
|
root=root,
|
|
num_train=num_train,
|
|
num_test=num_test,
|
|
seed=seed,
|
|
)
|
|
self.video_length = num_frames
|
|
self.frame_shape = frame_shape
|
|
self.video_generator = DummyVideoGenerator(num_frames=num_frames, frame_shape=frame_shape)
|
|
|
|
def _generate_dummy_ucsdped_dataset(self, train_dir: str = "Train", test_dir: str = "Test") -> None:
|
|
"""Generate dummy UCSD dataset."""
|
|
# generate training data
|
|
dataset_category = "dummy"
|
|
train_path = self.dataset_root / dataset_category / train_dir
|
|
for clip_idx in range(self.num_train):
|
|
clip_name = train_path / f"Train{clip_idx:03}"
|
|
frames, _ = self.video_generator.generate_video(
|
|
length=self.video_length,
|
|
first_label=LabelName.NORMAL,
|
|
p_state_switch=0,
|
|
)
|
|
for frame_idx, frame in enumerate(frames):
|
|
filename = clip_name / f"{frame_idx:03}.tif"
|
|
self.video_generator.save_frame(filename, frame)
|
|
|
|
# generate test data
|
|
test_path = self.dataset_root / dataset_category / test_dir
|
|
for clip_idx in range(self.num_test):
|
|
clip_path = test_path / f"Test{clip_idx:03}"
|
|
mask_path = test_path / f"Test{clip_idx:03}_gt"
|
|
frames, masks = self.video_generator.generate_video(length=self.video_length, p_state_switch=0.2)
|
|
for frame_idx, (frame, mask) in enumerate(zip(frames, masks, strict=True)):
|
|
filename_frame = clip_path / f"{frame_idx:03}.tif"
|
|
filename_mask = mask_path / f"{frame_idx:03}.bmp"
|
|
self.video_generator.save_frame(filename_frame, frame)
|
|
self.video_generator.save_frame(filename_mask, (mask * 255).astype(np.uint8))
|
|
|
|
def _generate_dummy_avenue_dataset(
|
|
self,
|
|
train_dir: str = "training_videos",
|
|
test_dir: str = "testing_videos",
|
|
ground_truth_dir: str = "ground_truth_demo",
|
|
) -> None:
|
|
"""Generate dummy Avenue dataset."""
|
|
# generate training data
|
|
train_path = self.dataset_root / train_dir
|
|
train_path.mkdir(exist_ok=True, parents=True)
|
|
for clip_idx in range(self.num_train):
|
|
clip_path = train_path / f"{clip_idx + 1:02}.avi"
|
|
frames, _ = self.video_generator.generate_video(length=32, first_label=LabelName.NORMAL, p_state_switch=0)
|
|
fourcc = cv2.VideoWriter_fourcc("F", "M", "P", "4")
|
|
writer = cv2.VideoWriter(str(clip_path), fourcc, 30, self.frame_shape)
|
|
for _, frame in enumerate(frames):
|
|
writer.write(frame)
|
|
writer.release()
|
|
|
|
# generate test data
|
|
test_path = self.dataset_root / test_dir
|
|
test_path.mkdir(exist_ok=True, parents=True)
|
|
gt_path = self.dataset_root / ground_truth_dir / "testing_label_mask"
|
|
|
|
for clip_idx in range(self.num_test):
|
|
clip_path = test_path / f"{clip_idx + 1:02}.avi"
|
|
mask_path = gt_path / f"{clip_idx + 1}_label"
|
|
mask_path.mkdir(exist_ok=True, parents=True)
|
|
frames, masks = self.video_generator.generate_video(length=32, p_state_switch=0.2)
|
|
fourcc = cv2.VideoWriter_fourcc("F", "M", "P", "4")
|
|
writer = cv2.VideoWriter(str(clip_path), fourcc, 30, self.frame_shape)
|
|
for frame_idx, (frame, mask) in enumerate(zip(frames, masks, strict=True)):
|
|
writer.write(frame)
|
|
mask_filename = mask_path / f"{frame_idx:04}.png"
|
|
self.video_generator.save_image(mask_filename, (mask).astype(np.uint8))
|
|
masks_array = np.stack(masks)
|
|
mat_filename = mask_path.with_suffix(".mat")
|
|
savemat(mat_filename, {"data": masks_array})
|
|
|
|
def _generate_dummy_shanghaitech_dataset(
|
|
self,
|
|
train_dir: str = "training",
|
|
test_dir: str = "testing",
|
|
) -> None:
|
|
"""Generate dummy ShanghaiTech dataset."""
|
|
# generate training data
|
|
path = self.dataset_root / train_dir / "converted_videos"
|
|
path.mkdir(exist_ok=True, parents=True)
|
|
num_clips = self.num_train
|
|
for clip_idx in range(num_clips):
|
|
clip_path = path / f"01_{clip_idx:03}.avi"
|
|
frames, _ = self.video_generator.generate_video(length=32, first_label=LabelName.NORMAL, p_state_switch=0)
|
|
fourcc = cv2.VideoWriter_fourcc("F", "M", "P", "4")
|
|
writer = cv2.VideoWriter(str(clip_path), fourcc, 30, self.frame_shape)
|
|
for _, frame in enumerate(frames):
|
|
writer.write(frame)
|
|
writer.release()
|
|
|
|
# generate test data
|
|
test_path = self.dataset_root / test_dir / "frames"
|
|
test_path.mkdir(exist_ok=True, parents=True)
|
|
gt_path = self.dataset_root / test_dir / "test_pixel_mask"
|
|
gt_path.mkdir(exist_ok=True, parents=True)
|
|
|
|
for clip_idx in range(self.num_test):
|
|
clip_path = test_path / f"01_{clip_idx:04}"
|
|
clip_path.mkdir(exist_ok=True, parents=True)
|
|
mask_path = gt_path / f"01_{clip_idx:04}.npy"
|
|
frames, masks = self.video_generator.generate_video(length=32, p_state_switch=0.2)
|
|
for frame_idx, frame in enumerate(frames):
|
|
image_filename = clip_path / f"{frame_idx:03}.jpg"
|
|
self.video_generator.save_image(image_filename, frame)
|
|
masks_array = np.stack(masks)
|
|
np.save(mask_path, masks_array)
|