Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5b4931b
Initial Implementation of GLASS Model
code-dev05 Mar 26, 2025
4789f49
Created the trainer class for glass model
code-dev05 Apr 14, 2025
050fd4c
Added suggested changes
code-dev05 Apr 27, 2025
cdd0984
Modified forward method for model
code-dev05 Apr 27, 2025
381eec6
Fixed backbone loading logic
code-dev05 Apr 30, 2025
9b1c51a
Added type for input shape
code-dev05 May 4, 2025
161005c
Fixed bugs
code-dev05 May 4, 2025
3d78beb
Merge branch 'main' into feature/model/glass
samet-akcay May 7, 2025
617cf49
Changed files as needed
code-dev05 May 13, 2025
f9d3207
Merge remote-tracking branch 'origin/feature/model/glass' into featur…
code-dev05 May 13, 2025
7fea20f
Matched code to the original implementation
code-dev05 Jun 19, 2025
1beedf5
Added support for gpu
code-dev05 Jun 23, 2025
838bc50
Refactored code from lightning model to torch model
code-dev05 Jul 1, 2025
1baa0b7
GPU bug fixed
code-dev05 Jul 2, 2025
f066b3c
used image device in torch model
code-dev05 Jul 2, 2025
6e780b0
fixed bug
code-dev05 Jul 2, 2025
b1be6f5
Added validation step
code-dev05 Jul 11, 2025
20d97dd
Merge branch 'main' into feature/model/glass
samet-akcay Jul 14, 2025
d5affe4
Refactored code for better readability
code-dev05 Jul 28, 2025
f008537
Merge remote-tracking branch 'origin/feature/model/glass' into featur…
code-dev05 Jul 28, 2025
a1097e5
Set automatic optimization to False and made component functions
code-dev05 Jul 31, 2025
7e9d4d4
Resolved third-party-programs.txt conflict
code-dev05 Aug 5, 2025
44dcd60
Added automated download for dtd dataset in Glass Model
code-dev05 Aug 12, 2025
da57095
Removed some input args
code-dev05 Aug 14, 2025
ba5a6dd
Change in default parameters
code-dev05 Aug 14, 2025
714a3c3
Fixed default backbone name
code-dev05 Aug 14, 2025
1a3519c
Changed configure pre_processor method
code-dev05 Aug 14, 2025
9e12285
Merge remote-tracking branch 'up/main' into feature/model/glass
code-dev05 Sep 13, 2025
5466d46
Made some changes to the workflow of GLASS Model
code-dev05 Sep 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/anomalib/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
Fastflow,
Fre,
Ganomaly,
Glass,
Padim,
Patchcore,
ReverseDistillation,
Expand Down Expand Up @@ -102,6 +103,7 @@ class UnknownModelError(ModuleNotFoundError):
"Fastflow",
"Fre",
"Ganomaly",
"Glass",
"Padim",
"Patchcore",
"ReverseDistillation",
Expand Down
2 changes: 2 additions & 0 deletions src/anomalib/models/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .fastflow import Fastflow
from .fre import Fre
from .ganomaly import Ganomaly
from .glass import Glass
from .padim import Padim
from .patchcore import Patchcore
from .reverse_distillation import ReverseDistillation
Expand All @@ -76,6 +77,7 @@
"Fastflow",
"Fre",
"Ganomaly",
"Glass",
"Padim",
"Patchcore",
"ReverseDistillation",
Expand Down
23 changes: 23 additions & 0 deletions src/anomalib/models/image/glass/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""GLASS - Unsupervised anomaly detection via Gradient Ascent for Industrial Anomaly detection and localization.

This module implements the GLASS model for unsupervised anomaly detection and localization. GLASS synthesizes both
global and local anomalies using Gaussian noise guided by gradient ascent to enhance weak defect detection in
industrial settings.

The model consists of:
- A feature extractor and feature adaptor to obtain robust normal representations
- A Global Anomaly Synthesis (GAS) module that perturbs features using Gaussian noise and gradient ascent with
truncated projection
- A Local Anomaly Synthesis (LAS) module that overlays augmented textures onto images using Perlin noise masks
- A shared discriminator trained with features from normal, global, and local synthetic samples

Paper: `A Unified Anomaly Synthesis Strategy with Gradient Ascent for Industrial Anomaly Detection and Localization
<https://arxiv.org/pdf/2407.09359>`
"""

from .lightning_model import Glass

__all__ = ["Glass"]
19 changes: 19 additions & 0 deletions src/anomalib/models/image/glass/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Utility functions for GLASS Model."""

from .aggregator import Aggregator
from .discriminator import Discriminator
from .patch_maker import PatchMaker
from .preprocessing import Preprocessing
from .projection import Projection
from .rescale_segmentor import RescaleSegmentor

__all__ = ["Aggregator",
"Discriminator",
"PatchMaker",
"Preprocessing",
"Projection",
"RescaleSegmentor",
]
25 changes: 25 additions & 0 deletions src/anomalib/models/image/glass/components/aggregator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Aggregates and reshapes features to a target dimension."""

import torch
import torch.nn.functional as f


class Aggregator(torch.nn.Module):
"""Aggregates and reshapes features to a target dimension.

Input: Multi-dimensional feature tensors
Output: Reshaped and pooled features of specified target dimension
"""

def __init__(self, target_dim: int) -> None:
super().__init__()
self.target_dim = target_dim

def forward(self, features: torch.Tensor) -> torch.Tensor:
"""Returns reshaped and average pooled features."""
features = features.reshape(len(features), 1, -1)
features = f.adaptive_avg_pool1d(features, self.target_dim)
return features.reshape(len(features), -1)
52 changes: 52 additions & 0 deletions src/anomalib/models/image/glass/components/discriminator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Discriminator network for anomaly detection."""

import torch

from .init_weight import init_weight


class Discriminator(torch.nn.Module):
"""Discriminator network for anomaly detection.

Args:
in_planes: Input feature dimension
n_layers: Number of layers
hidden: Hidden layer dimensions
"""

def __init__(self, in_planes: int, n_layers: int = 2, hidden: int | None = None) -> None:
super().__init__()

hidden_ = in_planes if hidden is None else hidden
self.body = torch.nn.Sequential()
for i in range(n_layers - 1):
in_ = in_planes if i == 0 else hidden_
hidden_ = int(hidden_ // 1.5) if hidden is None else hidden
self.body.add_module(
f"block{i + 1}",
torch.nn.Sequential(
torch.nn.Linear(in_, hidden_),
torch.nn.BatchNorm1d(hidden_),
torch.nn.LeakyReLU(0.2),
),
)
self.tail = torch.nn.Sequential(
torch.nn.Linear(hidden_, 1, bias=False),
torch.nn.Sigmoid(),
)
self.apply(init_weight)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Performs a forward pass through the discriminator network.

Args:
x (torch.Tensor): Input tensor of shape (B, in_planes), where B is the batch size.

Returns:
torch.Tensor: Output tensor of shape (B, 1) containing probability scores.
"""
x = self.body(x)
return self.tail(x)
22 changes: 22 additions & 0 deletions src/anomalib/models/image/glass/components/init_weight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Initializes network weights using Xavier normal initialization."""

import torch
from torch import nn


def init_weight(m: nn.Module) -> None:
"""Initializes network weights using Xavier normal initialization.

Applies Xavier initialization for linear layers and normal initialization
for convolutional and batch normalization layers.
"""
if isinstance(m, torch.nn.Linear):
torch.nn.init.xavier_normal_(m.weight)
if isinstance(m, torch.nn.BatchNorm2d):
m.weight.data.normal_(1.0, 0.02)
m.bias.data.fill_(0)
elif isinstance(m, torch.nn.Conv2d):
m.weight.data.normal_(0.0, 0.02)
90 changes: 90 additions & 0 deletions src/anomalib/models/image/glass/components/patch_maker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Handles patch-based processing of feature maps."""

import torch


class PatchMaker:
"""Handles patch-based processing of feature maps.

This class provides utilities for converting feature maps into patches,
reshaping patch scores back to original dimensions, and computing global
anomaly scores from patch-wise predictions.

Attributes:
patchsize (int): Size of each patch (patchsize x patchsize).
stride (int or None): Stride used for patch extraction. Defaults to patchsize if None.
top_k (int): Number of top patch scores to consider. Used for score reduction.
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor/nitpick : Please make sure the docstrings are in proper format and also that the params are in the order __init__(self, patchsize: int, top_k: int = 0, stride: int | None = None)

"""

def __init__(self, patchsize: int, top_k: int = 0, stride: int | None = None) -> None:
self.patchsize = patchsize
self.stride = stride if stride is not None else patchsize
self.top_k = top_k
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not see top_k being used in any of the methods. Am I missing something ?


def patchify(
self,
features: torch.Tensor,
return_spatial_info: bool = False,
) -> tuple[torch.Tensor, list[int]] | torch.Tensor:
"""Converts a batch of feature maps into patches.

Args:
features (torch.Tensor): Input feature maps of shape (B, C, H, W).
return_spatial_info (bool): If True, also returns spatial patch count. Default is False.

Returns:
torch.Tensor: Output tensor of shape (B, N, C, patchsize, patchsize), where N is number of patches.
list[int], optional: Number of patches in (height, width) dimensions, only if return_spatial_info is True.
"""
padding = int((self.patchsize - 1) / 2)
unfolder = torch.nn.Unfold(
kernel_size=self.patchsize,
stride=self.stride,
padding=padding,
dilation=1,
)
unfolded_features = unfolder(features)
number_of_total_patches = []
for s in features.shape[-2:]:
n_patches = (s + 2 * padding - 1 * (self.patchsize - 1) - 1) / self.stride + 1
number_of_total_patches.append(int(n_patches))
unfolded_features = unfolded_features.reshape(
*features.shape[:2],
self.patchsize,
self.patchsize,
-1,
)
unfolded_features = unfolded_features.permute(0, 4, 1, 2, 3)

if return_spatial_info:
return unfolded_features, number_of_total_patches
return unfolded_features

@staticmethod
def unpatch_scores(x: torch.Tensor, batchsize: int) -> torch.Tensor:
"""Reshapes patch scores back into per-batch format.

Args:
x (torch.Tensor): Input tensor of shape (B * N, ...).
batchsize (int): Original batch size.

Returns:
torch.Tensor: Reshaped tensor of shape (B, N, ...).
"""
return x.reshape(batchsize, -1, *x.shape[1:])

@staticmethod
def compute_score(x: torch.Tensor) -> torch.Tensor:
"""Computes final anomaly scores from patch-wise predictions.

Args:
x (torch.Tensor): Patch scores of shape (B, N, 1).

Returns:
torch.Tensor: Final anomaly score per image, shape (B,).
"""
x = x[:, :, 0] # remove last dimension if singleton
return torch.max(x, dim=1).values
66 changes: 66 additions & 0 deletions src/anomalib/models/image/glass/components/preprocessing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Maps input features to a fixed dimension using adaptive average pooling."""

import torch
import torch.nn.functional as f


class MeanMapper(torch.nn.Module):
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks very similar to the Aggregator class.
Could you please tell me the difference between their usage ?
Could one be redundant ?

"""Maps input features to a fixed dimension using adaptive average pooling.

Input: Variable-sized feature tensors
Output: Fixed-size feature representations
"""

def __init__(self, preprocessing_dim: int) -> None:
super().__init__()
self.preprocessing_dim = preprocessing_dim

def forward(self, features: torch.Tensor) -> torch.Tensor:
"""Applies adaptive average pooling to reshape features to a fixed size.

Args:
features (torch.Tensor): Input tensor of shape (B, *) where * denotes
any number of remaining dimensions. It is flattened before pooling.

Returns:
torch.Tensor: Output tensor of shape (B, D), where D is `preprocessing_dim`.
"""
features = features.reshape(len(features), 1, -1)
return f.adaptive_avg_pool1d(features, self.preprocessing_dim).squeeze(1)


class Preprocessing(torch.nn.Module):
"""Handles initial feature preprocessing across multiple input dimensions.

Input: List of features from different backbone layers
Output: Processed features with consistent dimensionality
"""

def __init__(self, input_dims: list[int | tuple[int, int]], output_dim: int) -> None:
super().__init__()
self.input_dims = input_dims
self.output_dim = output_dim

self.preprocessing_modules = torch.nn.ModuleList()
for _ in input_dims:
module = MeanMapper(output_dim)
self.preprocessing_modules.append(module)

def forward(self, features: list[torch.Tensor]) -> torch.Tensor:
"""Applies preprocessing modules to a list of input feature tensors.

Args:
features (list of torch.Tensor): List of feature maps from different
layers of the backbone network. Each tensor can have a different shape.

Returns:
torch.Tensor: A single tensor with shape (B, N, D), where B is the batch size,
N is the number of feature maps, and D is the output dimension (`output_dim`).
"""
features_ = []
for module, feature in zip(self.preprocessing_modules, features, strict=False):
features_.append(module(feature))
return torch.stack(features_, dim=1)
46 changes: 46 additions & 0 deletions src/anomalib/models/image/glass/components/projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Multi-layer projection network for feature adaptation."""

import torch

from .init_weight import init_weight


class Projection(torch.nn.Module):
"""Multi-layer projection network for feature adaptation.

Args:
in_planes: Input feature dimension
out_planes: Output feature dimension
n_layers: Number of projection layers
layer_type: Type of intermediate layers
"""

def __init__(self, in_planes: int, out_planes: int | None = None, n_layers: int = 1, layer_type: int = 0) -> None:
super().__init__()

if out_planes is None:
out_planes = in_planes
self.layers = torch.nn.Sequential()
in_ = None
out = None
for i in range(n_layers):
in_ = in_planes if i == 0 else out
out = out_planes
self.layers.add_module(f"{i}fc", torch.nn.Linear(in_, out))
if i < n_layers - 1 and layer_type > 1:
self.layers.add_module(f"{i}relu", torch.nn.LeakyReLU(0.2))
self.apply(init_weight)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Applies the projection network to the input features.

Args:
x (torch.Tensor): Input tensor of shape (B, in_planes), where B is the batch size.

Returns:
torch.Tensor: Transformed tensor of shape (B, out_planes).
"""
return self.layers(x)
Loading