Spaces:
Running
Running
KB Teo
commited on
Commit
·
098c98c
1
Parent(s):
1c527f9
Create space
Browse files- README.md +3 -3
- app.py +63 -0
- inference/openvino/__init__.py +1 -0
- inference/openvino/ov_infer.py +220 -0
- inference/openvino/utils.py +79 -0
- requirements.txt +5 -0
README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
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
|