-
Notifications
You must be signed in to change notification settings - Fork 703
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
πAdd SuperSimpleNet model #2428
Changes from 35 commits
90625bb
4d5e480
742e0ca
fd95793
26e6f5a
eeddb15
090120d
2be15bd
f986c8e
e08366d
e45b6d4
8768607
0e5a6c7
1238512
28006f1
40e7475
ce6c7f5
a47fc50
3c65da6
07e02fb
fda2c4e
8530a28
122ec03
fe57b27
952653b
fd499ad
521aee7
6687abc
50032c6
5bcb533
a569e03
bb142dd
64fba6c
528d594
ebc16ba
d1ded74
abc71ac
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,26 @@ | ||
# SuperSimpleNet | ||
|
||
## Architecture | ||
|
||
```{eval-rst} | ||
.. image:: ../../../../../images/supersimplenet/architecture.png | ||
:alt: SuperSimpleNet Architecture | ||
``` | ||
|
||
```{eval-rst} | ||
.. automodule:: anomalib.models.image.supersimplenet.lightning_model | ||
:members: | ||
:show-inheritance: | ||
``` | ||
|
||
```{eval-rst} | ||
.. automodule:: anomalib.models.image.supersimplenet.torch_model | ||
:members: | ||
:show-inheritance: | ||
``` | ||
|
||
```{eval-rst} | ||
.. automodule:: anomalib.models.image.supersimplenet.anomaly_generator | ||
:members: | ||
:show-inheritance: | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
Copyright (c) 2024 Intel Corporation | ||
SPDX-License-Identifier: Apache-2.0 | ||
|
||
Some files in this folder are based on the original SuperSimpleNet implementation by BlaΕΎ Rolih | ||
|
||
Original license: | ||
----------------- | ||
|
||
MIT License | ||
|
||
Copyright (c) 2024 BlaΕΎ Rolih | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# SuperSimpleNet: Unifying Unsupervised and Supervised Learning for Fast and Reliable Surface Defect Detection | ||
|
||
This is an implementation of the [SuperSimpleNet](https://arxiv.org/pdf/2408.03143) paper, based on the [official code](https://github.com/blaz-r/SuperSimpleNet). | ||
|
||
Model Type: Segmentation | ||
|
||
## Description | ||
|
||
**SuperSimpleNet** is a simple yet strong discriminative defect / anomaly detection model evolved from the SimpleNet architecture. It consists of four components: | ||
feature extractor with upscaling, feature adaptor, feature-level synthetic anomaly generation module, and | ||
segmentation-detection module. | ||
|
||
A ResNet-like feature extractor first extracts features, which are then upscaled and | ||
average-pooled to capture neighboring context. Features are further refined for anomaly detection task in the adaptor module. | ||
During training, synthetic anomalies are generated at the feature level by adding Gaussian noise to regions defined by the | ||
binary Perlin noise mask. The perturbed features are then fed into the segmentation-detection | ||
module, which produces the anomaly map and the anomaly score. During inference, anomaly generation is skipped, and the model | ||
directly predicts the anomaly map and score. The predicted anomaly map is upscaled to match the input image size | ||
and refined with a Gaussian filter. | ||
|
||
This implementation supports both unsupervised and supervised setting, but Anomalib currently supports only unsupervised learning. | ||
|
||
## Architecture | ||
|
||
![SuperSimpleNet architecture](/docs/source/images/supersimplenet/architecture.png "SuperSimpleNet architecture") | ||
|
||
## Usage | ||
|
||
`anomalib train --model SuperSimpleNet --data MVTec --data.category <category>` | ||
|
||
> It is recommended to train the model for 300 epochs with batch size of 32 to achieve stable training with random anomaly generation. Training with lower parameter values will still work, but might not yield the optimal results. | ||
> | ||
> For supervised learning, refer to the [official code](https://github.com/blaz-r/SuperSimpleNet). | ||
|
||
## MVTec AD results | ||
|
||
The following results were obtained using this Anomalib implementation trained for 300 epochs with seed 0, default params, and batch size 32. | ||
| | **Image AUROC** | **Pixel AUPRO** | | ||
| ----------- | :-------------: | :-------------: | | ||
| Bottle | 1.000 | 0.903 | | ||
| Cable | 0.981 | 0.901 | | ||
| Capsule | 0.989 | 0.931 | | ||
| Carpet | 0.985 | 0.929 | | ||
| Grid | 0.994 | 0.930 | | ||
| Hazelnut | 0.994 | 0.943 | | ||
| Leather | 1.000 | 0.970 | | ||
| Metal_nut | 0.995 | 0.920 | | ||
| Pill | 0.962 | 0.936 | | ||
| Screw | 0.912 | 0.947 | | ||
| Tile | 0.994 | 0.854 | | ||
| Toothbrush | 0.908 | 0.860 | | ||
| Transistor | 1.000 | 0.907 | | ||
| Wood | 0.987 | 0.858 | | ||
| Zipper | 0.995 | 0.928 | | ||
| Average | 0.980 | 0.914 | | ||
|
||
For other results on VisA, SensumSODF, and KSDD2, refer to the [paper](https://arxiv.org/pdf/2408.03143). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"""SuperSimpleNet model.""" | ||
|
||
# Copyright (C) 2024 Intel Corporation | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from .lightning_model import Supersimplenet | ||
|
||
__all__ = ["Supersimplenet"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
"""Anomaly generator for the SuperSimplenet model implementation.""" | ||
|
||
# Copyright (C) 2024 Intel Corporation | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
import torch | ||
import torch.nn.functional as F # noqa: N812 | ||
from torch import nn | ||
|
||
from anomalib.data.utils.generators import generate_perlin_noise | ||
|
||
|
||
class SSNAnomalyGenerator(nn.Module): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about the naming here. Why not simply use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is now just |
||
"""Anomaly generator of the SuperSimpleNet model.""" | ||
|
||
def __init__( | ||
self, | ||
noise_mean: float, | ||
noise_std: float, | ||
threshold: float, | ||
) -> None: | ||
super().__init__() | ||
|
||
self.noise_mean = noise_mean | ||
self.noise_std = noise_std | ||
|
||
self.threshold = threshold | ||
|
||
@staticmethod | ||
def next_power_2(num: int) -> int: | ||
"""Get the next power of 2 for given number. | ||
Args: | ||
num (int): value of interest | ||
Returns: | ||
next power of 2 value for given number | ||
""" | ||
return 1 << (num - 1).bit_length() | ||
|
||
def generate_perlin(self, batches: int, height: int, width: int) -> torch.Tensor: | ||
"""Generate 2d perlin noise masks with dims [b, 1, h, w]. | ||
Args: | ||
batches (int): number of batches (different masks) | ||
height (int): height of features | ||
width (int): width of features | ||
Returns: | ||
tensor with b perlin binarized masks | ||
""" | ||
perlin = [] | ||
for _ in range(batches): | ||
perlin_height = self.next_power_2(height) | ||
perlin_width = self.next_power_2(width) | ||
|
||
# keep power of 2 here for reproduction purpose, although this function supports power2 internally | ||
perlin_noise = generate_perlin_noise(height=perlin_height, width=perlin_width) | ||
|
||
# original is power of 2 scale, so fit to our size | ||
perlin_noise = F.interpolate( | ||
perlin_noise.reshape(1, 1, perlin_height, perlin_width), | ||
size=(height, width), | ||
mode="bilinear", | ||
) | ||
# binarize | ||
perlin_thr = torch.where(perlin_noise > self.threshold, 1, 0) | ||
|
||
# 50% of anomaly | ||
if torch.rand(1).item() > 0.5: | ||
perlin_thr = torch.zeros_like(perlin_thr) | ||
|
||
perlin.append(perlin_thr) | ||
return torch.cat(perlin) | ||
|
||
def forward( | ||
self, | ||
features: torch.Tensor, | ||
mask: torch.Tensor, | ||
labels: torch.Tensor, | ||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | ||
"""Generate anomaly on features using thresholded perlin noise and Gaussian noise. | ||
Also update GT masks and labels with new anomaly information. | ||
Args: | ||
features: input features. | ||
mask: GT masks. | ||
labels: GT labels. | ||
Returns: | ||
perturbed features, updated GT masks and labels. | ||
""" | ||
b, _, h, w = features.shape | ||
|
||
# duplicate | ||
features = torch.cat((features, features)) | ||
mask = torch.cat((mask, mask)) | ||
labels = torch.cat((labels, labels)) | ||
|
||
noise = torch.normal( | ||
mean=self.noise_mean, | ||
std=self.noise_std, | ||
size=features.shape, | ||
device=features.device, | ||
requires_grad=False, | ||
) | ||
|
||
# mask indicating which regions will have noise applied | ||
# [B * 2, 1, H, W] initial all masked as anomalous | ||
noise_mask = torch.ones( | ||
b * 2, | ||
1, | ||
h, | ||
w, | ||
device=features.device, | ||
requires_grad=False, | ||
) | ||
|
||
# no overlap: don't apply to already anomalous regions (mask=1 -> bad) | ||
noise_mask = noise_mask * (1 - mask) | ||
|
||
# shape of noise is [B * 2, 1, H, W] | ||
perlin_mask = self.generate_perlin(b * 2, h, w).to(features.device) | ||
# only apply where perlin mask is 1 | ||
noise_mask = noise_mask * perlin_mask | ||
|
||
# update gt mask | ||
mask = mask + noise_mask | ||
# binarize | ||
mask = torch.where(mask > 0, torch.ones_like(mask), torch.zeros_like(mask)) | ||
|
||
# make new labels. 1 if any part of mask is 1, 0 otherwise | ||
new_anomalous = noise_mask.reshape(b * 2, -1).any(dim=1).type(torch.float32) | ||
labels = labels + new_anomalous | ||
# binarize | ||
labels = torch.where(labels > 0, torch.ones_like(labels), torch.zeros_like(labels)) | ||
|
||
# apply masked noise | ||
perturbed = features + noise * noise_mask | ||
|
||
return perturbed, mask, labels |
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.
What are the missing points in Anomalib to support supervised setting
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.
Right now I believe there are no standard supervised datasets. Another problem is the Folder dataset as it assumes that abnormal samples are always in test set:
anomalib/src/anomalib/data/image/folder.py
Lines 174 to 175 in bcc0b43
Another thing for full reproduction of SuperSimpleNet results is the fixed flipping augmentation and frequency sampling. This is however not necessary, but needed for best results. It's also not SuperSimpleNet specific, so might be worth considering if other supervised model will be supported.
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.
yeah, we would like to diversify the model pool, and include more learning types than one-class models. Thanks for the feedback.
@abc-125, you might want to be aware of this discussion as you have recently worked on this stuff
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.
Thank you for adding me, it would be great to have supervised models and datasets in Anomalib. Recently, I looked at how to add a supervised dataset, and it certainly would require changing some base structures, such as paths to folders (I guess we will need
abnormal_train_dir
and maybe renaming the rest to make it easier to understand,normal_train_dir
, etc.):anomalib/src/anomalib/data/image/folder.py
Lines 195 to 202 in bcc0b43