-
Notifications
You must be signed in to change notification settings - Fork 831
β¨ feat(post-processing): Add MEBin post processor from AnomalyNCD #3030
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 14 commits
e028449
ce4097b
1e68949
3603157
0753520
575632f
f873e67
35e3d0d
fa69cf6
2f144d9
ab8a5af
1b6e845
46c0ae8
b757b27
1f29065
188275b
bf9ecfa
473c6db
b1ea898
d91e685
637eb7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,274 @@ | ||||||||||||
| # Copyright (C) 2025 Intel Corporation | ||||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||||
| """MEBin (Main Element Binarization) adaptive thresholding for anomaly detection. | ||||||||||||
|
|
||||||||||||
| This module provides the ``MEBin`` class which implements the Main Element | ||||||||||||
| Binarization algorithm designed to address the non-prominence of anomalies | ||||||||||||
| in anomaly maps. MEBin obtains anomaly-centered images by analyzing the | ||||||||||||
| stability of connected components across multiple threshold levels. | ||||||||||||
|
|
||||||||||||
| The algorithm is particularly effective for: | ||||||||||||
| - Industrial anomaly detection scenarios | ||||||||||||
| - Multi-class anomaly classification tasks | ||||||||||||
| - Cases where anomalies are non-prominent in anomaly maps | ||||||||||||
| - Avoiding the impact of incorrect detections | ||||||||||||
|
|
||||||||||||
| The threshold is computed by: | ||||||||||||
| 1. Adaptively determining threshold search range from anomaly map statistics | ||||||||||||
| 2. Sampling anomaly maps at configurable rates across threshold range | ||||||||||||
| 3. Counting connected components at each threshold level | ||||||||||||
| 4. Finding stable intervals where component count remains constant | ||||||||||||
| 5. Selecting threshold from the longest stable interval | ||||||||||||
|
|
||||||||||||
| MEBin was introduced in "AnomalyNCD: Towards Novel Anomaly Class Discovery | ||||||||||||
| in Industrial Scenarios" (https://arxiv.org/abs/2410.14379). | ||||||||||||
|
|
||||||||||||
| Example: | ||||||||||||
| >>> from anomalib.metrics.threshold import MEBin | ||||||||||||
| >>> import numpy as np | ||||||||||||
| >>> # Create sample anomaly maps | ||||||||||||
| >>> anomaly_maps = [np.random.rand(256, 256) * 255 for _ in range(10)] | ||||||||||||
| >>> # Initialize and compute thresholds | ||||||||||||
| >>> mebin = MEBin(anomaly_maps, sample_rate=4) | ||||||||||||
| >>> binarized_maps, thresholds = mebin.binarize_anomaly_maps() | ||||||||||||
| >>> print(f"Computed {len(thresholds)} thresholds") | ||||||||||||
|
|
||||||||||||
| Note: | ||||||||||||
| MEBin is designed for industrial scenarios where anomalies may be | ||||||||||||
| non-prominent. The min_interval_len parameter should be tuned based | ||||||||||||
| on the expected stability of connected component counts. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| from __future__ import annotations | ||||||||||||
|
|
||||||||||||
| import cv2 | ||||||||||||
| import numpy as np | ||||||||||||
| from tqdm import tqdm | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class MEBin: | ||||||||||||
| """MEBin (Main Element Binarization) adaptive thresholding algorithm. | ||||||||||||
|
|
||||||||||||
| This class implements the Main Element Binarization algorithm designed | ||||||||||||
| to address non-prominent anomalies in industrial anomaly detection scenarios. | ||||||||||||
| MEBin determines optimal thresholds by analyzing the stability of connected | ||||||||||||
| component counts across different threshold levels to obtain anomaly-centered | ||||||||||||
| binary representations. | ||||||||||||
|
|
||||||||||||
| The algorithm works by: | ||||||||||||
| - Adaptively determining threshold search ranges from anomaly statistics | ||||||||||||
| - Sampling anomaly maps at configurable rates across threshold range | ||||||||||||
| - Counting connected components at each threshold level | ||||||||||||
| - Identifying stable intervals where component count remains constant | ||||||||||||
| - Selecting the optimal threshold from the longest stable interval | ||||||||||||
| - Optionally applying morphological erosion to reduce noise | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| anomaly_map_list (list[np.ndarray]): List of anomaly map arrays as numpy arrays. | ||||||||||||
| sample_rate (int, optional): Sampling rate for threshold search. Higher | ||||||||||||
| values reduce processing time but may affect accuracy. | ||||||||||||
| Defaults to 4. | ||||||||||||
| min_interval_len (int, optional): Minimum length of stable intervals. | ||||||||||||
| Should be tuned based on the expected stability of anomaly score | ||||||||||||
| distributions. | ||||||||||||
| Defaults to 4. | ||||||||||||
| erode (bool, optional): Whether to apply morphological erosion to | ||||||||||||
| binarized results to reduce noise. | ||||||||||||
| Defaults to True. | ||||||||||||
|
|
||||||||||||
| Example: | ||||||||||||
| >>> from anomalib.metrics.threshold import MEBin | ||||||||||||
| >>> import numpy as np | ||||||||||||
| >>> # Create sample anomaly maps | ||||||||||||
| >>> anomaly_maps = [np.random.rand(256, 256) * 255 for _ in range(10)] | ||||||||||||
| >>> # Initialize MEBin | ||||||||||||
| >>> mebin = MEBin(anomaly_maps, sample_rate=4) | ||||||||||||
| >>> # Compute binary masks and thresholds | ||||||||||||
| >>> binarized_maps, thresholds = mebin.binarize_anomaly_maps() | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| def __init__( | ||||||||||||
| self, | ||||||||||||
| anomaly_map_list: list[np.ndarray], | ||||||||||||
| sample_rate: int = 4, | ||||||||||||
| min_interval_len: int = 4, | ||||||||||||
| erode: bool = True, | ||||||||||||
| ) -> None: | ||||||||||||
| self.anomaly_map_list = anomaly_map_list | ||||||||||||
|
|
||||||||||||
| self.sample_rate = sample_rate | ||||||||||||
| self.min_interval_len = min_interval_len | ||||||||||||
| self.erode = erode | ||||||||||||
|
|
||||||||||||
| # Adaptively determine the threshold search range | ||||||||||||
| self.max_th, self.min_th = self.get_search_range() | ||||||||||||
|
|
||||||||||||
| def get_search_range(self) -> tuple[float, float]: | ||||||||||||
| """Determine the threshold search range adaptively. | ||||||||||||
|
|
||||||||||||
| This method analyzes all anomaly maps to determine the minimum and maximum | ||||||||||||
| threshold values for the binarization process. The search range is based | ||||||||||||
| on the actual anomaly score distributions in the input maps. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| max_th (int): Maximum threshold for binarization. | ||||||||||||
| min_th (int): Minimum threshold for binarization. | ||||||||||||
| """ | ||||||||||||
| # Get the anomaly scores of all anomaly maps | ||||||||||||
| anomaly_score_list = [np.max(x) for x in self.anomaly_map_list] | ||||||||||||
|
|
||||||||||||
| # Select the maximum and minimum anomaly scores from images | ||||||||||||
| max_score, min_score = max(anomaly_score_list), min(anomaly_score_list) | ||||||||||||
| max_th, min_th = max_score, min_score | ||||||||||||
|
|
||||||||||||
| print(f"Value range: {min_score} - {max_score}") | ||||||||||||
rajeshgangireddy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
rajeshgangireddy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
| return max_th, min_th | ||||||||||||
|
|
||||||||||||
| def get_threshold( | ||||||||||||
| self, | ||||||||||||
| anomaly_num_sequence: list[int], | ||||||||||||
| min_interval_len: int, | ||||||||||||
| ) -> tuple[int, int]: | ||||||||||||
rajeshgangireddy marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| """Find the 'stable interval' in the anomaly region number sequence. | ||||||||||||
|
|
||||||||||||
| Stable Interval: A continuous threshold range in which the number of connected components remains constant, | ||||||||||||
| and the length of the threshold range is greater than or equal to the given length threshold | ||||||||||||
| (min_interval_len). | ||||||||||||
|
Comment on lines
+154
to
+156
|
||||||||||||
| Stable Interval: A continuous threshold range in which the number of connected components remains constant, | |
| and the length of the threshold range is greater than or equal to the given length threshold | |
| (min_interval_len). | |
| A "stable interval" is defined as a continuous threshold range in which the number of connected components remains constant, | |
| and the length of the threshold range is greater than or equal to the given minimum interval length (`min_interval_len`). |
Copilot
AI
Oct 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type hint 'list' is not specific. Should be 'list[int]' to match the actual usage.
| anomaly_num_sequence (list): Sequence of connected component counts | |
| anomaly_num_sequence (list[int]): Sequence of connected component counts |
Uh oh!
There was an error while loading. Please reload this page.