|
"""Post Process This module contains utils function to apply post-processing to the output predictions.""" |
|
|
|
|
|
|
|
|
|
|
|
import math |
|
|
|
import cv2 |
|
import numpy as np |
|
from skimage import morphology |
|
|
|
|
|
def add_label( |
|
image: np.ndarray, |
|
label_name: str, |
|
color: tuple[int, int, int], |
|
confidence: float | None = None, |
|
font_scale: float = 5e-3, |
|
thickness_scale: float = 1e-3, |
|
) -> np.ndarray: |
|
"""Add a label to an image. |
|
|
|
Args: |
|
image (np.ndarray): Input image. |
|
label_name (str): Name of the label that will be displayed on the image. |
|
color (tuple[int, int, int]): RGB values for background color of label. |
|
confidence (float | None): confidence score of the label. |
|
font_scale (float): scale of the font size relative to image size. Increase for bigger font. |
|
thickness_scale (float): scale of the font thickness. Increase for thicker font. |
|
|
|
Returns: |
|
np.ndarray: Image with label. |
|
""" |
|
image = image.copy() |
|
img_height, img_width, _ = image.shape |
|
|
|
font = cv2.FONT_HERSHEY_PLAIN |
|
text = label_name if confidence is None else f"{label_name} ({confidence*100:.0f}%)" |
|
|
|
|
|
font_scale = min(img_width, img_height) * font_scale |
|
thickness = math.ceil(min(img_width, img_height) * thickness_scale) |
|
(width, height), baseline = cv2.getTextSize(text, font, fontScale=font_scale, thickness=thickness) |
|
|
|
|
|
label_patch = np.zeros((height + baseline, width + baseline, 3), dtype=np.uint8) |
|
label_patch[:, :] = color |
|
cv2.putText( |
|
label_patch, |
|
text, |
|
(0, baseline // 2 + height), |
|
font, |
|
fontScale=font_scale, |
|
thickness=thickness, |
|
color=0, |
|
lineType=cv2.LINE_AA, |
|
) |
|
|
|
|
|
image[: baseline + height, : baseline + width] = label_patch |
|
return image |
|
|
|
|
|
def add_normal_label(image: np.ndarray, confidence: float | None = None) -> np.ndarray: |
|
"""Add the normal label to the image.""" |
|
return add_label(image, "normal", (225, 252, 134), confidence) |
|
|
|
|
|
def add_anomalous_label(image: np.ndarray, confidence: float | None = None) -> np.ndarray: |
|
"""Add the anomalous label to the image.""" |
|
return add_label(image, "anomalous", (255, 100, 100), confidence) |
|
|
|
|
|
def anomaly_map_to_color_map(anomaly_map: np.ndarray, normalize: bool = True) -> np.ndarray: |
|
"""Compute anomaly color heatmap. |
|
|
|
Args: |
|
anomaly_map (np.ndarray): Final anomaly map computed by the distance metric. |
|
normalize (bool, optional): Bool to normalize the anomaly map prior to applying |
|
the color map. Defaults to True. |
|
|
|
Returns: |
|
np.ndarray: [description] |
|
""" |
|
if normalize: |
|
anomaly_map = (anomaly_map - anomaly_map.min()) / np.ptp(anomaly_map) |
|
anomaly_map = anomaly_map * 255 |
|
anomaly_map = anomaly_map.astype(np.uint8) |
|
|
|
anomaly_map = cv2.applyColorMap(anomaly_map, cv2.COLORMAP_JET) |
|
return cv2.cvtColor(anomaly_map, cv2.COLOR_BGR2RGB) |
|
|
|
|
|
def superimpose_anomaly_map( |
|
anomaly_map: np.ndarray, |
|
image: np.ndarray, |
|
alpha: float = 0.4, |
|
gamma: int = 0, |
|
normalize: bool = False, |
|
) -> np.ndarray: |
|
"""Superimpose anomaly map on top of in the input image. |
|
|
|
Args: |
|
anomaly_map (np.ndarray): Anomaly map |
|
image (np.ndarray): Input image |
|
alpha (float, optional): Weight to overlay anomaly map |
|
on the input image. Defaults to 0.4. |
|
gamma (int, optional): Value to add to the blended image |
|
to smooth the processing. Defaults to 0. Overall, |
|
the formula to compute the blended image is |
|
I' = (alpha*I1 + (1-alpha)*I2) + gamma |
|
normalize: whether or not the anomaly maps should |
|
be normalized to image min-max at image level |
|
|
|
|
|
Returns: |
|
np.ndarray: Image with anomaly map superimposed on top of it. |
|
""" |
|
anomaly_map = anomaly_map_to_color_map(anomaly_map.squeeze(), normalize=normalize) |
|
return cv2.addWeighted(anomaly_map, alpha, image, (1 - alpha), gamma) |
|
|
|
|
|
def compute_mask(anomaly_map: np.ndarray, threshold: float, kernel_size: int = 4) -> np.ndarray: |
|
"""Compute anomaly mask via thresholding the predicted anomaly map. |
|
|
|
Args: |
|
anomaly_map (np.ndarray): Anomaly map predicted via the model |
|
threshold (float): Value to threshold anomaly scores into 0-1 range. |
|
kernel_size (int): Value to apply morphological operations to the predicted mask. Defaults to 4. |
|
|
|
Returns: |
|
Predicted anomaly mask |
|
""" |
|
anomaly_map = anomaly_map.squeeze() |
|
mask: np.ndarray = np.zeros_like(anomaly_map).astype(np.uint8) |
|
mask[anomaly_map > threshold] = 1 |
|
|
|
kernel = morphology.disk(kernel_size) |
|
mask = morphology.opening(mask, kernel) |
|
|
|
mask *= 255 |
|
|
|
return mask |
|
|
|
|
|
def draw_boxes(image: np.ndarray, boxes: np.ndarray, color: tuple[int, int, int]) -> np.ndarray: |
|
"""Draw bounding boxes on an image. |
|
|
|
Args: |
|
image (np.ndarray): Source image. |
|
boxes (np.nparray): 2D array of shape (N, 4) where each row contains the xyxy coordinates of a bounding box. |
|
color (tuple[int, int, int]): Color of the drawn boxes in RGB format. |
|
|
|
Returns: |
|
np.ndarray: Image showing the bounding boxes drawn on top of the source image. |
|
""" |
|
for box in boxes: |
|
x_1, y_1, x_2, y_2 = box.astype(int) |
|
image = cv2.rectangle(image, (x_1, y_1), (x_2, y_2), color=color, thickness=2) |
|
return image |
|
|