Skip to content

feat(deploy): configurable threshold with metadata.json#3596

Draft
ashwinvaidya17 wants to merge 3 commits into
open-edge-platform:mainfrom
ashwinvaidya17:feat/export/configurable_threshold
Draft

feat(deploy): configurable threshold with metadata.json#3596
ashwinvaidya17 wants to merge 3 commits into
open-edge-platform:mainfrom
ashwinvaidya17:feat/export/configurable_threshold

Conversation

@ashwinvaidya17
Copy link
Copy Markdown
Contributor

📝 Description

✨ Changes

Select what type of change your PR is:

  • 🚀 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)
  • ⚡ Performance improvements
  • 🎨 Style changes (code style/formatting)
  • 🧪 Tests (adding/modifying tests)
  • 📚 Documentation update
  • 📦 Build system changes
  • 🚧 CI/CD configuration
  • 🔧 Chore (general maintenance)
  • 🔒 Security update
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)

✅ Checklist

Before you submit your pull request, please make sure you have completed the following steps:

  • 📚 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.

For more information about code review checklists, see the Code Review Checklist.

Signed-off-by: Ashwin Vaidya <ashwin.vaidya@intel.com>
Copilot AI review requested due to automatic review settings May 13, 2026 11:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces deploy-time configurable thresholding by adding image/pixel sensitivity overrides that can be provided at inference time, and by exporting a metadata.json sidecar containing default post-processing parameters (incl. sensitivities). This aligns with the deploy/export pipeline by making exported artifacts more self-describing and enabling inferencers to pick up defaults automatically.

Changes:

  • Extend AnomalibModule / PostProcessor forward paths to accept optional image_sensitivity and pixel_sensitivity overrides.
  • Write a metadata.json sidecar during Torch/ONNX/OpenVINO export and add a small metadata load/dump + migration utility.
  • Update Torch/OpenVINO inferencers to read metadata.json and allow constructor/per-call sensitivity overrides; add unit tests for metadata and the new post-processor threshold logic.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/unit/post_processing/test_post_processor.py Adds unit coverage for effective-threshold computation and sensitivity overrides in PostProcessor.forward.
tests/unit/deploy/test_metadata.py Adds unit coverage for metadata load/dump, validation errors, and migration behavior.
src/anomalib/post_processing/post_processor.py Adds sensitivity override inputs, effective-threshold computation, and alters thresholding behavior in forward.
src/anomalib/post_processing/mebin_post_processor.py Adjusts forward signature to accept sensitivity args for interface compatibility.
src/anomalib/models/components/base/export_mixin.py Adds metadata.json writing during export and extends ONNX export to include sensitivity inputs.
src/anomalib/models/components/base/anomalib_module.py Extends model forward to accept sensitivity overrides and forwards them to the post-processor.
src/anomalib/deploy/metadata/migration.py Introduces schema versioning and a legacy-to-v1 migration path.
src/anomalib/deploy/metadata/_core.py Implements metadata load/dump/validation helpers and a SchemaValidationError.
src/anomalib/deploy/metadata/init.py Exposes the metadata public API (load_metadata, dump_metadata, etc.).
src/anomalib/deploy/inferencers/torch_inferencer.py Adds metadata-driven default sensitivities and per-call overrides for Torch inference.
src/anomalib/deploy/inferencers/openvino_inferencer.py Adds metadata-driven default sensitivities and per-call overrides for OpenVINO inference.

Comment thread src/anomalib/post_processing/post_processor.py
Comment thread src/anomalib/post_processing/post_processor.py Outdated
Comment thread src/anomalib/models/components/base/export_mixin.py Outdated
Comment thread src/anomalib/deploy/metadata/migration.py Outdated
Comment thread src/anomalib/models/components/base/anomalib_module.py
Comment thread src/anomalib/deploy/inferencers/torch_inferencer.py
Comment thread src/anomalib/deploy/inferencers/torch_inferencer.py Outdated
Comment thread src/anomalib/deploy/inferencers/openvino_inferencer.py Outdated
Signed-off-by: Ashwin Vaidya <ashwin.vaidya@intel.com>
Signed-off-by: Ashwin Vaidya <ashwin.vaidya@intel.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (2)

src/anomalib/post_processing/post_processor.py:264

  • When normalization stats are unavailable (image_min/image_max or pixel_min/pixel_max are NaN), _normalize() returns the raw predictions unchanged. In that case, forward() still thresholds using a fixed 0.5 (the normalized-space boundary), which will silently apply the wrong decision boundary to unnormalized scores/maps. Consider gating the 0.5 thresholding on normalization actually being applied (stats present), otherwise fall back to raw thresholds (or skip thresholding).
        if self.enable_thresholding:
            if self.enable_normalization:
                threshold = torch.tensor(0.5, device=self.image_threshold.device, dtype=self.image_threshold.dtype)
                pred_label = self._apply_threshold(pred_score, threshold)
                pred_mask = self._apply_threshold(anomaly_map, threshold)
            else:
                pred_label = self._apply_threshold(pred_score, self.image_threshold)
                pred_mask = self._apply_threshold(anomaly_map, self.pixel_threshold)

src/anomalib/post_processing/post_processor.py:358

  • _apply_threshold no longer guards against threshold being NaN. Since thresholds are initialized as NaN buffers and may remain NaN if calibration/threshold fitting was not run, preds > threshold will produce all-False labels/masks, which is a silent failure mode. Consider explicitly handling NaN thresholds (e.g., return None to indicate "no threshold available" or raise a clear error).
        Returns:
            torch.Tensor | None: Thresholded predictions or None if input is None.
        """
        if preds is None:
            return preds
        return preds > threshold

Comment on lines 238 to +252
if self.enable_normalization:
pred_score = self._normalize(pred_score, self.image_min, self.image_max, self.image_threshold)
anomaly_map = self._normalize(predictions.anomaly_map, self.pixel_min, self.pixel_max, self.pixel_threshold)
image_eff_threshold = self._effective_threshold(
self.image_threshold,
self.image_min,
self.image_max,
image_sens,
)
pixel_eff_threshold = self._effective_threshold(
self.pixel_threshold,
self.pixel_min,
self.pixel_max,
pixel_sens,
)
pred_score = self._normalize(pred_score, self.image_min, self.image_max, image_eff_threshold)
anomaly_map = self._normalize(predictions.anomaly_map, self.pixel_min, self.pixel_max, pixel_eff_threshold)
Comment on lines +308 to +319
try:
predictions = self.model(
image,
torch.tensor(img_sens, device=self.device),
torch.tensor(pix_sens, device=self.device),
)
except TypeError:
logger.warning(
"Image only API is deprecated and will be removed in Anomalib 2.7.0."
" Models exported from current version of Anomalib already support the new API.",
)
predictions = self.model(image)
Comment on lines +133 to +147
self.metadata = self._load_metadata(path)
self._default_image_sensitivity = (
image_sensitivity
if image_sensitivity is not None
else self.metadata.get("postprocess", {}).get("image_sensitivity", 0.5)
if self.metadata
else 0.5
)
self._default_pixel_sensitivity = (
pixel_sensitivity
if pixel_sensitivity is not None
else self.metadata.get("postprocess", {}).get("pixel_sensitivity", 0.5)
if self.metadata
else 0.5
)
Comment on lines +174 to +179
def forward(
self,
batch: torch.Tensor,
image_sensitivity: torch.Tensor | None = None,
pixel_sensitivity: torch.Tensor | None = None,
) -> InferenceBatch:
if _parse_version(metadata["schema_version"]) > _parse_version(CURRENT_SCHEMA_VERSION):
logger.warning(
"Metadata schema %s is newer than supported (%s). "
"Unknown fields will be ignored. Consider upgrading anomalib.",
Comment on lines 79 to +91
def to_torch(
self,
export_root: Path | str,
model_file_name: str = "model",
write_metadata: bool = True,
) -> Path:
"""Export model to PyTorch format.

Args:
export_root (Path | str): Path to the output folder
model_file_name (str): Name of the exported model
write_metadata (bool): Whether to write metadata.json sidecar file.
Defaults to ``True``.
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.

2 participants