|
|
@@ -0,0 +1,410 @@ |
|
|
|
# OBSS SAHI Tool |
|
|
|
# Code written by Fatih C Akyon, 2020. |
|
|
|
|
|
|
|
import time |
|
|
|
from typing import Dict, List, Optional, Union, Tuple |
|
|
|
import numpy as np |
|
|
|
import requests |
|
|
|
from PIL import Image |
|
|
|
from numpy import ndarray |
|
|
|
|
|
|
|
|
|
|
|
def read_image_as_pil(image: Union[Image.Image, str, np.ndarray]): |
|
|
|
""" |
|
|
|
Loads an image as PIL.Image.Image. |
|
|
|
|
|
|
|
Args: |
|
|
|
image : Can be image path or url (str), numpy image (np.ndarray) or PIL.Image |
|
|
|
""" |
|
|
|
# https://stackoverflow.com/questions/56174099/how-to-load-images-larger-than-max-image-pixels-with-pil |
|
|
|
Image.MAX_IMAGE_PIXELS = None |
|
|
|
|
|
|
|
if isinstance(image, Image.Image): |
|
|
|
image_pil = image |
|
|
|
elif isinstance(image, str): |
|
|
|
# read image if str image path is provided |
|
|
|
try: |
|
|
|
image_pil = Image.open( |
|
|
|
requests.get(image, stream=True).raw if str(image).startswith("http") else image |
|
|
|
).convert("RGB") |
|
|
|
except: # handle large/tiff image reading |
|
|
|
try: |
|
|
|
import skimage.io |
|
|
|
except ImportError: |
|
|
|
raise ImportError("Please run 'pip install -U scikit-image imagecodecs' for large image handling.") |
|
|
|
image_sk = skimage.io.imread(image).astype(np.uint8) |
|
|
|
if len(image_sk.shape) == 2: # b&w |
|
|
|
image_pil = Image.fromarray(image_sk, mode="1") |
|
|
|
elif image_sk.shape[2] == 4: # rgba |
|
|
|
image_pil = Image.fromarray(image_sk, mode="RGBA") |
|
|
|
elif image_sk.shape[2] == 3: # rgb |
|
|
|
image_pil = Image.fromarray(image_sk, mode="RGB") |
|
|
|
else: |
|
|
|
raise TypeError(f"image with shape: {image_sk.shape[3]} is not supported.") |
|
|
|
elif isinstance(image, np.ndarray): |
|
|
|
if image.shape[0] < 5: # image in CHW |
|
|
|
image = image[:, :, ::-1] |
|
|
|
image_pil = Image.fromarray(image) |
|
|
|
else: |
|
|
|
raise TypeError("read image with 'pillow' using 'Image.open()'") |
|
|
|
return image_pil |
|
|
|
|
|
|
|
def get_slice_bboxes( |
|
|
|
image_height: int, |
|
|
|
image_width: int, |
|
|
|
slice_height: int = None, |
|
|
|
slice_width: int = None, |
|
|
|
auto_slice_resolution: bool = True, |
|
|
|
overlap_height_ratio: float = 0.2, |
|
|
|
overlap_width_ratio: float = 0.2, |
|
|
|
) -> List[List[int]]: |
|
|
|
"""Slices `image_pil` in crops. |
|
|
|
Corner values of each slice will be generated using the `slice_height`, |
|
|
|
`slice_width`, `overlap_height_ratio` and `overlap_width_ratio` arguments. |
|
|
|
|
|
|
|
Args: |
|
|
|
image_height (int): Height of the original image. |
|
|
|
image_width (int): Width of the original image. |
|
|
|
slice_height (int): Height of each slice. Default 512. |
|
|
|
slice_width (int): Width of each slice. Default 512. |
|
|
|
overlap_height_ratio(float): Fractional overlap in height of each |
|
|
|
slice (e.g. an overlap of 0.2 for a slice of size 100 yields an |
|
|
|
overlap of 20 pixels). Default 0.2. |
|
|
|
overlap_width_ratio(float): Fractional overlap in width of each |
|
|
|
slice (e.g. an overlap of 0.2 for a slice of size 100 yields an |
|
|
|
overlap of 20 pixels). Default 0.2. |
|
|
|
auto_slice_resolution (bool): if not set slice parameters such as slice_height and slice_width, |
|
|
|
it enables automatically calculate these params from image resolution and orientation. |
|
|
|
|
|
|
|
Returns: |
|
|
|
List[List[int]]: List of 4 corner coordinates for each N slices. |
|
|
|
[ |
|
|
|
[slice_0_left, slice_0_top, slice_0_right, slice_0_bottom], |
|
|
|
... |
|
|
|
[slice_N_left, slice_N_top, slice_N_right, slice_N_bottom] |
|
|
|
] |
|
|
|
""" |
|
|
|
slice_bboxes = [] |
|
|
|
y_max = y_min = 0 |
|
|
|
|
|
|
|
if slice_height and slice_width: |
|
|
|
y_overlap = int(overlap_height_ratio * slice_height) |
|
|
|
x_overlap = int(overlap_width_ratio * slice_width) |
|
|
|
elif auto_slice_resolution: |
|
|
|
x_overlap, y_overlap, slice_width, slice_height = get_auto_slice_params(height=image_height, width=image_width) |
|
|
|
else: |
|
|
|
raise ValueError("Compute type is not auto and slice width and height are not provided.") |
|
|
|
|
|
|
|
while y_max < image_height: |
|
|
|
x_min = x_max = 0 |
|
|
|
y_max = y_min + slice_height |
|
|
|
while x_max < image_width: |
|
|
|
x_max = x_min + slice_width |
|
|
|
if y_max > image_height or x_max > image_width: |
|
|
|
xmax = min(image_width, x_max) |
|
|
|
ymax = min(image_height, y_max) |
|
|
|
xmin = max(0, xmax - slice_width) |
|
|
|
ymin = max(0, ymax - slice_height) |
|
|
|
slice_bboxes.append([xmin, ymin, xmax, ymax]) |
|
|
|
else: |
|
|
|
slice_bboxes.append([x_min, y_min, x_max, y_max]) |
|
|
|
x_min = x_max - x_overlap |
|
|
|
y_min = y_max - y_overlap |
|
|
|
return slice_bboxes |
|
|
|
|
|
|
|
|
|
|
|
class SlicedImage: |
|
|
|
def __init__(self, image, starting_pixel): |
|
|
|
""" |
|
|
|
image: np.array |
|
|
|
Sliced image. |
|
|
|
starting_pixel: list of list of int |
|
|
|
Starting pixel coordinates of the sliced image. |
|
|
|
""" |
|
|
|
self.image = image |
|
|
|
self.starting_pixel = starting_pixel |
|
|
|
|
|
|
|
|
|
|
|
class SliceImageResult: |
|
|
|
def __init__(self, original_image_size=None): |
|
|
|
""" |
|
|
|
sliced_image_list: list of SlicedImage |
|
|
|
image_dir: str |
|
|
|
Directory of the sliced image exports. |
|
|
|
original_image_size: list of int |
|
|
|
Size of the unsliced original image in [height, width] |
|
|
|
""" |
|
|
|
self._sliced_image_list: List[SlicedImage] = [] |
|
|
|
self.original_image_height = original_image_size[0] |
|
|
|
self.original_image_width = original_image_size[1] |
|
|
|
|
|
|
|
def add_sliced_image(self, sliced_image: SlicedImage): |
|
|
|
if not isinstance(sliced_image, SlicedImage): |
|
|
|
raise TypeError("sliced_image must be a SlicedImage instance") |
|
|
|
|
|
|
|
self._sliced_image_list.append(sliced_image) |
|
|
|
|
|
|
|
@property |
|
|
|
def sliced_image_list(self): |
|
|
|
return self._sliced_image_list |
|
|
|
|
|
|
|
@property |
|
|
|
def images(self): |
|
|
|
"""Returns sliced images. |
|
|
|
|
|
|
|
Returns: |
|
|
|
images: a list of np.array |
|
|
|
""" |
|
|
|
images = [] |
|
|
|
for sliced_image in self._sliced_image_list: |
|
|
|
images.append(sliced_image.image) |
|
|
|
return images |
|
|
|
|
|
|
|
@property |
|
|
|
def starting_pixels(self) -> List[int]: |
|
|
|
"""Returns a list of starting pixels for each slice. |
|
|
|
|
|
|
|
Returns: |
|
|
|
starting_pixels: a list of starting pixel coords [x,y] |
|
|
|
""" |
|
|
|
starting_pixels = [] |
|
|
|
for sliced_image in self._sliced_image_list: |
|
|
|
starting_pixels.append(sliced_image.starting_pixel) |
|
|
|
return starting_pixels |
|
|
|
|
|
|
|
def __len__(self): |
|
|
|
return len(self._sliced_image_list) |
|
|
|
|
|
|
|
|
|
|
|
def slice_image( |
|
|
|
image: Union[str, Image.Image], |
|
|
|
slice_height: int = None, |
|
|
|
slice_width: int = None, |
|
|
|
overlap_height_ratio: float = None, |
|
|
|
overlap_width_ratio: float = None, |
|
|
|
auto_slice_resolution: bool = True, |
|
|
|
) -> Tuple[ndarray, ndarray]: |
|
|
|
"""Slice a large image into smaller windows. If output_file_name is given export |
|
|
|
sliced images. |
|
|
|
|
|
|
|
Args: |
|
|
|
auto_slice_resolution: |
|
|
|
image (str or PIL.Image): File path of image or Pillow Image to be sliced. |
|
|
|
coco_annotation_list (CocoAnnotation): List of CocoAnnotation objects. |
|
|
|
output_file_name (str, optional): Root name of output files (coordinates will |
|
|
|
be appended to this) |
|
|
|
output_dir (str, optional): Output directory |
|
|
|
slice_height (int): Height of each slice. Default 512. |
|
|
|
slice_width (int): Width of each slice. Default 512. |
|
|
|
overlap_height_ratio (float): Fractional overlap in height of each |
|
|
|
slice (e.g. an overlap of 0.2 for a slice of size 100 yields an |
|
|
|
overlap of 20 pixels). Default 0.2. |
|
|
|
overlap_width_ratio (float): Fractional overlap in width of each |
|
|
|
slice (e.g. an overlap of 0.2 for a slice of size 100 yields an |
|
|
|
overlap of 20 pixels). Default 0.2. |
|
|
|
min_area_ratio (float): If the cropped annotation area to original annotation |
|
|
|
ratio is smaller than this value, the annotation is filtered out. Default 0.1. |
|
|
|
out_ext (str, optional): Extension of saved images. Default is the |
|
|
|
original suffix. |
|
|
|
verbose (bool, optional): Switch to print relevant values to screen. |
|
|
|
Default 'False'. |
|
|
|
|
|
|
|
Returns: |
|
|
|
sliced_image_result: SliceImageResult: |
|
|
|
sliced_image_list: list of SlicedImage |
|
|
|
image_dir: str |
|
|
|
Directory of the sliced image exports. |
|
|
|
original_image_size: list of int |
|
|
|
Size of the unsliced original image in [height, width] |
|
|
|
num_total_invalid_segmentation: int |
|
|
|
Number of invalid segmentation annotations. |
|
|
|
""" |
|
|
|
|
|
|
|
# read image |
|
|
|
image_pil = read_image_as_pil(image) |
|
|
|
|
|
|
|
image_width, image_height = image_pil.size |
|
|
|
if not (image_width != 0 and image_height != 0): |
|
|
|
raise RuntimeError(f"invalid image size: {image_pil.size} for 'slice_image'.") |
|
|
|
slice_bboxes = get_slice_bboxes( |
|
|
|
image_height=image_height, |
|
|
|
image_width=image_width, |
|
|
|
auto_slice_resolution=auto_slice_resolution, |
|
|
|
slice_height=slice_height, |
|
|
|
slice_width=slice_width, |
|
|
|
overlap_height_ratio=overlap_height_ratio, |
|
|
|
overlap_width_ratio=overlap_width_ratio, |
|
|
|
) |
|
|
|
|
|
|
|
t0 = time.time() |
|
|
|
n_ims = 0 |
|
|
|
|
|
|
|
# init images and annotations lists |
|
|
|
sliced_image_result = SliceImageResult(original_image_size=[image_height, image_width]) |
|
|
|
|
|
|
|
image_pil_arr = np.asarray(image_pil) |
|
|
|
# iterate over slices |
|
|
|
for slice_bbox in slice_bboxes: |
|
|
|
n_ims += 1 |
|
|
|
|
|
|
|
# extract image |
|
|
|
tlx = slice_bbox[0] |
|
|
|
tly = slice_bbox[1] |
|
|
|
brx = slice_bbox[2] |
|
|
|
bry = slice_bbox[3] |
|
|
|
image_pil_slice = image_pil_arr[tly:bry, tlx:brx] |
|
|
|
|
|
|
|
# create sliced image and append to sliced_image_result |
|
|
|
sliced_image = SlicedImage( |
|
|
|
image=image_pil_slice, starting_pixel=[slice_bbox[0], slice_bbox[1]] |
|
|
|
) |
|
|
|
sliced_image_result.add_sliced_image(sliced_image) |
|
|
|
|
|
|
|
image_numpy = np.array(sliced_image_result.images) |
|
|
|
shift_amount = np.array(sliced_image_result.starting_pixels) |
|
|
|
|
|
|
|
return image_numpy, shift_amount |
|
|
|
|
|
|
|
|
|
|
|
def calc_ratio_and_slice(orientation, slide=1, ratio=0.1): |
|
|
|
""" |
|
|
|
According to image resolution calculation overlap params |
|
|
|
Args: |
|
|
|
orientation: image capture angle |
|
|
|
slide: sliding window |
|
|
|
ratio: buffer value |
|
|
|
|
|
|
|
Returns: |
|
|
|
overlap params |
|
|
|
""" |
|
|
|
if orientation == "vertical": |
|
|
|
slice_row, slice_col, overlap_height_ratio, overlap_width_ratio = slide, slide * 2, ratio, ratio |
|
|
|
elif orientation == "horizontal": |
|
|
|
slice_row, slice_col, overlap_height_ratio, overlap_width_ratio = slide * 2, slide, ratio, ratio |
|
|
|
elif orientation == "square": |
|
|
|
slice_row, slice_col, overlap_height_ratio, overlap_width_ratio = slide, slide, ratio, ratio |
|
|
|
|
|
|
|
return slice_row, slice_col, overlap_height_ratio, overlap_width_ratio # noqa |
|
|
|
|
|
|
|
|
|
|
|
def calc_resolution_factor(resolution: int) -> int: |
|
|
|
""" |
|
|
|
According to image resolution calculate power(2,n) and return the closest smaller `n`. |
|
|
|
Args: |
|
|
|
resolution: the width and height of the image multiplied. such as 1024x720 = 737280 |
|
|
|
|
|
|
|
Returns: |
|
|
|
|
|
|
|
""" |
|
|
|
expo = 0 |
|
|
|
while np.power(2, expo) < resolution: |
|
|
|
expo += 1 |
|
|
|
|
|
|
|
return expo - 1 |
|
|
|
|
|
|
|
|
|
|
|
def calc_aspect_ratio_orientation(width: int, height: int) -> str: |
|
|
|
""" |
|
|
|
|
|
|
|
Args: |
|
|
|
width: |
|
|
|
height: |
|
|
|
|
|
|
|
Returns: |
|
|
|
image capture orientation |
|
|
|
""" |
|
|
|
|
|
|
|
if width < height: |
|
|
|
return "vertical" |
|
|
|
elif width > height: |
|
|
|
return "horizontal" |
|
|
|
else: |
|
|
|
return "square" |
|
|
|
|
|
|
|
|
|
|
|
def calc_slice_and_overlap_params(resolution: str, height: int, width: int, orientation: str) -> List: |
|
|
|
""" |
|
|
|
This function calculate according to image resolution slice and overlap params. |
|
|
|
Args: |
|
|
|
resolution: str |
|
|
|
height: int |
|
|
|
width: int |
|
|
|
orientation: str |
|
|
|
|
|
|
|
Returns: |
|
|
|
x_overlap, y_overlap, slice_width, slice_height |
|
|
|
""" |
|
|
|
|
|
|
|
if resolution == "medium": |
|
|
|
split_row, split_col, overlap_height_ratio, overlap_width_ratio = calc_ratio_and_slice( |
|
|
|
orientation, slide=1, ratio=0.8 |
|
|
|
) |
|
|
|
|
|
|
|
elif resolution == "high": |
|
|
|
split_row, split_col, overlap_height_ratio, overlap_width_ratio = calc_ratio_and_slice( |
|
|
|
orientation, slide=2, ratio=0.4 |
|
|
|
) |
|
|
|
|
|
|
|
elif resolution == "ultra-high": |
|
|
|
split_row, split_col, overlap_height_ratio, overlap_width_ratio = calc_ratio_and_slice( |
|
|
|
orientation, slide=4, ratio=0.4 |
|
|
|
) |
|
|
|
else: # low condition |
|
|
|
split_col = 1 |
|
|
|
split_row = 1 |
|
|
|
overlap_width_ratio = 1 |
|
|
|
overlap_height_ratio = 1 |
|
|
|
|
|
|
|
slice_height = height // split_col |
|
|
|
slice_width = width // split_row |
|
|
|
|
|
|
|
x_overlap = int(slice_width * overlap_width_ratio) |
|
|
|
y_overlap = int(slice_height * overlap_height_ratio) |
|
|
|
|
|
|
|
return x_overlap, y_overlap, slice_width, slice_height # noqa |
|
|
|
|
|
|
|
|
|
|
|
def get_resolution_selector(res: str, height: int, width: int): |
|
|
|
""" |
|
|
|
|
|
|
|
Args: |
|
|
|
res: resolution of image such as low, medium |
|
|
|
height: |
|
|
|
width: |
|
|
|
|
|
|
|
Returns: |
|
|
|
trigger slicing params function and return overlap params |
|
|
|
""" |
|
|
|
orientation = calc_aspect_ratio_orientation(width=width, height=height) |
|
|
|
x_overlap, y_overlap, slice_width, slice_height = calc_slice_and_overlap_params( |
|
|
|
resolution=res, height=height, width=width, orientation=orientation |
|
|
|
) |
|
|
|
|
|
|
|
return x_overlap, y_overlap, slice_width, slice_height |
|
|
|
|
|
|
|
|
|
|
|
def get_auto_slice_params(height: int, width: int): |
|
|
|
""" |
|
|
|
According to Image HxW calculate overlap sliding window and buffer params |
|
|
|
factor is the power value of 2 closest to the image resolution. |
|
|
|
factor <= 18: low resolution image such as 300x300, 640x640 |
|
|
|
18 < factor <= 21: medium resolution image such as 1024x1024, 1336x960 |
|
|
|
21 < factor <= 24: high resolution image such as 2048x2048, 2048x4096, 4096x4096 |
|
|
|
factor > 24: ultra-high resolution image such as 6380x6380, 4096x8192 |
|
|
|
Args: |
|
|
|
height: |
|
|
|
width: |
|
|
|
|
|
|
|
Returns: |
|
|
|
slicing overlap params x_overlap, y_overlap, slice_width, slice_height |
|
|
|
""" |
|
|
|
resolution = height * width |
|
|
|
factor = calc_resolution_factor(resolution) |
|
|
|
if factor <= 18: |
|
|
|
return get_resolution_selector("low", height=height, width=width) |
|
|
|
elif 18 <= factor < 21: |
|
|
|
return get_resolution_selector("medium", height=height, width=width) |
|
|
|
elif 21 <= factor < 24: |
|
|
|
return get_resolution_selector("high", height=height, width=width) |
|
|
|
else: |
|
|
|
return get_resolution_selector("ultra-high", height=height, width=width) |