Skip to content

🐞 fix(cli): enable --model.pre_processor.image_size via CLI (fixes #3460)#3482

Open
AbhayKumarDas wants to merge 2 commits intoopen-edge-platform:mainfrom
AbhayKumarDas:fix-cli-image-size
Open

🐞 fix(cli): enable --model.pre_processor.image_size via CLI (fixes #3460)#3482
AbhayKumarDas wants to merge 2 commits intoopen-edge-platform:mainfrom
AbhayKumarDas:fix-cli-image-size

Conversation

@AbhayKumarDas
Copy link
Copy Markdown
Contributor

@AbhayKumarDas AbhayKumarDas commented Mar 29, 2026


Problem

After the pre-processor refactor, changing image size from the CLI requires rebuilding the entire preprocessing pipeline specifying PreProcessor, Compose, Resize, and Normalize just to change one number. For something as commonly adjusted as image size, this creates unnecessary friction.

Previous approach and mentor feedback

My initial attempt added image_size as a pass-through parameter to every model's __init__, forwarding it to AnomalibModule. While functional, this touched 22+ model files with identical boilerplate and duplicated a concept that already belongs to the pre-processor. Every new model would need the same pass-through — not scalable.

@ashwinvaidya17 pointed out that the CLI already exposes --model.pre_processor (defaults to True), and every model already has a configure_pre_processor method that accepts image_size. The infrastructure was already there. He suggested following the Lightning CLI configure_optimizers pattern, where a method on the module is overridden via the CLI using functools.partial as anomalib already uses this exact pattern for optimizers at cli.py:587.

Design considerations

Before settling on the current approach, I evaluated a simpler alternative: replace model.pre_processor after model instantiation by calling configure_pre_processor(image_size=...) post-init. This would have been fewer lines, but it breaks five models — CsFlow, Fastflow, Ganomaly, UFlow, and ReverseDistillation as that read self.input_size during __init__ to construct their torch model architectures. By the time we'd replace the pre-processor, the model would already be built with the wrong input dimensions.

The chosen approach patches configure_pre_processor on the model class before instantiation, so the correct image size flows through from the start. This mirrors how Lightning CLI overrides configure_optimizers — the method is temporarily replaced with a partial that has the CLI arguments baked in, then restored after instantiation.

How it works

The solution has two stages, both in cli.py.

The first stage handles a parsing constraint. pre_processor is typed as nn.Module | bool in AnomalibModule.__init__, so jsonargparse rejects --model.pre_processor.image_size at parse time. To solve this, _extract_pre_processor_args intercepts the raw CLI arguments before parse_args, extracts any --model.pre_processor.* entries, and removes them from the arg list. The pre_processor parameter then falls back to its default value True, meaning "use configure_pre_processor() to create the default."

The second stage injects the extracted arguments into the model's configure_pre_processor method. Befor parser.instantiate_classes() creates the model, _patch_configure_pre_processor resolves the model class from config, saves the original method descriptor, and replaces it with staticmethod(partial(original, **extracted_kwargs)). When the model's __init__ calls _resolve_component(True, ..., self.configure_pre_processor), the partial fires with image_size already bound. After instantiation, the original method is restored in a try/finally block — no permanent side effects on the class.

This approach correctly handles both @staticmethod and @classmethod overrides across different models. For models that inherit configure_pre_processor from the base class without their own override, delattr removes the temporary patch and restores normal MRO resolution.

What doesn't break

When no --model.pre_processor.* args are passed, _pre_processor_kwargs stays empty, no patching occurs, and the original code path runs untouched. Model-specific overrides are respected — PatchCore keeps its center_crop_size default, WinClip and UFlow still warn and ignore image_size, DRAEM and EfficientAd still skip normalization. Custom PreProcessor instances passed via --model.pre_processor <class_path> are unaffected since there are no sub-args to extract.

This design is also forward-compatible with exposing additional pre-processor parameters in the future. Any keyword argument accepted by a model's configure_pre_processor can be passed via --model.pre_processor.<key> without further code changes — for example, PatchCore's center_crop_size already works today.

Example usage

# Square resize
anomalib train --model Padim --model.pre_processor.image_size 512 --data MVTecAD --data.category transistor --trainer.fast_dev_run True

# Rectangular resize
anomalib train --model Padim --model.pre_processor.image_size "[512, 1024]" --data MVTecAD --data.category transistor --trainer.fast_dev_run True

Files changed

src/anomalib/cli/cli.py — 1 file, +114 lines, 0 model files touched

Previous Approach Overview (click to expand)

Previous Approach Overview

This PR fixes #3460 by allowing users to directly configure input image size from the CLI using --model.image_size.

Before this change, modifying image size required redefining the entire preprocessing pipeline, which made a very simple task unnecessarily complicated. This update restores a clean and intuitive way to control input resolution from the CLI.


Problem

After the pre-processor refactor, image resizing became tied to the PreProcessor configuration. Because of this, users could not change the image size directly from the CLI.

In practice, this meant users had to either reconstruct the full PreProcessor using CLI arguments or write a YAML configuration just to change a single parameter. For something as commonly adjusted as image size, this created unnecessary friction.


Root Cause

The CLI is built on top of jsonargparse, which determines available arguments based on the __init__ signatures of model classes.

Although image_size logically belongs to the base class (AnomalibModule), it was not exposed in the constructors of model subclasses like Padim or Stfpm. As a result, the CLI could not recognize --model.image_size, even though the base module had the capability to handle it.


Solution

The solution follows the existing architecture instead of introducing new CLI-level logic.

First, image_size is added to the constructor of every model and forwarded to AnomalibModule. This makes the parameter visible to jsonargparse, allowing it to be used directly from the CLI. This approach is consistent with how other parameters such as pre_processor, post_processor, evaluator, and visualizer are already handled.

Second, the base module (AnomalibModule) is extended to apply the image size dynamically. When image_size is provided and the default pre-processor is being used, the preprocessing pipeline is modified at runtime. If a custom PreProcessor is provided by the user, the behavior remains unchanged.

Instead of rebuilding the preprocessing pipeline, the implementation updates it in-place. If a Resize transform already exists, it is replaced with the new size. If it does not exist, a Resize transform is added at the beginning. Existing attributes such as interpolation and antialiasing are preserved, ensuring consistent behavior.


Architecture

This approach keeps the design aligned with the existing system:

  • Parameters are exposed through model constructors, not the CLI layer
  • jsonargparse automatically handles CLI parsing based on these constructors
  • Shared behavior is implemented once in the base module
  • Model classes only act as pass-through for configuration

No changes were made to cli.py, and no special argument parsing logic was introduced. This keeps the solution clean, scalable, and consistent with the framework’s design principles.


Example Usage

anomalib train --model Padim --model.image_size 512 --data MVTecAD --data.category transistor --trainer.fast_dev_run True

anomalib train --model Padim --model.image_size "[512, 512]" --data MVTecAD --data.category transistor

anomalib train --model Stfpm --model.image_size 384 --data MVTecAD --data.category bottle --trainer.fast_dev_run True

</details> ```

---

## ✨ Changes

- 🚀 New feature (non-breaking change which adds functionality)
- 🐞 Bug fix (non-breaking change which fixes an issue)
- 🔄 Refactor (non-breaking change which refactors the code base)

## ✅ Checklist

- 📚 I have made the necessary updates to the documentation (if applicable).
- 🧪 I have written tests that support my changes and prove that my fix is effective or my feature works (if applicable).
- 🏷️ My PR title follows conventional commit format.

@ashwinvaidya17
Copy link
Copy Markdown
Contributor

@AbhayKumarDas thanks for the PR. Current solution touches a lot of files and introduces another argument in all the models (which itself is already part of pre-processor). The CLI already exposes the pre-processor via --model.preprocessor. Currently the default value is True. Can we come up with a design that allows us to set the image size something like model.preprocessor.image_size? We can then expose the input transforms in the future via the same argument. A good starting point would be to see how Lightning CLI configures the optimizers through command line. Though I am not a fan of partial functions, in this case it does follow a similar pattern i.e method named configure_optimizer in the lightning module is overridden via the CLI. In our case, the method is configure_preprocessor

@AbhayKumarDas
Copy link
Copy Markdown
Contributor Author

@AbhayKumarDas thanks for the PR. Current solution touches a lot of files and introduces another argument in all the models (which itself is already part of pre-processor). The CLI already exposes the pre-processor via --model.preprocessor. Currently the default value is True. Can we come up with a design that allows us to set the image size something like model.preprocessor.image_size? We can then expose the input transforms in the future via the same argument. A good starting point would be to see how Lightning CLI configures the optimizers through command line. Though I am not a fan of partial functions, in this case it does follow a similar pattern i.e method named configure_optimizer in the lightning module is overridden via the CLI. In our case, the method is configure_preprocessor

Thanks @ashwinvaidya17 sir for the detailed feedback, this makes sense.

I agree that exposing image_size through model constructors is not the cleanest approach, and handling it through the preprocessor aligns better with the existing design.

I will explore restructuring this using a configure_preprocessor-style pattern similar to how Lightning CLI handles optimizers, and update the PR accordingly.

Thanks for pointing me in the right direction.

Signed-off-by: Abhay Kumar Das <dasabhay.jsr@gmail.com>
@AbhayKumarDas AbhayKumarDas changed the title 🐞 fix(model): enable CLI support for image_size via model.image_size (fixes #3460) 🐞 fix(cli): enable --model.pre_processor.image_size via CLI (fixes #3460) Mar 31, 2026
@AbhayKumarDas
Copy link
Copy Markdown
Contributor Author

Good Evening @ashwinvaidya17 , I have updated the implementation based on the feedback and reworked the design to align with the existing pre-processor architecture instead of introducing model-level parameters.

The changes now use the configure_pre_processor flow with CLI-based argument injection, keeping the solution scoped to the CLI and avoiding modifications across model files. I have also added validation scenarios to ensure correctness across different models and edge cases.

Sharing the validation document here for reference: [Click to View]

Would appreciate a quick review when you get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 cli does not allow custom input size

2 participants