Skip to content

feat(visualization): add bounding box overlay for anomaly regions#3532

Open
Midoriya-w wants to merge 8 commits into
open-edge-platform:mainfrom
Midoriya-w:feature/bounding-box-visualization
Open

feat(visualization): add bounding box overlay for anomaly regions#3532
Midoriya-w wants to merge 8 commits into
open-edge-platform:mainfrom
Midoriya-w:feature/bounding-box-visualization

Conversation

@Midoriya-w
Copy link
Copy Markdown

📋 Description

Added add_bounding_boxes_to_image() function to item_visualizer.py to draw bounding boxes around detected anomaly regions.

  • Fixes #(no issue - new feature proposal)

✨ Changes

  • 🚀 New feature (non-breaking change which adds functionality)

What does this PR do?

  • Draws red bounding boxes around anomalous regions detected in the heatmap
  • Filters out noise by ignoring contours smaller than 5x5 pixels
  • Labels each detected region with "DEFECT" text
  • Fully configurable: threshold, box color, and line thickness are all parameters

Motivation

Anomaly heatmaps show pixel-level intensity but bounding boxes make defect locations immediately interpretable — especially useful for real-time warehouse/industrial inspection demos and production pipelines.

Testing

Tested on MVTecAD bottle dataset with PaDiM model. Bounding boxes correctly highlight defective regions identified by the anomaly heatmap.

Signed-off-by: dinesh <midoriya54378@gmail.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

This PR introduces a new visualization utility to draw bounding boxes over anomaly regions derived from an anomaly heatmap, improving interpretability of pixel-level anomaly outputs.

Changes:

  • Added add_bounding_boxes_to_image() to compute a binary anomaly mask via thresholding and draw contour-based bounding boxes.
  • Added optional parameters for threshold, box color, and line thickness.

Comment on lines +377 to +383
def add_bounding_boxes_to_image(
image: np.ndarray,
anomaly_map: np.ndarray,
threshold: float = 0.5,
box_color: tuple = (255, 0, 0),
box_thickness: int = 2,
) -> np.ndarray:
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

np is used in type annotations and in the function body, but this module does not import NumPy. This will raise NameError when the module is imported (annotations are evaluated eagerly without from __future__ import annotations). Add import numpy as np at module scope (or enable postponed evaluation of annotations) before using np.ndarray/np.uint8.

Copilot uses AI. Check for mistakes.
Comment on lines +384 to +392
"""Draw bounding boxes around detected anomaly regions.

Args:
image: Original input image as numpy array.
anomaly_map: Anomaly heatmap from model output.
threshold: Score threshold to consider a region anomalous.
box_color: BGR color tuple for bounding boxes.
box_thickness: Thickness of bounding box lines.

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The docstring says box_color is in BGR, but anomalib utilities consistently document box colors as RGB (e.g., src/anomalib/utils/post_processing.py:362-405 expects RGB even though it uses OpenCV). Please align this function’s color convention with the rest of the codebase (update docs/type hints and/or convert the image/color ordering accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +383 to +402
) -> np.ndarray:
"""Draw bounding boxes around detected anomaly regions.

Args:
image: Original input image as numpy array.
anomaly_map: Anomaly heatmap from model output.
threshold: Score threshold to consider a region anomalous.
box_color: BGR color tuple for bounding boxes.
box_thickness: Thickness of bounding box lines.

Returns:
Image with bounding boxes drawn around anomalous regions.
"""
import cv2

# Normalize anomaly map to 0-1
norm_map = (anomaly_map - anomaly_map.min()) / (anomaly_map.max() - anomaly_map.min() + 1e-8)

# Threshold to get binary mask
binary_mask = (norm_map > threshold).astype(np.uint8) * 255
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

This function min-max normalizes anomaly_map internally, which changes the meaning of threshold (it becomes relative per-image rather than the model’s score scale). Consider either (a) assuming inputs are already normalized (consistent with compute_mask(...): src/anomalib/utils/post_processing.py:308+ / _superimpose_segmentation_mask: src/anomalib/deploy/inferencers/base_inferencer.py:147+), or (b) adding a normalize flag and documenting that threshold applies after normalization.

Suggested change
) -> np.ndarray:
"""Draw bounding boxes around detected anomaly regions.
Args:
image: Original input image as numpy array.
anomaly_map: Anomaly heatmap from model output.
threshold: Score threshold to consider a region anomalous.
box_color: BGR color tuple for bounding boxes.
box_thickness: Thickness of bounding box lines.
Returns:
Image with bounding boxes drawn around anomalous regions.
"""
import cv2
# Normalize anomaly map to 0-1
norm_map = (anomaly_map - anomaly_map.min()) / (anomaly_map.max() - anomaly_map.min() + 1e-8)
# Threshold to get binary mask
binary_mask = (norm_map > threshold).astype(np.uint8) * 255
normalize: bool = True,
) -> np.ndarray:
"""Draw bounding boxes around detected anomaly regions.
Args:
image: Original input image as numpy array.
anomaly_map: Anomaly heatmap from model output. If ``normalize`` is ``False``,
values are assumed to already be on the intended thresholding scale.
threshold: Score threshold to consider a region anomalous. Applied after min-max
normalization when ``normalize=True``; otherwise applied directly to
``anomaly_map`` values.
box_color: BGR color tuple for bounding boxes.
box_thickness: Thickness of bounding box lines.
normalize: Whether to min-max normalize ``anomaly_map`` before thresholding.
Defaults to ``True`` to preserve the existing visualization behavior.
Returns:
Image with bounding boxes drawn around anomalous regions.
"""
import cv2
score_map = anomaly_map
if normalize:
# Normalize anomaly map to 0-1 before applying the threshold.
score_map = (anomaly_map - anomaly_map.min()) / (anomaly_map.max() - anomaly_map.min() + 1e-8)
# Threshold to get binary mask
binary_mask = (score_map > threshold).astype(np.uint8) * 255

Copilot uses AI. Check for mistakes.
Comment on lines +409 to +414
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if w > 5 and h > 5: # filter tiny noise boxes
cv2.rectangle(result, (x, y), (x + w, y + h), box_color, box_thickness)
cv2.putText(result, "DEFECT", (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, box_color, box_thickness)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

There are hard-coded values (w > 5 and h > 5 and the "DEFECT" label) that affect output but aren’t configurable. To avoid magic numbers/strings and make the utility reusable, consider parameters like min_box_size and label (or omit the text overlay by default).

Copilot uses AI. Check for mistakes.
base_image = add_text_to_image(base_image, title, **text_config)
output_images.append(base_image)

return create_image_grid(output_images, nrow=len(output_images)) if output_images else None
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

PEP8/ruff formatting: add the required blank line(s) between the end of visualize_image_item and this new top-level function definition. Also consider reformatting the multi-line cv2.putText(...) call to match the project’s formatter output.

Suggested change
return create_image_grid(output_images, nrow=len(output_images)) if output_images else None
return create_image_grid(output_images, nrow=len(output_images)) if output_images else None

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@ashwinvaidya17 ashwinvaidya17 left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution. I have added a few comments.

output_images.append(base_image)

return create_image_grid(output_images, nrow=len(output_images)) if output_images else None
def add_bounding_boxes_to_image(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's move this to functional.py


return create_image_grid(output_images, nrow=len(output_images)) if output_images else None
def add_bounding_boxes_to_image(
image: np.ndarray,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be consistent with other utils such as add_text_to_image can we use Image.Image instead

Suggested change
image: np.ndarray,
image: Image.Image,

Returns:
Image with bounding boxes drawn around anomalous regions.
"""
import cv2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try to avoid inline imports if possible.

cv2.putText(result, "DEFECT", (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, box_color, box_thickness)

return result No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you run pre-commit hooks

Signed-off-by: dinesh <midoriya54378@gmail.com>
…d to functional

Signed-off-by: dinesh <midoriya54378@gmail.com>
@Midoriya-w
Copy link
Copy Markdown
Author

Midoriya-w commented Apr 13, 2026

Thank you for the review @ashwinvaidya17! I've addressed all the comments:

Moved add bounding boxes to image to functional.py
Updated image type hint to Image.Image
Fixed box color docstring to say RGB instead of BGR
Moved cv2 and numpy imports to top of file
Added min box size and label parameters
Added newline at end of file

Please take another look when you get a chance!

Comment thread src/anomalib/visualization/image/functional.py
@Midoriya-w
Copy link
Copy Markdown
Author

Hi @ashwinvaidya17,

Thanks again for your detailed review earlier I’ve addressed all the points and updated the branch.

I’ve also synced with the latest main branch.
Would really appreciate it if you could take another look when you get a chance.

Happy to make further improvements if needed!

@Midoriya-w
Copy link
Copy Markdown
Author

Midoriya-w commented May 18, 2026

Hey @ashwinvaidya17, just wanted to share that I posted about my Anomalib contribution experience on LinkedIn including the learnings from your code review feedback. Thanks again for taking the time to review!

🔗 https://www.linkedin.com/posts/dinesh-ch-480987307_github-open-edge-platformanomalib-an-activity-7462191416337661952-wabB?utm_source=share&utm_medium=member_desktop&rcm=ACoAAE5G1eABdWqL6YEY33vbAdGViF6AAhk1KwM

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.

3 participants