KB Teo commited on
Commit
098c98c
·
1 Parent(s): 1c527f9

Create space

Browse files
README.md CHANGED
@@ -1,7 +1,7 @@
1
  ---
2
- title: AD Anomalib
3
- emoji: 📉
4
- colorFrom: blue
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 4.16.0
 
1
  ---
2
+ title: Anomaly Detection
3
+ emoji: 🏭
4
+ colorFrom: red
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 4.16.0
app.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import *
2
+ import importlib
3
+ from pathlib import Path
4
+
5
+ import cv2
6
+ import gradio as gr
7
+ import numpy as np
8
+
9
+ available_modes = []
10
+ inferencer = None
11
+ last_model_path: str = None
12
+
13
+ if importlib.util.find_spec("openvino"):
14
+ available_modes.append("OpenVINO")
15
+ from inference.openvino import OpenVINOInferencer
16
+ def predict_openvino(
17
+ image: Union[str, Path, np.ndarray],
18
+ model_path: Union[str, Path],
19
+ device: str) -> Dict[str, np.ndarray]:
20
+ global inferencer, last_model_path
21
+ if not isinstance(inferencer, OpenVINOInferencer) or last_model_path != str(model_path):
22
+ inferencer = OpenVINOInferencer(
23
+ path = model_path,
24
+ device=device.upper()
25
+ )
26
+ last_model_path = str(model_path)
27
+ return inferencer(image)
28
+
29
+ def draw_contour(image, mask, color=(255,0,0), thickness=3):
30
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
31
+ return cv2.drawContours(image.copy(), contours, -1, color, thickness)
32
+
33
+ def convert_to_heatmap(heatmap):
34
+ heatmap = cv2.applyColorMap((heatmap*255).astype("uint8"), cv2.COLORMAP_HSV)
35
+ return heatmap
36
+
37
+ def predict(image, device, model_dir):
38
+ outputs = predict_openvino(image, model_dir, device)
39
+ out_image = draw_contour(image, outputs["pred_mask"])
40
+ heatmap = convert_to_heatmap(outputs["anomaly_map"])
41
+ return out_image, heatmap
42
+
43
+ def launch():
44
+ input_image = gr.Image(label="Input image")
45
+ devices = gr.Radio(
46
+ label="Device",
47
+ choices=["AUTO", "CPU", "CUDA"],
48
+ value="CPU",
49
+ interactive=False
50
+ )
51
+ output_image = gr.Image(label="Output image")
52
+ output_heatmap = gr.Image(label="Heatmap"),
53
+ model = gr.Text(label="Model", interactive=False, value="models/default")
54
+ intf = gr.Interface(
55
+ title="Anomaly Detection",
56
+ fn=predict,
57
+ inputs=[input_image, devices, model],
58
+ outputs=[output_image, output_heatmap]
59
+ )
60
+ intf.launch()
61
+
62
+ if __name__ == "__main__":
63
+ launch()
inference/openvino/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .ov_infer import OpenVINOInferencer
inference/openvino/ov_infer.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import *
2
+ from pathlib import Path
3
+ import importlib
4
+
5
+ import albumentations as A
6
+ import cv2
7
+ from omegaconf import DictConfig, OmegaConf
8
+ import numpy as np
9
+
10
+ from .utils import read_image, standardize, normalize_cdf, normalize_min_max, get_boxes
11
+
12
+ if importlib.util.find_spec("openvino") is not None:
13
+ import openvino as ov
14
+ else:
15
+ raise ImportError("OpenVINO is not installed.")
16
+
17
+ class OpenVINOInferencer():
18
+ """For perform inference with OpenVINO.
19
+
20
+ Args:
21
+ path (str | Path): Folder path to exported OpenVINO model. Must contains
22
+ model.xml, model.bin, and metadata.json.
23
+ device (str): Device to run inference on. Defaults to "AUTO".
24
+ cache_dir (str | Path): Cache directory for OpenVINO
25
+ """
26
+ def __init__(self,
27
+ path: Union[str, Path],
28
+ device: str="AUTO",
29
+ cache_dir: Union[str, Path, None]=None
30
+ ) -> None:
31
+ if isinstance(path, str):
32
+ path = Path(path)
33
+
34
+ self.model = self._load_model(path, device, cache_dir)
35
+ self.metadata = self._load_metadata(path)
36
+
37
+ # Note: Transformation require Albumentations package
38
+ self.transform = A.from_dict(self.metadata["transform"])
39
+ self.metadata["expand_offset"] = self._get_expand_offset(self.transform)
40
+
41
+ # Record input & output blob (key)
42
+ self.metadata["input_blob"] = self.model.input(0).get_names().pop()
43
+ self.metadata["output_blob"] = self.model.output(0).get_names().pop()
44
+
45
+ def _load_model(self, path: Path, device: str, cache_dir: Union[str, Path, None]) -> ov.CompiledModel:
46
+ xml_path = path / "model.xml"
47
+ bin_path = path / "model.bin"
48
+
49
+ ov_core = ov.Core()
50
+ model = ov_core.read_model(xml_path, bin_path)
51
+
52
+ # Create cache directory
53
+ if cache_dir is None:
54
+ cache_dir = "cache"
55
+ if isinstance(cache_dir, str):
56
+ cache_dir = Path(cache_dir)
57
+ cache_dir.mkdir(parents=True, exist_ok=True)
58
+ ov_core.set_property({"CACHE_DIR": cache_dir})
59
+
60
+ model = ov_core.compile_model(model=model, device_name=device.upper())
61
+ return model
62
+
63
+ def _load_metadata(self, path: Path) -> DictConfig:
64
+ metadata = path / "metadata.json"
65
+ metadata = OmegaConf.load(metadata)
66
+ metadata = cast(DictConfig, metadata)
67
+ return metadata
68
+
69
+ def _get_expand_offset(self, transform):
70
+ is_center_cropped = False
71
+ for t in reversed(transform.transforms):
72
+ if isinstance(t, A.CenterCrop):
73
+ is_center_cropped = True
74
+ cropped_h = t.height
75
+ cropped_w = t.width
76
+ elif isinstance(t, A.Resize) and is_center_cropped:
77
+ return (t.height - cropped_h) // 2, (t.width - cropped_w) // 2
78
+
79
+ def predict(self, image: Union[str, Path, np.ndarray]) -> Dict[str, np.ndarray]:
80
+ if isinstance(image, (str, Path)):
81
+ image = read_image(image)
82
+
83
+ # Record input image size
84
+ self.metadata["image_shape"] = image.shape[:2]
85
+
86
+ inputs = self._pre_process(image, self.transform)
87
+ predictions = self.model(inputs)
88
+ outputs = self._post_processs(predictions, self.metadata)
89
+ outputs.update({"image": image})
90
+ return outputs
91
+
92
+ def __call__(self, image: Union[str, Path, np.ndarray]) -> Dict[str, np.ndarray]:
93
+ return self.predict(image)
94
+
95
+ def _pre_process(self, image: np.ndarray, transform=None) -> np.ndarray:
96
+ if transform is not None:
97
+ image = transform(image=image)["image"]
98
+
99
+ if len(image.shape) == 3:
100
+ # Add batch_size axis
101
+ image = np.expand_dims(image, axis=0)
102
+
103
+ if image.shape[3] == 3:
104
+ # Transpose the color_channel axis
105
+ # Expected shape: [b, c, h, w]
106
+ image = image.transpose(0, 3, 1, 2)
107
+
108
+ return image
109
+
110
+ def _post_processs(self, predictions: np.ndarray, metadata: DictConfig) -> Dict[str, np.ndarray]:
111
+ predictions = predictions[metadata["output_blob"]]
112
+
113
+ anomaly_map: np.ndarray = None
114
+ pred_label: float = None
115
+ pred_mask: float = None
116
+
117
+ if metadata["task"] == "classification":
118
+ pred_score = predictions
119
+ else:
120
+ anomaly_map = predictions.squeeze()
121
+ pred_score = anomaly_map.reshape(-1).max()
122
+
123
+ if "image_threshold" in metadata:
124
+ # Assign anomalous label to predictions with score >= threshold
125
+ pred_label = pred_score >= metadata["image_threshold"]
126
+
127
+ if metadata["task"] == "classification":
128
+ _, pred_score = self._normalize(pred_scores=pred_score, metadata=metadata)
129
+ else:
130
+ if "pixel_threshold" in metadata:
131
+ pred_mask = (anomaly_map >= metadata["pixel_threshold"]).astype(np.uint8)
132
+
133
+ anomaly_map, pred_score = self._normalize(
134
+ pred_scores=pred_score,
135
+ metadata=metadata,
136
+ anomaly_map=anomaly_map
137
+ )
138
+
139
+ if "image_shape" in metadata and anomaly_map.shape != metadata["image_shape"]:
140
+ if "expand_offset" in metadata and metadata["expand_offset"] is not None:
141
+ anomaly_map = self._expand(anomaly_map, metadata["expand_offset"][0], metadata["expand_offset"][1])
142
+ pred_mask = self._expand(pred_mask, metadata["expand_offset"][0], metadata["expand_offset"][1])
143
+ h, w = metadata["image_shape"] # Fix: cv2.resize take (w, h) as argument
144
+ anomaly_map = cv2.resize(anomaly_map, (w, h))
145
+ if pred_mask is not None:
146
+ pred_mask = cv2.resize(pred_mask, (w, h))
147
+
148
+ if metadata["task"] == "detection":
149
+ pred_boxes = get_boxes(pred_mask)
150
+ box_labels = np.ones(pred_boxes.shape[0])
151
+ else:
152
+ pred_boxes: np.ndarray | None = None
153
+ box_labels: np.ndarray | None = None
154
+
155
+ return {
156
+ "anomaly_map": anomaly_map,
157
+ "pred_label": pred_label,
158
+ "pred_score": pred_score,
159
+ "pred_mask": pred_mask,
160
+ "pred_boxes": pred_boxes,
161
+ "box_labels": box_labels
162
+ }
163
+
164
+ @staticmethod
165
+ def _expand(map, offset_h, offset_w):
166
+ h, w = map.shape
167
+ if map is not None:
168
+ expanded_map = np.zeros((h + offset_h * 2, w + offset_w * 2), dtype=map.dtype)
169
+ expanded_map[offset_h:offset_h+h, offset_w:offset_w+w] = map
170
+ return expanded_map
171
+
172
+ @staticmethod
173
+ def _normalize(
174
+ pred_scores: np.float32,
175
+ metadata: DictConfig,
176
+ anomaly_map: np.ndarray | None = None
177
+ ) -> Tuple[Union[np.ndarray, None], float]:
178
+ # Min-max normalization
179
+ if "min" in metadata and "max" in metadata:
180
+ if anomaly_map is not None:
181
+ anomaly_map = normalize_min_max(
182
+ anomaly_map,
183
+ metadata["pixel_threshold"],
184
+ metadata["min"],
185
+ metadata["max"]
186
+ )
187
+ pred_scores = normalize_min_max(
188
+ pred_scores,
189
+ metadata["image_threshold"],
190
+ metadata["min"],
191
+ metadata["max"]
192
+ )
193
+
194
+ # Standardize pixel scores
195
+ if "pixel_mean" in metadata and "pixel_std" in metadata:
196
+ if anomaly_map is not None:
197
+ anomaly_map = standardize(
198
+ anomaly_map,
199
+ metadata["pixel_mean"],
200
+ metadata["pixel_std"],
201
+ center_at=metadata["image_mean"]
202
+ )
203
+ anomaly_map = normalize_cdf(
204
+ anomaly_map,
205
+ metadata["pixel_threshold"]
206
+ )
207
+
208
+ # Standardize image scores
209
+ if "image_mean" in metadata and "image_std" in metadata:
210
+ pred_scores = standardize(
211
+ pred_scores,
212
+ metadata["image_mean"],
213
+ metadata["image_std"]
214
+ )
215
+ pred_scores = normalize_cdf(
216
+ pred_scores,
217
+ metadata["image_threshold"]
218
+ )
219
+
220
+ return anomaly_map, float(pred_scores)
inference/openvino/utils.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import *
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from scipy.stats import norm
7
+
8
+ def read_image(
9
+ path: Union[str, Path],
10
+ image_size: Union[int, Tuple[int, int], None]=None
11
+ ) -> np.ndarray:
12
+ """Read image from file path in RGB format.
13
+
14
+ Args:
15
+ path (str | Path): Path to image file.
16
+ image_size (int | tuple[int, int] | None, optional): Resize image.
17
+
18
+ Returns:
19
+ image (np.ndarray): The image array.
20
+
21
+ Example:
22
+ >>> read_image("/path/to/image.jpg", 256)
23
+ """
24
+ if isinstance(path, Path):
25
+ path = str(path)
26
+ image = cv2.imread(path)
27
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
28
+
29
+ if image_size:
30
+ if isinstance(image_size, int):
31
+ image_size = (image_size, image_size)
32
+ image = cv2.resize(image, image_size, interpolation=cv2.INTER_AREA)
33
+
34
+ return image
35
+
36
+ def standardize(
37
+ targets: np.ndarray,
38
+ mean: float,
39
+ std: float,
40
+ center_at: float | None = None
41
+ ) -> np.ndarray:
42
+ """Standardize the targets to the z-domain."""
43
+ targets = np.log(targets)
44
+ standardized = (targets - mean) / std
45
+ if center_at:
46
+ standardized -= (center_at - mean) / std
47
+ return standardized
48
+
49
+ def normalize_cdf(targets: np.ndarray, threshold: float) -> np.ndarray:
50
+ return norm.cdf(targets - threshold)
51
+
52
+ def normalize_min_max(
53
+ targets: np.ndarray | np.float32,
54
+ threshold: float | np.ndarray,
55
+ min_val: float | np.ndarray,
56
+ max_val: float | np.ndarray
57
+ ) -> np.ndarray:
58
+ normalized = ((targets - threshold) / (max_val - min_val)) + 0.5
59
+ normalized = np.minimum(normalized, 1)
60
+ normalized = np.maximum(normalized, 0)
61
+ return normalized
62
+
63
+ def get_boxes(mask: np.ndarray) -> np.ndarray:
64
+ """Get bounding boxes from masks.
65
+
66
+ Args:
67
+ masks (np.ndarray): Input mask of shape (H, W).
68
+
69
+ Returns:
70
+ boxes (np.ndarray): Array of shape (N, 4) containing bounding boxes in xyxy format.
71
+ """
72
+ _, comps = cv2.connectedComponents(mask)
73
+ labels = comps = np.unique(comps)
74
+ boxes = []
75
+ for label in labels[labels != 0]:
76
+ y_loc, x_loc = np.where(comps == label)
77
+ boxes.append((np.min(x_loc), np.min(y_loc), np.max(x_loc), np.ma(y_loc)))
78
+ boxes = np.stack(boxes) if boxes else np.empty((0, 4))
79
+ return boxes
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ OmegaConf>=2.1.1
2
+ albumentations>=1.1.0,<2.0.0
3
+ opencv-python>=4.5.3.56
4
+ scipy>=1.11.4,<2.0.0
5
+ openvino>=2023.2.0,<2024.0.0