|
from enum import Enum |
|
from typing import Dict, Tuple |
|
|
|
import cv2 |
|
import numpy as np |
|
from skimage.exposure import rescale_intensity |
|
|
|
from inference.core.env import ( |
|
DISABLE_PREPROC_CONTRAST, |
|
DISABLE_PREPROC_GRAYSCALE, |
|
DISABLE_PREPROC_STATIC_CROP, |
|
) |
|
from inference.core.exceptions import PreProcessingError |
|
|
|
STATIC_CROP_KEY = "static-crop" |
|
CONTRAST_KEY = "contrast" |
|
GRAYSCALE_KEY = "grayscale" |
|
ENABLED_KEY = "enabled" |
|
TYPE_KEY = "type" |
|
|
|
|
|
class ContrastAdjustmentType(Enum): |
|
CONTRAST_STRETCHING = "Contrast Stretching" |
|
HISTOGRAM_EQUALISATION = "Histogram Equalization" |
|
ADAPTIVE_EQUALISATION = "Adaptive Equalization" |
|
|
|
|
|
def prepare( |
|
image: np.ndarray, |
|
preproc, |
|
disable_preproc_contrast: bool = False, |
|
disable_preproc_grayscale: bool = False, |
|
disable_preproc_static_crop: bool = False, |
|
) -> Tuple[np.ndarray, Tuple[int, int]]: |
|
""" |
|
Prepares an image by applying a series of preprocessing steps defined in the `preproc` dictionary. |
|
|
|
Args: |
|
image (PIL.Image.Image): The input PIL image object. |
|
preproc (dict): Dictionary containing preprocessing steps. Example: |
|
{ |
|
"resize": {"enabled": true, "width": 416, "height": 416, "format": "Stretch to"}, |
|
"static-crop": {"y_min": 25, "x_max": 75, "y_max": 75, "enabled": true, "x_min": 25}, |
|
"auto-orient": {"enabled": true}, |
|
"grayscale": {"enabled": true}, |
|
"contrast": {"enabled": true, "type": "Adaptive Equalization"} |
|
} |
|
disable_preproc_contrast (bool, optional): If true, the contrast preprocessing step is disabled for this call. Default is False. |
|
disable_preproc_grayscale (bool, optional): If true, the grayscale preprocessing step is disabled for this call. Default is False. |
|
disable_preproc_static_crop (bool, optional): If true, the static crop preprocessing step is disabled for this call. Default is False. |
|
|
|
Returns: |
|
PIL.Image.Image: The preprocessed image object. |
|
tuple: The dimensions of the image. |
|
|
|
Note: |
|
The function uses global flags like `DISABLE_PREPROC_AUTO_ORIENT`, `DISABLE_PREPROC_STATIC_CROP`, etc. |
|
to conditionally enable or disable certain preprocessing steps. |
|
""" |
|
try: |
|
h, w = image.shape[0:2] |
|
img_dims = (h, w) |
|
if static_crop_should_be_applied( |
|
preprocessing_config=preproc, |
|
disable_preproc_static_crop=disable_preproc_static_crop, |
|
): |
|
image = take_static_crop( |
|
image=image, crop_parameters=preproc[STATIC_CROP_KEY] |
|
) |
|
if contrast_adjustments_should_be_applied( |
|
preprocessing_config=preproc, |
|
disable_preproc_contrast=disable_preproc_contrast, |
|
): |
|
adjustment_type = ContrastAdjustmentType(preproc[CONTRAST_KEY][TYPE_KEY]) |
|
image = apply_contrast_adjustment( |
|
image=image, adjustment_type=adjustment_type |
|
) |
|
if grayscale_conversion_should_be_applied( |
|
preprocessing_config=preproc, |
|
disable_preproc_grayscale=disable_preproc_grayscale, |
|
): |
|
image = apply_grayscale_conversion(image=image) |
|
return image, img_dims |
|
except KeyError as error: |
|
raise PreProcessingError( |
|
f"Pre-processing of image failed due to misconfiguration. Missing key: {error}." |
|
) from error |
|
|
|
|
|
def static_crop_should_be_applied( |
|
preprocessing_config: dict, |
|
disable_preproc_static_crop: bool, |
|
) -> bool: |
|
return ( |
|
STATIC_CROP_KEY in preprocessing_config.keys() |
|
and not DISABLE_PREPROC_STATIC_CROP |
|
and not disable_preproc_static_crop |
|
and preprocessing_config[STATIC_CROP_KEY][ENABLED_KEY] |
|
) |
|
|
|
|
|
def take_static_crop(image: np.ndarray, crop_parameters: Dict[str, int]) -> np.ndarray: |
|
height, width = image.shape[0:2] |
|
x_min = int(crop_parameters["x_min"] / 100 * width) |
|
y_min = int(crop_parameters["y_min"] / 100 * height) |
|
x_max = int(crop_parameters["x_max"] / 100 * width) |
|
y_max = int(crop_parameters["y_max"] / 100 * height) |
|
return image[y_min:y_max, x_min:x_max, :] |
|
|
|
|
|
def contrast_adjustments_should_be_applied( |
|
preprocessing_config: dict, |
|
disable_preproc_contrast: bool, |
|
) -> bool: |
|
return ( |
|
CONTRAST_KEY in preprocessing_config.keys() |
|
and not DISABLE_PREPROC_CONTRAST |
|
and not disable_preproc_contrast |
|
and preprocessing_config[CONTRAST_KEY][ENABLED_KEY] |
|
) |
|
|
|
|
|
def apply_contrast_adjustment( |
|
image: np.ndarray, |
|
adjustment_type: ContrastAdjustmentType, |
|
) -> np.ndarray: |
|
adjustment = CONTRAST_ADJUSTMENTS_METHODS[adjustment_type] |
|
return adjustment(image) |
|
|
|
|
|
def apply_contrast_stretching(image: np.ndarray) -> np.ndarray: |
|
p2, p98 = np.percentile(image, (2, 98)) |
|
return rescale_intensity(image, in_range=(p2, p98)) |
|
|
|
|
|
def apply_histogram_equalisation(image: np.ndarray) -> np.ndarray: |
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
image = cv2.equalizeHist(image) |
|
return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
|
|
|
|
|
def apply_adaptive_equalisation(image: np.ndarray) -> np.ndarray: |
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
clahe = cv2.createCLAHE(clipLimit=0.03, tileGridSize=(8, 8)) |
|
image = clahe.apply(image) |
|
return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
|
|
|
|
|
CONTRAST_ADJUSTMENTS_METHODS = { |
|
ContrastAdjustmentType.CONTRAST_STRETCHING: apply_contrast_stretching, |
|
ContrastAdjustmentType.HISTOGRAM_EQUALISATION: apply_histogram_equalisation, |
|
ContrastAdjustmentType.ADAPTIVE_EQUALISATION: apply_adaptive_equalisation, |
|
} |
|
|
|
|
|
def grayscale_conversion_should_be_applied( |
|
preprocessing_config: dict, |
|
disable_preproc_grayscale: bool, |
|
) -> bool: |
|
return ( |
|
GRAYSCALE_KEY in preprocessing_config.keys() |
|
and not DISABLE_PREPROC_GRAYSCALE |
|
and not disable_preproc_grayscale |
|
and preprocessing_config[GRAYSCALE_KEY][ENABLED_KEY] |
|
) |
|
|
|
|
|
def apply_grayscale_conversion(image: np.ndarray) -> np.ndarray: |
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) |
|
|
|
|
|
def letterbox_image( |
|
image: np.ndarray, |
|
desired_size: Tuple[int, int], |
|
color: Tuple[int, int, int] = (0, 0, 0), |
|
) -> np.ndarray: |
|
""" |
|
Resize and pad image to fit the desired size, preserving its aspect ratio. |
|
|
|
Parameters: |
|
- image: numpy array representing the image. |
|
- desired_size: tuple (width, height) representing the target dimensions. |
|
- color: tuple (B, G, R) representing the color to pad with. |
|
|
|
Returns: |
|
- letterboxed image. |
|
""" |
|
resized_img = resize_image_keeping_aspect_ratio( |
|
image=image, |
|
desired_size=desired_size, |
|
) |
|
new_height, new_width = resized_img.shape[:2] |
|
top_padding = (desired_size[1] - new_height) // 2 |
|
bottom_padding = desired_size[1] - new_height - top_padding |
|
left_padding = (desired_size[0] - new_width) // 2 |
|
right_padding = desired_size[0] - new_width - left_padding |
|
return cv2.copyMakeBorder( |
|
resized_img, |
|
top_padding, |
|
bottom_padding, |
|
left_padding, |
|
right_padding, |
|
cv2.BORDER_CONSTANT, |
|
value=color, |
|
) |
|
|
|
|
|
def downscale_image_keeping_aspect_ratio( |
|
image: np.ndarray, |
|
desired_size: Tuple[int, int], |
|
) -> np.ndarray: |
|
if image.shape[0] <= desired_size[1] and image.shape[1] <= desired_size[0]: |
|
return image |
|
return resize_image_keeping_aspect_ratio(image=image, desired_size=desired_size) |
|
|
|
|
|
def resize_image_keeping_aspect_ratio( |
|
image: np.ndarray, |
|
desired_size: Tuple[int, int], |
|
) -> np.ndarray: |
|
""" |
|
Resize reserving its aspect ratio. |
|
|
|
Parameters: |
|
- image: numpy array representing the image. |
|
- desired_size: tuple (width, height) representing the target dimensions. |
|
""" |
|
img_ratio = image.shape[1] / image.shape[0] |
|
desired_ratio = desired_size[0] / desired_size[1] |
|
|
|
|
|
if img_ratio >= desired_ratio: |
|
|
|
new_width = desired_size[0] |
|
new_height = int(desired_size[0] / img_ratio) |
|
else: |
|
|
|
new_height = desired_size[1] |
|
new_width = int(desired_size[1] * img_ratio) |
|
|
|
|
|
return cv2.resize(image, (new_width, new_height)) |
|
|