Adding a New Dataset / DataModule
anomalib splits data support into two layers per source, both under src/anomalib/data/:
datasets/image/<name>.py — a torch-facing AnomalibDataset subclass (one dataset = one split).
datamodules/image/<name>.py — a Lightning-facing AnomalibDataModule subclass that owns train/val/test
dataloaders and split logic.
(Use datasets/video/ and datamodules/video/, or depth/, for other modalities — the pattern is identical.)
Base classes to implement against
AnomalibDataset — src/anomalib/data/datasets/base/image.py
__init__(self, augmentations=None) — call via super().__init__(...).
- You must build a
pandas.DataFrame and assign it to self.samples. Required columns:
image_path, split, label_index (0 for normal, 1 for abnormal); segmentation datasets also
need mask_path (set to empty string "" for normal samples). After building the DataFrame, set
samples.attrs["task"] to "classification" or "segmentation".
collate_fn defaults to ImageBatch.collate; override only for non-image batch types.
AnomalibDataModule — src/anomalib/data/datamodules/base/image.py
- Only abstract method you must implement:
_setup(self, _stage=None) -> None, where you set
self.train_data and self.test_data (and self.val_data if you don't rely on the base class's
val_split_mode machinery).
- The base class already implements
setup(), train_dataloader(), val_dataloader(),
test_dataloader(), and from_config() (jsonargparse subclass integration) — do not override these
unless the data source genuinely needs custom dataloader construction.
- Constructor should accept and forward:
train_batch_size, eval_batch_size, num_workers,
train_augmentations / val_augmentations / test_augmentations / augmentations,
test_split_mode / test_split_ratio, val_split_mode / val_split_ratio, seed.
Reference: MVTecAD (standard benchmark-style dataset)
src/anomalib/data/datasets/image/mvtecad.py — MVTecADDataset(AnomalibDataset); builds self.samples
via the make_mvtec_ad_dataset(root_category, split, extensions) helper.
src/anomalib/data/datamodules/image/mvtecad.py — MVTecAD(AnomalibDataModule); _setup() constructs
MVTecADDataset(split=Split.TRAIN, root=self.root, category=self.category) for train/test, and
prepare_data() downloads the dataset archive if missing.
Reference: Folder (generic custom-folder dataset — use this as your template for ad hoc data)
src/anomalib/data/datasets/image/folder.py — FolderDataset(AnomalibDataset), built via the
make_folder_dataset(...) helper: collects filenames/labels from directories, builds the samples
DataFrame, and attaches mask paths to abnormal samples when mask_dir is given.
src/anomalib/data/datamodules/image/folder.py — Folder(AnomalibDataModule) constructor (key args):
Folder(
name: str, # required, becomes datamodule.name
normal_dir: str | Path | Sequence[str | Path], # required
root: str | Path | None = None,
abnormal_dir: str | Path | Sequence[str | Path] | None = None,
normal_test_dir: str | Path | Sequence[str | Path] | None = None, # separate normal images for test set
mask_dir: str | Path | Sequence[str | Path] | None = None, # for segmentation masks
normal_split_ratio: float = 0.2,
extensions: tuple[str] | None = None,
train_batch_size: int = 32,
eval_batch_size: int = 32,
num_workers: int = 8,
train_augmentations: Transform | None = None,
val_augmentations: Transform | None = None,
test_augmentations: Transform | None = None,
augmentations: Transform | 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: int | None = None,
)
Note: test_split_ratio (inherited from AnomalibDataModule) controls the fraction of training
images held out for testing when test_split_mode triggers a synthetic split. normal_split_ratio
is stored by Folder but used only by FolderDataset internally to split normal images between
train and test sets when normal_test_dir is not provided and test data must come from the normal
pool.
Use Folder directly (no new code needed) whenever the data is already laid out as
root/normal_dir/*, root/abnormal_dir/*, optionally root/mask_dir/*. Only write a brand-new
dataset/datamodule pair when the data needs custom parsing logic Folder can't express.
Writing a brand-new datamodule (skeleton)
# src/anomalib/data/datasets/image/my_dataset.py
from anomalib.data.datasets.base import AnomalibDataset
class MyDataset(AnomalibDataset):
def __init__(self, root=None, augmentations=None, split=None):
super().__init__(augmentations=augmentations)
samples = make_my_dataset_samples(root=root, split=split) # build the DataFrame yourself
# DataFrame must have columns: image_path, split, label_index (and mask_path for segmentation)
samples.attrs["task"] = "segmentation" # or "classification"
self.samples = samples
# src/anomalib/data/datamodules/image/my_dataset.py
from pathlib import Path
from anomalib.data.datamodules.base.image import AnomalibDataModule
from anomalib.data.datasets.image.my_dataset import MyDataset
from anomalib.data.utils import Split
class MyDataModule(AnomalibDataModule):
def __init__(
self,
root: str | Path = "./datasets/MyDataset",
train_batch_size: int = 32,
eval_batch_size: int = 32,
num_workers: int = 8,
train_augmentations=None,
val_augmentations=None,
test_augmentations=None,
augmentations=None,
test_split_mode=None,
test_split_ratio: float = 0.2,
val_split_mode=None,
val_split_ratio: float = 0.5,
seed: int | None = None,
) -> None:
super().__init__(
train_batch_size=train_batch_size, eval_batch_size=eval_batch_size,
num_workers=num_workers, train_augmentations=train_augmentations,
val_augmentations=val_augmentations, test_augmentations=test_augmentations,
augmentations=augmentations, test_split_mode=test_split_mode,
test_split_ratio=test_split_ratio, val_split_mode=val_split_mode,
val_split_ratio=val_split_ratio, seed=seed,
)
self.root = Path(root)
def _setup(self, _stage=None) -> None:
self.train_data = MyDataset(split=Split.TRAIN, root=self.root)
self.test_data = MyDataset(split=Split.TEST, root=self.root)
def prepare_data(self) -> None:
... # optional: download/validate on rank-zero
Registration — how the datamodule becomes discoverable
Export from the image (or video/depth) package __init__.py —
src/anomalib/data/datamodules/image/__init__.py: add the import and __all__ entry there first.
Then add the import and __all__ entry in src/anomalib/data/__init__.py, alongside the existing
datamodules.image import block:
from .datamodules.image import (
...,
MyDataModule,
)
Once exported, it is usable as anomalib.data.MyDataModule, and from the CLI:
anomalib train --model Patchcore --data anomalib.data.MyDataModule --data.root ./datasets/mine.
Tests
Add tests/unit/data/datamodule/image/test_my_dataset.py following the pattern in
tests/unit/data/datamodule/image/test_mvtec_ad.py: a datamodule fixture that instantiates the
datamodule against a generated dummy dataset, calls prepare_data() + setup(), then reuses the
shared assertions in tests/unit/data/datamodule/base/image.py (batch shapes, split non-overlap, etc.).
Add a dummy dataset generator (required for a new DataFormat)
Real datasets aren't checked into the repo — tests generate synthetic data on the fly via
tests/helpers/data.py. If your new datamodule corresponds to a new DataFormat value (i.e. it isn't
just Folder under another name), you must add a matching generator method:
Add the format to ImageDataFormat (or VideoDataFormat) in
src/anomalib/data/datamodules/image/__init__.py (or .../video/__init__.py),
e.g. MY_DATASET = "my_dataset".
Implement _generate_dummy_my_dataset_dataset(self) -> None on DummyImageDatasetGenerator
(tests/helpers/data.py) for image datasets, or DummyVideoDatasetGenerator for video datasets —
the method name must be _generate_dummy_{data_format.value}_dataset;
DummyDatasetGenerator.generate_dataset() dispatches to it via getattr. Build the on-disk layout
your datamodule expects using the low-level DummyImageGenerator
(tests/helpers/data.py::DummyImageGenerator) for image datasets, or DummyVideoGenerator for
video datasets:
def _generate_dummy_my_dataset_dataset(self) -> None:
"""Generate dummy MyDataset dataset in a temporary directory."""
dataset_category = "dummy"
# normal train/test images
for split in ("train", "test"):
path = self.dataset_root / dataset_category / split / self.normal_category
num_images = self.num_train if split == "train" else self.num_test
for i in range(num_images):
image_filename = path / f"{i:03}.png"
self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_filename)
# abnormal test images + masks
path = self.dataset_root / dataset_category / "test" / self.abnormal_category
mask_path = self.dataset_root / dataset_category / "ground_truth" / self.abnormal_category
for i in range(self.num_test):
image_filename = path / f"{i:03}.png"
mask_filename = mask_path / f"{i:03}_mask.png"
self.image_generator.generate_image(LabelName.ABNORMAL, image_filename, mask_filename)
See _generate_dummy_mvtecad_dataset and _generate_dummy_folder_dataset in the same file for the
two canonical layouts (category-per-split-per-class vs. flat normal/abnormal/mask dirs) — mirror
whichever matches your real dataset's directory structure.
The session-scoped dataset_path fixture in tests/conftest.py dispatches image formats to
DummyImageDatasetGenerator and video formats to DummyVideoDatasetGenerator (skipping folder/
tabular, which tests construct manually) — you only need to implement the _generate_dummy_*
method on the appropriate generator class.
In your datamodule test, consume the generated data via the shared fixture:
@pytest.fixture()
def datamodule(dataset_path: Path) -> MyDataModule:
dm = MyDataModule(root=dataset_path / "my_dataset")
dm.prepare_data()
dm.setup()
return dm
If your datamodule is just a thin wrapper around Folder (same on-disk convention, different
defaults), you don't need a new DataFormat/generator — reuse _generate_dummy_folder_dataset and
construct your datamodule directly against its output directory.
Gotchas
self.samples must be assigned (not mutated in place before assignment) — the samples setter on
AnomalibDataset validates required columns and paths.
- Don't skip
samples.attrs["task"] — post-processing and metrics branch on "classification" vs
"segmentation".
- Prefer
Folder over a new dataset class whenever the on-disk layout is a plain normal/abnormal/mask
directory split — writing a new class is only needed for non-standard parsing.
- Tests never touch real downloaded datasets. If you add a new
DataFormat, you must also add a
_generate_dummy_<format>_dataset method on the appropriate generator (DummyImageDatasetGenerator
for image formats, DummyVideoDatasetGenerator for video formats) — otherwise the shared
dataset_path fixture (tests/conftest.py) will raise NotImplementedError for that format.
Reviewer / self-check before opening a PR
1---2name: anomalib-adding-a-datamodule3description: Adds a new dataset/datamodule to anomalib under src/anomalib/data/. Use when wiring a new data source into the AnomalibDataset/AnomalibDataModule base classes, exporting it so anomalib.data.<Name> and the CLI/config (jsonargparse) can discover it, and adding matching tests. Do not use for model architecture work (see anomalib-adding-a-model) or for training an existing datamodule (see anomalib-training).4license: Apache-2.05---67# Adding a New Dataset / DataModule89anomalib splits data support into two layers per source, both under `src/anomalib/data/`:1011- `datasets/image/<name>.py` — a `torch`-facing `AnomalibDataset` subclass (one dataset = one split).12- `datamodules/image/<name>.py` — a Lightning-facing `AnomalibDataModule` subclass that owns train/val/test13 dataloaders and split logic.1415(Use `datasets/video/` and `datamodules/video/`, or `depth/`, for other modalities — the pattern is identical.)1617## Base classes to implement against1819- `AnomalibDataset` — `src/anomalib/data/datasets/base/image.py`20 - `__init__(self, augmentations=None)` — call via `super().__init__(...)`.21 - You must build a `pandas.DataFrame` and assign it to `self.samples`. Required columns:22 `image_path`, `split`, `label_index` (0 for normal, 1 for abnormal); segmentation datasets also23 need `mask_path` (set to empty string `""` for normal samples). After building the DataFrame, set24 `samples.attrs["task"]` to `"classification"` or `"segmentation"`.25 - `collate_fn` defaults to `ImageBatch.collate`; override only for non-image batch types.26- `AnomalibDataModule` — `src/anomalib/data/datamodules/base/image.py`27 - Only abstract method you must implement: `_setup(self, _stage=None) -> None`, where you set28 `self.train_data` and `self.test_data` (and `self.val_data` if you don't rely on the base class's29 `val_split_mode` machinery).30 - The base class already implements `setup()`, `train_dataloader()`, `val_dataloader()`,31 `test_dataloader()`, and `from_config()` (jsonargparse subclass integration) — do not override these32 unless the data source genuinely needs custom dataloader construction.33 - Constructor should accept and forward: `train_batch_size`, `eval_batch_size`, `num_workers`,34 `train_augmentations` / `val_augmentations` / `test_augmentations` / `augmentations`,35 `test_split_mode` / `test_split_ratio`, `val_split_mode` / `val_split_ratio`, `seed`.3637## Reference: MVTecAD (standard benchmark-style dataset)3839- `src/anomalib/data/datasets/image/mvtecad.py` — `MVTecADDataset(AnomalibDataset)`; builds `self.samples`40 via the `make_mvtec_ad_dataset(root_category, split, extensions)` helper.41- `src/anomalib/data/datamodules/image/mvtecad.py` — `MVTecAD(AnomalibDataModule)`; `_setup()` constructs42 `MVTecADDataset(split=Split.TRAIN, root=self.root, category=self.category)` for train/test, and43 `prepare_data()` downloads the dataset archive if missing.4445## Reference: Folder (generic custom-folder dataset — use this as your template for ad hoc data)4647- `src/anomalib/data/datasets/image/folder.py` — `FolderDataset(AnomalibDataset)`, built via the48 `make_folder_dataset(...)` helper: collects filenames/labels from directories, builds the `samples`49 DataFrame, and attaches mask paths to abnormal samples when `mask_dir` is given.50- `src/anomalib/data/datamodules/image/folder.py` — `Folder(AnomalibDataModule)` constructor (key args):5152 ```python53 Folder(54 name: str, # required, becomes datamodule.name55 normal_dir: str | Path | Sequence[str | Path], # required56 root: str | Path | None = None,57 abnormal_dir: str | Path | Sequence[str | Path] | None = None,58 normal_test_dir: str | Path | Sequence[str | Path] | None = None, # separate normal images for test set59 mask_dir: str | Path | Sequence[str | Path] | None = None, # for segmentation masks60 normal_split_ratio: float = 0.2,61 extensions: tuple[str] | None = None,62 train_batch_size: int = 32,63 eval_batch_size: int = 32,64 num_workers: int = 8,65 train_augmentations: Transform | None = None,66 val_augmentations: Transform | None = None,67 test_augmentations: Transform | None = None,68 augmentations: Transform | None = None,69 test_split_mode: TestSplitMode = TestSplitMode.FROM_DIR,70 test_split_ratio: float = 0.2,71 val_split_mode: ValSplitMode = ValSplitMode.FROM_TEST,72 val_split_ratio: float = 0.5,73 seed: int | None = None,74 )75 ```7677 Note: `test_split_ratio` (inherited from `AnomalibDataModule`) controls the fraction of training78 images held out for testing when `test_split_mode` triggers a synthetic split. `normal_split_ratio`79 is stored by `Folder` but used only by `FolderDataset` internally to split normal images between80 train and test sets when `normal_test_dir` is not provided and test data must come from the normal81 pool.8283Use `Folder` directly (no new code needed) whenever the data is already laid out as84`root/normal_dir/*`, `root/abnormal_dir/*`, optionally `root/mask_dir/*`. Only write a brand-new85dataset/datamodule pair when the data needs custom parsing logic `Folder` can't express.8687## Writing a brand-new datamodule (skeleton)8889```python90# src/anomalib/data/datasets/image/my_dataset.py91from anomalib.data.datasets.base import AnomalibDataset9293class MyDataset(AnomalibDataset):94 def __init__(self, root=None, augmentations=None, split=None):95 super().__init__(augmentations=augmentations)96 samples = make_my_dataset_samples(root=root, split=split) # build the DataFrame yourself97 # DataFrame must have columns: image_path, split, label_index (and mask_path for segmentation)98 samples.attrs["task"] = "segmentation" # or "classification"99 self.samples = samples100```101102```python103# src/anomalib/data/datamodules/image/my_dataset.py104from pathlib import Path105106from anomalib.data.datamodules.base.image import AnomalibDataModule107from anomalib.data.datasets.image.my_dataset import MyDataset108from anomalib.data.utils import Split109110class MyDataModule(AnomalibDataModule):111 def __init__(112 self,113 root: str | Path = "./datasets/MyDataset",114 train_batch_size: int = 32,115 eval_batch_size: int = 32,116 num_workers: int = 8,117 train_augmentations=None,118 val_augmentations=None,119 test_augmentations=None,120 augmentations=None,121 test_split_mode=None,122 test_split_ratio: float = 0.2,123 val_split_mode=None,124 val_split_ratio: float = 0.5,125 seed: int | None = None,126 ) -> None:127 super().__init__(128 train_batch_size=train_batch_size, eval_batch_size=eval_batch_size,129 num_workers=num_workers, train_augmentations=train_augmentations,130 val_augmentations=val_augmentations, test_augmentations=test_augmentations,131 augmentations=augmentations, test_split_mode=test_split_mode,132 test_split_ratio=test_split_ratio, val_split_mode=val_split_mode,133 val_split_ratio=val_split_ratio, seed=seed,134 )135 self.root = Path(root)136137 def _setup(self, _stage=None) -> None:138 self.train_data = MyDataset(split=Split.TRAIN, root=self.root)139 self.test_data = MyDataset(split=Split.TEST, root=self.root)140141 def prepare_data(self) -> None:142 ... # optional: download/validate on rank-zero143```144145## Registration — how the datamodule becomes discoverable1461471. Export from the image (or video/depth) package `__init__.py` —148 `src/anomalib/data/datamodules/image/__init__.py`: add the import and `__all__` entry there first.1491502. Then add the import and `__all__` entry in `src/anomalib/data/__init__.py`, alongside the existing151 `datamodules.image` import block:152153```python154from .datamodules.image import (155 ...,156 MyDataModule,157)158```159160Once exported, it is usable as `anomalib.data.MyDataModule`, and from the CLI:161`anomalib train --model Patchcore --data anomalib.data.MyDataModule --data.root ./datasets/mine`.162163## Tests164165Add `tests/unit/data/datamodule/image/test_my_dataset.py` following the pattern in166`tests/unit/data/datamodule/image/test_mvtec_ad.py`: a `datamodule` fixture that instantiates the167datamodule against a generated dummy dataset, calls `prepare_data()` + `setup()`, then reuses the168shared assertions in `tests/unit/data/datamodule/base/image.py` (batch shapes, split non-overlap, etc.).169170### Add a dummy dataset generator (required for a new `DataFormat`)171172Real datasets aren't checked into the repo — tests generate synthetic data on the fly via173`tests/helpers/data.py`. If your new datamodule corresponds to a new `DataFormat` value (i.e. it isn't174just `Folder` under another name), you must add a matching generator method:1751761. Add the format to `ImageDataFormat` (or `VideoDataFormat`) in177 `src/anomalib/data/datamodules/image/__init__.py` (or `.../video/__init__.py`),178 e.g. `MY_DATASET = "my_dataset"`.1792. Implement `_generate_dummy_my_dataset_dataset(self) -> None` on `DummyImageDatasetGenerator`180 (`tests/helpers/data.py`) for image datasets, or `DummyVideoDatasetGenerator` for video datasets —181 the method name must be `_generate_dummy_{data_format.value}_dataset`;182 `DummyDatasetGenerator.generate_dataset()` dispatches to it via `getattr`. Build the on-disk layout183 your datamodule expects using the low-level `DummyImageGenerator`184 (`tests/helpers/data.py::DummyImageGenerator`) for image datasets, or `DummyVideoGenerator` for185 video datasets:186187 ```python188 def _generate_dummy_my_dataset_dataset(self) -> None:189 """Generate dummy MyDataset dataset in a temporary directory."""190 dataset_category = "dummy"191 # normal train/test images192 for split in ("train", "test"):193 path = self.dataset_root / dataset_category / split / self.normal_category194 num_images = self.num_train if split == "train" else self.num_test195 for i in range(num_images):196 image_filename = path / f"{i:03}.png"197 self.image_generator.generate_image(label=LabelName.NORMAL, image_filename=image_filename)198199 # abnormal test images + masks200 path = self.dataset_root / dataset_category / "test" / self.abnormal_category201 mask_path = self.dataset_root / dataset_category / "ground_truth" / self.abnormal_category202 for i in range(self.num_test):203 image_filename = path / f"{i:03}.png"204 mask_filename = mask_path / f"{i:03}_mask.png"205 self.image_generator.generate_image(LabelName.ABNORMAL, image_filename, mask_filename)206 ```207208 See `_generate_dummy_mvtecad_dataset` and `_generate_dummy_folder_dataset` in the same file for the209 two canonical layouts (category-per-split-per-class vs. flat normal/abnormal/mask dirs) — mirror210 whichever matches your real dataset's directory structure.2112123. The session-scoped `dataset_path` fixture in `tests/conftest.py` dispatches image formats to213 `DummyImageDatasetGenerator` and video formats to `DummyVideoDatasetGenerator` (skipping `folder`/214 `tabular`, which tests construct manually) — you only need to implement the `_generate_dummy_*`215 method on the appropriate generator class.2164. In your datamodule test, consume the generated data via the shared fixture:217218 ```python219 @pytest.fixture()220 def datamodule(dataset_path: Path) -> MyDataModule:221 dm = MyDataModule(root=dataset_path / "my_dataset")222 dm.prepare_data()223 dm.setup()224 return dm225 ```226227If your datamodule is just a thin wrapper around `Folder` (same on-disk convention, different228defaults), you don't need a new `DataFormat`/generator — reuse `_generate_dummy_folder_dataset` and229construct your datamodule directly against its output directory.230231## Gotchas232233- `self.samples` must be assigned (not mutated in place before assignment) — the `samples` setter on234 `AnomalibDataset` validates required columns and paths.235- Don't skip `samples.attrs["task"]` — post-processing and metrics branch on `"classification"` vs236 `"segmentation"`.237- Prefer `Folder` over a new dataset class whenever the on-disk layout is a plain normal/abnormal/mask238 directory split — writing a new class is only needed for non-standard parsing.239- Tests never touch real downloaded datasets. If you add a new `DataFormat`, you must also add a240 `_generate_dummy_<format>_dataset` method on the appropriate generator (`DummyImageDatasetGenerator`241 for image formats, `DummyVideoDatasetGenerator` for video formats) — otherwise the shared242 `dataset_path` fixture (`tests/conftest.py`) will raise `NotImplementedError` for that format.243244## Reviewer / self-check before opening a PR245246- [ ] `AnomalibDataset` subclass sets `self.samples` (DataFrame with required columns + `task` attr).247- [ ] `AnomalibDataModule` subclass implements `_setup()` only; no unnecessary overrides of248 `train_dataloader`/`val_dataloader`/`test_dataloader`.249- [ ] Datamodule exported from `src/anomalib/data/__init__.py` and `__all__` updated.250- [ ] `anomalib.data.MyDataModule` resolves and works from the CLI `--data` flag.251- [ ] Unit tests added under `tests/unit/data/datamodule/`.252- [ ] If a new `DataFormat` was introduced, a matching `_generate_dummy_*_dataset` method was added to253 `DummyImageDatasetGenerator` in `tests/helpers/data.py`.