From 8f5fa932253d78ac3cb31aeaed8d85111683e909 Mon Sep 17 00:00:00 2001 From: Dick Ameln Date: Thu, 2 May 2024 09:09:31 +0200 Subject: [PATCH] Transform behaviour+documentation (#1953) * only export eval_transform * add transform documentation * update data configs * update changelog * minor fix * Update docs/source/snippets/data/transforms/inference_cli.sh Co-authored-by: Ashwin Vaidya --------- Co-authored-by: Ashwin Vaidya --- CHANGELOG.md | 1 + configs/data/avenue.yaml | 3 +- configs/data/btech.yaml | 3 +- configs/data/folder.yaml | 3 +- configs/data/kolektor.yaml | 3 +- configs/data/mvtec.yaml | 3 +- configs/data/mvtec_3d.yaml | 3 +- configs/data/shanghaitec.yaml | 3 +- configs/data/ucsd_ped.yaml | 3 +- configs/data/visa.yaml | 3 +- .../markdown/guides/how_to/data/index.md | 8 ++ .../markdown/guides/how_to/data/transforms.md | 131 ++++++++++++++++++ .../data/transforms/datamodule_custom.txt | 17 +++ .../transforms/datamodule_custom_cli.yaml | 18 +++ .../data/transforms/datamodule_default.txt | 10 ++ .../data/transforms/datamodule_train_eval.txt | 33 +++++ .../transforms/datamodule_train_eval_cli.yaml | 42 ++++++ .../snippets/data/transforms/inference.txt | 48 +++++++ .../snippets/data/transforms/inference_cli.sh | 2 + .../data/transforms/inference_cli.yaml | 39 ++++++ .../data/transforms/model_configure.txt | 9 ++ .../snippets/data/transforms/model_fit.txt | 24 ++++ .../snippets/data/transforms/model_fit_cli.sh | 1 + src/anomalib/data/base/datamodule.py | 7 +- tests/unit/engine/test_setup_transform.py | 3 +- 25 files changed, 399 insertions(+), 21 deletions(-) create mode 100644 docs/source/markdown/guides/how_to/data/transforms.md create mode 100644 docs/source/snippets/data/transforms/datamodule_custom.txt create mode 100644 docs/source/snippets/data/transforms/datamodule_custom_cli.yaml create mode 100644 docs/source/snippets/data/transforms/datamodule_default.txt create mode 100644 docs/source/snippets/data/transforms/datamodule_train_eval.txt create mode 100644 docs/source/snippets/data/transforms/datamodule_train_eval_cli.yaml create mode 100644 docs/source/snippets/data/transforms/inference.txt create mode 100644 docs/source/snippets/data/transforms/inference_cli.sh create mode 100644 docs/source/snippets/data/transforms/inference_cli.yaml create mode 100644 docs/source/snippets/data/transforms/model_configure.txt create mode 100644 docs/source/snippets/data/transforms/model_fit.txt create mode 100644 docs/source/snippets/data/transforms/model_fit_cli.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de6d36ad2..0c6b4e5d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +- Use default model-specific eval transform when only train_transform specified by @djdameln(https://github.com/djdameln) in () - 🔨Rename OptimalF1 to F1Max for consistency with the literature, by @samet-akcay in https://github.com/openvinotoolkit/anomalib/pull/1980 - 🐞Update OptimalF1 score to use BinaryPrecisionRecallCurve and remove num_classes by @ashwinvaidya17 in https://github.com/openvinotoolkit/anomalib/pull/1972 diff --git a/configs/data/avenue.yaml b/configs/data/avenue.yaml index 6558b4da0b..396a9ba6b5 100644 --- a/configs/data/avenue.yaml +++ b/configs/data/avenue.yaml @@ -5,11 +5,10 @@ init_args: clip_length_in_frames: 1 frames_between_clips: 1 target_frame: last - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null val_split_mode: from_test diff --git a/configs/data/btech.yaml b/configs/data/btech.yaml index 574f3eca71..900f1f9738 100644 --- a/configs/data/btech.yaml +++ b/configs/data/btech.yaml @@ -2,11 +2,10 @@ class_path: anomalib.data.BTech init_args: root: "./dtasets/BTech" category: "01" - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/configs/data/folder.yaml b/configs/data/folder.yaml index 4472a884f6..329fba6520 100644 --- a/configs/data/folder.yaml +++ b/configs/data/folder.yaml @@ -8,12 +8,11 @@ init_args: mask_dir: "ground_truth/broken_large" normal_split_ratio: 0 extensions: [".png"] - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 task: segmentation + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/configs/data/kolektor.yaml b/configs/data/kolektor.yaml index 7901bfddef..1b2e6fe6b4 100644 --- a/configs/data/kolektor.yaml +++ b/configs/data/kolektor.yaml @@ -1,11 +1,10 @@ class_path: anomalib.data.Kolektor init_args: root: "./datasets/kolektor" - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/configs/data/mvtec.yaml b/configs/data/mvtec.yaml index 4d6e16fd65..7728808ece 100644 --- a/configs/data/mvtec.yaml +++ b/configs/data/mvtec.yaml @@ -2,12 +2,11 @@ class_path: anomalib.data.MVTec init_args: root: ./datasets/MVTec category: bottle - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 task: segmentation + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/configs/data/mvtec_3d.yaml b/configs/data/mvtec_3d.yaml index 4718e43bfb..d880f92f8f 100644 --- a/configs/data/mvtec_3d.yaml +++ b/configs/data/mvtec_3d.yaml @@ -2,11 +2,10 @@ class_path: anomalib.data.MVTec3D init_args: root: ./datasets/MVTec3D category: "bagel" - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/configs/data/shanghaitec.yaml b/configs/data/shanghaitec.yaml index 2ef0358f66..be4da54311 100644 --- a/configs/data/shanghaitec.yaml +++ b/configs/data/shanghaitec.yaml @@ -5,11 +5,10 @@ init_args: clip_length_in_frames: 1 frames_between_clips: 1 target_frame: LAST - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null val_split_mode: FROM_TEST diff --git a/configs/data/ucsd_ped.yaml b/configs/data/ucsd_ped.yaml index a06dd7679b..009a5ef224 100644 --- a/configs/data/ucsd_ped.yaml +++ b/configs/data/ucsd_ped.yaml @@ -5,11 +5,10 @@ init_args: clip_length_in_frames: 2 frames_between_clips: 10 target_frame: LAST - image_size: [256, 256] - transform: null train_batch_size: 8 eval_batch_size: 1 num_workers: 8 + transform: null train_transform: null eval_transform: null val_split_mode: FROM_TEST diff --git a/configs/data/visa.yaml b/configs/data/visa.yaml index af7f36a376..c5656a2158 100644 --- a/configs/data/visa.yaml +++ b/configs/data/visa.yaml @@ -2,11 +2,10 @@ class_path: anomalib.data.Visa init_args: root: "./datasets/visa" category: "capsules" - image_size: [256, 256] - transform: null train_batch_size: 32 eval_batch_size: 32 num_workers: 8 + transform: null train_transform: null eval_transform: null test_split_mode: from_dir diff --git a/docs/source/markdown/guides/how_to/data/index.md b/docs/source/markdown/guides/how_to/data/index.md index 19b95a9eae..f5d0bbc9ae 100644 --- a/docs/source/markdown/guides/how_to/data/index.md +++ b/docs/source/markdown/guides/how_to/data/index.md @@ -13,6 +13,13 @@ This section contains tutorials on how to fully utilize the data components of a Learn more about how to use `Folder` dataset to train anomalib models on your custom data. ::: +:::{grid-item-card} {octicon}`versions` Using Data Transforms. +:link: ./transforms +:link-type: doc + +Learn how to apply custom data transforms to the input images. +::: + :::{grid-item-card} {octicon}`table` Input tiling :link: ./input_tiling :link-type: doc @@ -27,5 +34,6 @@ Learn more about how to use the tiler for input tiling. :hidden: ./custom_data +./transforms ./input_tiling ``` diff --git a/docs/source/markdown/guides/how_to/data/transforms.md b/docs/source/markdown/guides/how_to/data/transforms.md new file mode 100644 index 0000000000..fffe066e4f --- /dev/null +++ b/docs/source/markdown/guides/how_to/data/transforms.md @@ -0,0 +1,131 @@ +# Data Transforms + +This tutorial will show how Anomalib applies transforms to the input images, and how these transforms can be configured. Anomalib uses the [Torchvision Transforms v2 API](https://pytorch.org/vision/main/auto_examples/transforms/plot_transforms_getting_started.html) to apply transforms to the input images. + +Common transforms are the `Resize` transform, which is used to resize the input images to a fixed width and height, and the `Normalize` transform, which normalizes the pixel values of the input images to a pre-determined range. The normalization statistics are usually chosen to correspond to the pre-training characteristics of the model's backbone. For example, when the backbone of the model was pre-trained on ImageNet dataset, it is usually recommended to normalize the model's input images to the mean and standard deviation of the pixel values of ImageNet. In addition, there are many other transforms which could be useful to achieve the desired pre-processing of the input images and to apply data augmentations during training. + +## Using custom transforms for training and evaluation + +When we create a new datamodule, it will not be equipped with any transforms by default. When we load an image from the datamodule, it will have the same shape and pixel values as the original image from the file system. + +```{literalinclude} ../../../../snippets/data/transforms/datamodule_default.txt +:language: python +``` + +Now let's create another datamodule, this time passing a simple resize transform to the datamodule using the `transform` argument. + +::::{tab-set} +:::{tab-item} API +:sync: label-1 + +```{literalinclude} ../../../../snippets/data/transforms/datamodule_custom.txt +:language: python +``` + +::: + +:::{tab-item} CLI +:sync: label-2 + +In the CLI, we can specify a custom transforms by providing the class path and init args of the Torchvision transforms class: + +```{literalinclude} ../../../../snippets/data/transforms/datamodule_custom_cli.yaml +:language: yaml +``` + +:::: + +As we can see, the datamodule now applies the custom transform when loading the images, resizing both training and test data to the specified shape. + +In the above example, we used the `transform` argument to assign a single set of transforms to be used both in the training and in the evaluation subsets. In some cases, we might want to apply distinct sets of transforms between training and evaluation. This can be useful, for example, when we want to apply random data augmentations during training to improve generalization of our model. Using different transforms for training and evaluation can be done easily by specifying different values for the `train_transform` and `eval_transform` arguments. The train transforms will be applied to the images in the training subset, while the eval transforms will be applied to images in the validation, testing and prediction subsets. + +::::{tab-set} +:::{tab-item} API +:sync: label-1 + +```{literalinclude} ../../../../snippets/data/transforms/datamodule_train_eval.txt +:language: python +``` + +::: + +:::{tab-item} CLI +:sync: label-2 + +`train_transform` and `eval_transform` can also be set separately from CLI. Note that the CLI also supports stacking multiple transforms using a `Compose` object. + +```{literalinclude} ../../../../snippets/data/transforms/datamodule_train_eval_cli.yaml +:language: yaml +``` + +:::: + +```{note} +Please note that it is not recommended to pass only one of `train_transform` and `eval_transform` while keeping the other parameter empty. This could lead to unexpected behaviour, as it might lead to a mismatch between the training and testing subsets in terms of image shape and normalization characteristics. +``` + +## Model-specific transforms + +Each Anomalib model defines a default set of transforms, that will be applied to the input data when the user does not specify any custom transforms. The default transforms of a model can be inspected using the `configure_transforms` method, for example: + +```{literalinclude} ../../../../snippets/data/transforms/model_configure.txt +:language: python +``` + +As shown in the example, the default transforms for PatchCore consist of resizing the image to 256x256 pixels, followed by center cropping to an image size of 224x224. Finally, the pixel values are normalized to the mean and standard deviation of the ImageNet dataset. These transforms correspond to the recommended pre-processing steps described in the original PatchCore paper. + +The use of these model-specific transforms ensures that Anomalib automatically applies the right transforms when no custom transforms are passed to the datamodule by the user. When no user-defined transforms are passed to the datamodule, Anomalib's engine assigns the model's default transform to the `train_transform` and `eval_transform` of the datamodule at the start of the fit/val/test sequence: + +::::{tab-set} +:::{tab-item} API +:sync: label-1 + +```{literalinclude} ../../../../snippets/data/transforms/model_fit.txt +:language: python +``` + +::: + +:::{tab-item} CLI +:sync: label-2 + +Since the CLI uses the Anomalib engine under the hood, the same principles concerning model-specific transforms apply when running a model from the CI. Hence, the following command will ensure that Patchcore's model-specific default transform is used when fitting the model. + +```{literalinclude} ../../../../snippets/data/transforms/model_fit_cli.sh +:language: bash +``` + +:::: + +## Transforms during inference + +To ensure consistent transforms between training and inference, Anomalib includes the eval transform in the exported model. During inference, the transforms are infused in the model's forward pass which ensures that the transforms are always applied. The following example illustrates how Anomalib's torch inferencer automatically applies the transforms stored in the model. The same principles apply to both Lightning inference and OpenVINO inference. + +::::{tab-set} +:::{tab-item} API +:sync: label-1 + +```{literalinclude} ../../../../snippets/data/transforms/inference.txt +:language: python +``` + +::: + +:::{tab-item} CLI +:sync: label-2 + +The CLI behaviour is equivalent to that of the API. When a model is trained with a custom `eval_transform` like in the example below, the `eval_transform` is included both in the saved lightning model as in the exported torch model. + +```{literalinclude} ../../../../snippets/data/transforms/inference_cli.yaml +:language: yaml +``` + +```{literalinclude} ../../../../snippets/data/transforms/inference_cli.sh +:language: bash +``` + +:::: + +::: +:::: +::::: diff --git a/docs/source/snippets/data/transforms/datamodule_custom.txt b/docs/source/snippets/data/transforms/datamodule_custom.txt new file mode 100644 index 0000000000..c8cbcbf635 --- /dev/null +++ b/docs/source/snippets/data/transforms/datamodule_custom.txt @@ -0,0 +1,17 @@ +from torchvision.transforms.v2 import Resize + +transform = Resize((256, 256)) +datamodule = MVTec(transform=transform) + +datamodule.prepare_data() +datamodule.setup() + +datamodule.train_transform +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=warn) +datamodule.eval_transform +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=warn) + +next(iter(datamodule.train_data))["image"].shape +# torch.Size([3, 256, 256]) +next(iter(datamodule.test_data))["image"].shape +# torch.Size([3, 256, 256]) diff --git a/docs/source/snippets/data/transforms/datamodule_custom_cli.yaml b/docs/source/snippets/data/transforms/datamodule_custom_cli.yaml new file mode 100644 index 0000000000..9ce9dca453 --- /dev/null +++ b/docs/source/snippets/data/transforms/datamodule_custom_cli.yaml @@ -0,0 +1,18 @@ +class_path: anomalib.data.MVTec +init_args: + root: ./datasets/MVTec + category: bottle + image_size: [256, 256] + train_batch_size: 32 + eval_batch_size: 32 + num_workers: 8 + task: segmentation + test_split_mode: from_dir + test_split_ratio: 0.2 + val_split_mode: same_as_test + val_split_ratio: 0.5 + seed: null + transform: + - class_path: torchvision.transforms.v2.Resize + init_args: + size: [256, 256] diff --git a/docs/source/snippets/data/transforms/datamodule_default.txt b/docs/source/snippets/data/transforms/datamodule_default.txt new file mode 100644 index 0000000000..f8b9494030 --- /dev/null +++ b/docs/source/snippets/data/transforms/datamodule_default.txt @@ -0,0 +1,10 @@ +from anomalib.data import MVTec + +datamodule = MVTec() +datamodule.prepare_data() +datamodule.setup() + +next(iter(datamodule.train_data))["image"].shape +# torch.Size([3, 900, 900]) +next(iter(datamodule.test_data))["image"].shape +# torch.Size([3, 900, 900]) diff --git a/docs/source/snippets/data/transforms/datamodule_train_eval.txt b/docs/source/snippets/data/transforms/datamodule_train_eval.txt new file mode 100644 index 0000000000..625a19b724 --- /dev/null +++ b/docs/source/snippets/data/transforms/datamodule_train_eval.txt @@ -0,0 +1,33 @@ +from torchvision.transforms.v2 import Compose, RandomAdjustSharpness, RandomHorizontalFlip, Resize + +train_transform = Compose( + [ + RandomAdjustSharpness(sharpness_factor=0.7, p=0.5), + RandomHorizontalFlip(p=0.5), + Resize((256, 256), antialias=True), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ], +) +eval_transform = Compose( + [ + Resize((256, 256), antialias=True), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ], +) + +datamodule = MVTec(train_transform=train_transform, eval_transform=eval_transform) +datamodule.prepare_data() +datamodule.setup() + +datamodule.train_transform +# Compose( +# RandomAdjustSharpness(p=0.5, sharpness_factor=0.7) +# RandomHorizontalFlip(p=0.5) +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) +datamodule.eval_transform +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) diff --git a/docs/source/snippets/data/transforms/datamodule_train_eval_cli.yaml b/docs/source/snippets/data/transforms/datamodule_train_eval_cli.yaml new file mode 100644 index 0000000000..19005b97bc --- /dev/null +++ b/docs/source/snippets/data/transforms/datamodule_train_eval_cli.yaml @@ -0,0 +1,42 @@ +class_path: anomalib.data.MVTec +init_args: + root: ./datasets/MVTec + category: bottle + train_batch_size: 32 + eval_batch_size: 32 + num_workers: 8 + task: segmentation + test_split_mode: from_dir + test_split_ratio: 0.2 + val_split_mode: same_as_test + val_split_ratio: 0.5 + seed: null + train_transform: + class_path: torchvision.transforms.v2.Compose + init_args: + transforms: + - class_path: torchvision.transforms.v2.RandomAdjustSharpness + init_args: + sharpness_factor: 0.7 + p: 0.5 + - class_path: torchvision.transforms.v2.RandomHorizontalFlip + init_args: + p: 0.5 + - class_path: torchvision.transforms.v2.Resize + init_args: + size: [256, 256] + - class_path: torchvision.transforms.v2.Normalize + init_args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + eval_transform: + class_path: torchvision.transforms.v2.Compose + init_args: + transforms: + - class_path: torchvision.transforms.v2.Resize + init_args: + size: [256, 256] + - class_path: torchvision.transforms.v2.Normalize + init_args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] diff --git a/docs/source/snippets/data/transforms/inference.txt b/docs/source/snippets/data/transforms/inference.txt new file mode 100644 index 0000000000..914201fafc --- /dev/null +++ b/docs/source/snippets/data/transforms/inference.txt @@ -0,0 +1,48 @@ +import torch + +from anomalib.deploy import ExportType, TorchInferencer + +engine = Engine() +model = Patchcore() + +train_transform = Compose( + [ + RandomHorizontalFlip(p=0.5), + Resize((256, 256)), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ], +) + +eval_transform = Compose( + [ + Resize((256, 256)), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ], +) + +datamodule = MVTec(train_transform=train_transform, eval_transform=eval_transform) + +engine.fit(model, datamodule=datamodule) + +# after running fit, the used eval_transform will be stored in the model +model.transform +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) + +# when exporting the trained model, the transforms are added in the export +engine.export(model, export_type=ExportType.TORCH, export_root="./export_folder") + +inferencer = TorchInferencer("export_folder/weights/torch/model.pt") +inferencer.model.transform +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) + +# since the transforms are included in the forward pass, they are applied automatically +image = torch.rand((3, 900, 900)) +prediction = inferencer.predict(image) +prediction.pred_label +# diff --git a/docs/source/snippets/data/transforms/inference_cli.sh b/docs/source/snippets/data/transforms/inference_cli.sh new file mode 100644 index 0000000000..b01c559f02 --- /dev/null +++ b/docs/source/snippets/data/transforms/inference_cli.sh @@ -0,0 +1,2 @@ +anomalib fit --model Patchcore --data mvtec.yaml --default_root_dir export_path +anomalib export --model Patchcore --export_type TORCH --ckpt_path export_path/Patchcore/MVTec/bottle/latest/weights/lightning/model.ckpt diff --git a/docs/source/snippets/data/transforms/inference_cli.yaml b/docs/source/snippets/data/transforms/inference_cli.yaml new file mode 100644 index 0000000000..88e59fb258 --- /dev/null +++ b/docs/source/snippets/data/transforms/inference_cli.yaml @@ -0,0 +1,39 @@ +class_path: anomalib.data.MVTec +init_args: + root: ./datasets/MVTec + category: bottle + image_size: [256, 256] + train_batch_size: 32 + eval_batch_size: 32 + num_workers: 8 + task: segmentation + test_split_mode: from_dir + test_split_ratio: 0.2 + val_split_mode: same_as_test + val_split_ratio: 0.5 + seed: null + eval_transform: + class_path: torchvision.transforms.v2.Compose + init_args: + transforms: + - class_path: torchvision.transforms.v2.RandomHorizontalFlip + init_args: + p: 0.5 + - class_path: torchvision.transforms.v2.Resize + init_args: + size: [256, 256] + - class_path: torchvision.transforms.v2.Normalize + init_args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + train_transform: + class_path: torchvision.transforms.v2.Compose + init_args: + transforms: + - class_path: torchvision.transforms.v2.Resize + init_args: + size: [256, 256] + - class_path: torchvision.transforms.v2.Normalize + init_args: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] diff --git a/docs/source/snippets/data/transforms/model_configure.txt b/docs/source/snippets/data/transforms/model_configure.txt new file mode 100644 index 0000000000..7708705572 --- /dev/null +++ b/docs/source/snippets/data/transforms/model_configure.txt @@ -0,0 +1,9 @@ +from anomalib.models import Patchcore + +model = Patchcore() +model.configure_transforms() +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# CenterCrop(size=(224, 224)) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) diff --git a/docs/source/snippets/data/transforms/model_fit.txt b/docs/source/snippets/data/transforms/model_fit.txt new file mode 100644 index 0000000000..436a43618a --- /dev/null +++ b/docs/source/snippets/data/transforms/model_fit.txt @@ -0,0 +1,24 @@ +from anomalib.engine import Engine + +# instantiate the datamodule without passing custom transforms +datamodule = MVTec() +# initially, the datamodule will not have any transforms defined +datamodule.train_transform is None +# True + +engine = Engine() +engine.fit(model, datamodule=datamodule) + +# after running fit, the engine will have injected the model's default transform into the datamodule +datamodule.train_transform +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# CenterCrop(size=(224, 224)) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) +datamodule.eval_transform +# Compose( +# Resize(size=[256, 256], interpolation=InterpolationMode.BILINEAR, antialias=True) +# CenterCrop(size=(224, 224)) +# Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], inplace=False) +# ) diff --git a/docs/source/snippets/data/transforms/model_fit_cli.sh b/docs/source/snippets/data/transforms/model_fit_cli.sh new file mode 100644 index 0000000000..de86235129 --- /dev/null +++ b/docs/source/snippets/data/transforms/model_fit_cli.sh @@ -0,0 +1 @@ +anomalib fit --model Patchcore --data MVTec diff --git a/src/anomalib/data/base/datamodule.py b/src/anomalib/data/base/datamodule.py index 3a6a603e0e..a3ab4a5c72 100644 --- a/src/anomalib/data/base/datamodule.py +++ b/src/anomalib/data/base/datamodule.py @@ -102,6 +102,11 @@ def __init__( self.seed = seed # set transforms + if bool(train_transform) != bool(eval_transform): + msg = "Only one of train_transform and eval_transform was specified. This is not recommended because \ + it could lead to unexpected behaviour. Please ensure training and eval transforms have the same \ + reshape and normalization characteristics." + logger.warning(msg) self._train_transform = train_transform or transform self._eval_transform = eval_transform or transform @@ -255,8 +260,6 @@ def transform(self) -> Transform: """ if self._eval_transform: return self._eval_transform - if self._train_transform: - return self._train_transform return None @property diff --git a/tests/unit/engine/test_setup_transform.py b/tests/unit/engine/test_setup_transform.py index 0a6b4a4f9e..47255aba98 100644 --- a/tests/unit/engine/test_setup_transform.py +++ b/tests/unit/engine/test_setup_transform.py @@ -169,7 +169,8 @@ def test_custom_train_transform(self) -> None: # after calling setup, train_transform should be the custom transform and eval_transform should be the default assert datamodule.train_transform == transform assert datamodule.eval_transform is None - assert model.transform == transform + assert model.transform != transform + assert model.transform is not None # test if the user-specified transform is used when passed to the datamodule def test_custom_eval_transform(self) -> None: