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
|