duchieuvn commited on
Commit
2d020e8
·
1 Parent(s): eddf701

init project

Browse files
Files changed (4) hide show
  1. .gitignore +1 -0
  2. README.md +24 -1
  3. app.py +142 -0
  4. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ venv/
README.md CHANGED
@@ -9,4 +9,27 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  pinned: false
10
  ---
11
 
12
+ To use **EfficientNet** in the same way as ResNet-50 + PatchCore, just swap the backbone and pick an appropriate intermediate feature map. The steps are identical:
13
+
14
+ 1. **Backbone Selection & Freezing**
15
+ – Choose an EfficientNet variant (e.g. B4 or B5) pretrained on ImageNet.
16
+ – Remove (or ignore) the final classification head and freeze all weights.
17
+
18
+ 2. **Feature-Map Extraction**
19
+ – Identify a mid-level block whose spatial resolution is neither too coarse nor too fine (e.g. the output of MBConv block 4 or 5).
20
+ – Pass each input image through EfficientNet and take that block’s tensor of shape $$[C,H,W]$$.
21
+
22
+ 3. **Flatten into Patch Embeddings**
23
+ – For each spatial location $$(i,j)$$, flatten the $$C$$-dim vector into a patch embedding.
24
+ – Collect all embeddings from your **normal** training images into a large memory bank.
25
+
26
+ 4. **Memory Bank & k-NN Detector**
27
+ – Fit a k-NN model (PatchCore) on the normal patch embeddings.
28
+ – At test time, extract patch embeddings from new images and compute the Euclidean (or cosine) distance to nearest neighbors in the memory bank.
29
+ – That distance is your anomaly score per patch.
30
+
31
+ 5. **Anomaly Map & Segmentation**
32
+ – Reshape the patch scores into an $$[H,W]$$ map.
33
+ – Upsample to the original image resolution (bilinear) and apply a threshold or morphological filtering to segment abnormal regions.
34
+
35
+ Because EfficientNet’s inverted-bottleneck blocks are more parameter-efficient, you often get similar or better detection accuracy with lower FLOPs than ResNet-50—while the overall PatchCore workflow remains unchanged.
app.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import gradio as gr
4
+ import numpy as np
5
+ import torch
6
+ import torchvision.models as models
7
+ import torchvision.transforms as transforms
8
+ from PIL import Image
9
+ import glob
10
+ from sklearn.neighbors import NearestNeighbors
11
+
12
+ # ----------------- Config -----------------
13
+ BACKBONE_NAME = "efficientnet_b4"
14
+ PRETRAINED_WEIGHTS = models.EfficientNet_B4_Weights.IMAGENET1K_V1
15
+ FEATURE_LAYER_HOOK = "features[5]"
16
+ IMAGE_SIZE = 256
17
+ MEMORY_BANK_PATH = "memory_bank.npy"
18
+
19
+ class AnomalyDetector:
20
+ """
21
+ Encapsulates the anomaly detection model, data, and prediction logic.
22
+ """
23
+ def __init__(self, memory_bank_path: str):
24
+ """
25
+ Initializes the detector by loading the model, transforms, and memory bank.
26
+ """
27
+ print("Initializing AnomalyDetector...")
28
+ self.model, self.transform = self._get_model_and_transforms()
29
+ print("Model and transforms loaded.")
30
+
31
+ memory_bank = np.load(memory_bank_path)
32
+ print(f"Memory bank loaded from {memory_bank_path} with shape {memory_bank.shape}.")
33
+
34
+ self.knn = NearestNeighbors(n_neighbors=3, algorithm='ball_tree', metric='minkowski', p=2.0)
35
+ self.knn.fit(memory_bank)
36
+ print("k-NN detector fitted.")
37
+
38
+ def _get_model_and_transforms(self):
39
+ model = models.efficientnet_b4(weights=PRETRAINED_WEIGHTS)
40
+ for p in model.parameters():
41
+ p.requires_grad = False
42
+ model.eval()
43
+ transform = transforms.Compose([
44
+ transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
45
+ transforms.ToTensor(),
46
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
47
+ std=[0.229, 0.224, 0.225]),
48
+ ])
49
+ return model, transform
50
+
51
+ def _extract_features(self, image_tensor: torch.Tensor) -> torch.Tensor:
52
+ features = None
53
+ def hook(_, __, output):
54
+ nonlocal features
55
+ features = output
56
+ handle = eval(f"self.model.{FEATURE_LAYER_HOOK}").register_forward_hook(hook)
57
+ with torch.no_grad():
58
+ self.model(image_tensor.unsqueeze(0))
59
+ handle.remove()
60
+ return features # [1, C, H, W]
61
+
62
+ def predict(self, image_path: str):
63
+ """
64
+ Processes an image from a file path and returns the original, a heatmap, and an overlay.
65
+ """
66
+ image = Image.open(image_path)
67
+
68
+ # 1. Pre-process image
69
+ img_rgb = image.convert("RGB")
70
+ img_resized = img_rgb.resize((IMAGE_SIZE, IMAGE_SIZE))
71
+ image_tensor = self.transform(img_rgb)
72
+
73
+ # 2. Extract patch embeddings
74
+ feature_map = self._extract_features(image_tensor)
75
+ h, w = feature_map.shape[2], feature_map.shape[3]
76
+ embedding = feature_map.squeeze(0).permute(1, 2, 0).reshape(-1, feature_map.shape[1])
77
+ embedding = embedding.numpy()
78
+
79
+ # 3. Get anomaly scores from k-NN
80
+ distances, _ = self.knn.kneighbors(embedding)
81
+ patch_scores = np.mean(distances, axis=1)
82
+ anomaly_map = patch_scores.reshape(h, w)
83
+
84
+ # 4. Prepare anomaly map for visualization with a fixed range
85
+ # Resize the raw score map
86
+ anomaly_map_resized = cv2.resize(anomaly_map, (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_LINEAR)
87
+
88
+ # Clip scores to the fixed range [vmin, vmax]
89
+ vmin, vmax = 0, 200
90
+ clipped_map = np.clip(anomaly_map_resized, vmin, vmax)
91
+
92
+ # Scale the clipped scores to the 0-255 range for the colormap
93
+ scaled_map = 255 * (clipped_map - vmin) / (vmax - vmin)
94
+
95
+ # 5. Create visualizations
96
+ heatmap_rgb = cv2.applyColorMap(scaled_map.astype(np.uint8), cv2.COLORMAP_JET)
97
+ heatmap_rgb = cv2.cvtColor(heatmap_rgb, cv2.COLOR_BGR2RGB)
98
+ overlay = cv2.addWeighted(np.array(img_resized), 0.6, heatmap_rgb, 0.4, 0)
99
+
100
+ return img_resized, heatmap_rgb, overlay
101
+
102
+ DESCRIPTION = """
103
+ **PatchCore-style Anomaly Detection (EfficientNet-B4, k-NN on patch embeddings)**
104
+
105
+ - Upload an image (PNG/JPG).
106
+ - App returns the resized original, anomaly heatmap, and overlay.
107
+ - This app requires a `memory_bank.npy` file in the repository root.
108
+ """
109
+
110
+ examples = []
111
+ if os.path.isdir("examples"):
112
+ for f in os.listdir("examples"):
113
+ if f.lower().endswith((".png", ".jpg", ".jpeg")):
114
+ examples.append(os.path.join("examples", f))
115
+
116
+ if __name__ == "__main__":
117
+ if not os.path.exists(MEMORY_BANK_PATH):
118
+ print(f"FATAL: `{MEMORY_BANK_PATH}` not found.")
119
+ exit()
120
+
121
+ # Create a single instance of the detector. This performs the one-time setup.
122
+ detector = AnomalyDetector(MEMORY_BANK_PATH)
123
+
124
+ demo = gr.Interface(
125
+ fn=detector.predict,
126
+ inputs=gr.Dropdown(
127
+ choices=glob.glob("dataset/**/*.png", recursive=True),
128
+ label="Select Test Image",
129
+ info="Select a PNG file from the dataset directory."
130
+ ),
131
+ outputs=[
132
+ gr.Image(type="pil", label="Original (Resized)"),
133
+ gr.Image(type="pil", label="Anomaly Heatmap"),
134
+ gr.Image(type="pil", label="Overlay"),
135
+ ],
136
+ title="Anomaly Detection (EfficientNet-B4 + kNN)",
137
+ description=DESCRIPTION,
138
+ allow_flagging="never",
139
+ examples=examples if examples else None
140
+ )
141
+
142
+ demo.queue().launch()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch==2.2.2
2
+ torchvision==0.17.2
3
+ gradio>=4.36.0
4
+ scikit-learn>=1.3.2
5
+ opencv-python-headless>=4.9.0.80
6
+ Pillow>=10.3.0
7
+ numpy==1.26.4
8
+ fiftyone