# coding: utf-8
import base64
import io
import os.path
from enum import Enum
from typing import List, Optional, Tuple, Union
import cv2
import nrrd
import numpy as np
from PIL import Image as PILImage
from PIL import ImageDraw, ImageFile, ImageFont
from pkg_resources import parse_version
from supervisely._utils import abs_url, get_bytes_hash, is_development, rand_str
from supervisely.geometry.image_rotator import ImageRotator
from supervisely.geometry.rectangle import Rectangle
from supervisely.imaging.font import get_font
from supervisely.io.fs import ensure_base_path, get_file_ext, silent_remove
ImageFile.LOAD_TRUNCATED_IMAGES = True
# @TODO: refactoring image->img
KEEP_ASPECT_RATIO = -1 # TODO: need move it to best place
# Do NOT use directly for image extension validation. Use is_valid_ext() / has_valid_ext() below instead.
SUPPORTED_IMG_EXTS = [
".jpg",
".jpeg",
".mpo",
".bmp",
".png",
".webp",
".tiff",
".tif",
".nrrd",
".jfif",
]
DEFAULT_IMG_EXT = ".png"
class CornerAnchorMode:
""" """
TOP_LEFT = "tl"
TOP_RIGHT = "tr"
BOTTOM_LEFT = "bl"
BOTTOM_RIGHT = "br"
class RotateMode(Enum):
""" """
KEEP_BLACK = 0
""""""
CROP_BLACK = 1
""""""
SAVE_ORIGINAL_SIZE = 2
""""""
class ImageExtensionError(Exception):
""" """
pass
class UnsupportedImageFormat(Exception):
""" """
pass
class ImageReadException(Exception):
""" """
pass
[docs]def is_valid_ext(ext: str) -> bool:
"""
Checks file extension for list of supported images extensions('.jpg', '.jpeg', '.mpo', '.bmp', '.png', '.webp', '.tiff', '.tif', '.nrrd').
:param ext: Image extention.
:type ext: str
:return: True if image extention in list of supported images extensions, False - in otherwise
:rtype: :class:`bool`
:Usage example:
.. code-block:: python
import supervisely as sly
sly.image.is_valid_ext('.png') # True
sly.image.is_valid_ext('.py') # False
"""
return ext.lower() in SUPPORTED_IMG_EXTS
[docs]def has_valid_ext(path: str) -> bool:
"""
Checks if a given file has a supported extension('.jpg', '.jpeg', '.mpo', '.bmp', '.png', '.webp', '.tiff', '.tif', '.nrrd').
:param path: Path to file.
:type path: str
:return: True if file extention in list of supported images extensions, False - in otherwise
:rtype: :class:`bool`
:Usage example:
.. code-block:: python
import supervisely as sly
sly.image.has_valid_ext('/home/admin/work/docs/new_image.jpeg') # True
sly.image.has_valid_ext('/home/admin/work/docs/016_img.py') # False
"""
return is_valid_ext(os.path.splitext(path)[1])
[docs]def validate_ext(path: str) -> None:
"""
Generate exception error if file extention is not in list of supported images extensions('.jpg', '.jpeg', '.mpo', '.bmp', '.png', '.webp', '.tiff', '.tif', '.nrrd').
:param path: Path to file.
:type path: str
:return: None
:rtype: :class:`NoneType`
:Usage example:
.. code-block:: python
import supervisely as sly
print(sly.image.validate_ext('/home/admin/work/docs/new_image.jpeg'))
# Output: None
try:
print(sly.image.validate_ext('/home/admin/work/docs/016_img.py'))
except ImageExtensionError as error:
print(error)
# Output: Unsupported image extension: '.py' for file '/home/admin/work/docs/016_img.py'. Only the following extensions are supported: .jpg, .jpeg, .mpo, .bmp, .png, .webp.
"""
_, ext = os.path.splitext(path)
if not is_valid_ext(ext):
raise ImageExtensionError(
"Unsupported image extension: {!r} for file {!r}. Only the following extensions are supported: {}.".format(
ext, path, ", ".join(SUPPORTED_IMG_EXTS)
)
)
[docs]def read(path: str, remove_alpha_channel: Optional[bool] = True) -> np.ndarray:
"""
Loads an image from the specified file and returns it in RGB format.
:param path: Path to file.
:type path: str
:param remove_alpha_channel: Define remove alpha channel when reading file or not.
:type remove_alpha_channel: bool, optional
:return: Numpy array
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
im = sly.image.read('/home/admin/work/docs/image.jpeg')
"""
ext = get_file_ext(path)
if ext == ".nrrd":
data, header = nrrd.read(path, index_order="C")
return data
validate_format(path)
if remove_alpha_channel is True:
img = cv2.imread(path, cv2.IMREAD_COLOR)
if img is None:
raise IOError("OpenCV can not open the file {!r}".format(path))
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
else:
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if img is None:
raise IOError("OpenCV can not open the file {!r}".format(path))
if len(img.shape) == 2:
return cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
cnt_channels = img.shape[2]
if cnt_channels == 4:
return cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
elif cnt_channels == 3:
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
elif cnt_channels == 1:
return cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
else:
raise ValueError(
"image has {} channels. Please, contact support...".format(cnt_channels)
)
[docs]def read_bytes(image_bytes: str, keep_alpha: Optional[bool] = False) -> np.ndarray:
"""
Loads an byte image and returns it in RGB format.
:param image_bytes: Path to file.
:type image_bytes: str
:param keep_alpha: Define consider alpha channel when reading bytes or not.
:type keep_alpha: bool, optional
:return: Numpy array
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
im_bytes = '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\...\xd9'
im = sly.image.read_bytes(im_bytes)
"""
if image_bytes.startswith(b"NRRD"):
file_like = io.BytesIO(image_bytes)
header = nrrd.read_header(file_like)
data = nrrd.read_data(header, file_like, index_order="C")
return data
image_np_arr = np.asarray(bytearray(image_bytes), dtype="uint8")
if keep_alpha is True:
img = cv2.imdecode(image_np_arr, cv2.IMREAD_UNCHANGED)
if len(img.shape) == 2:
img = np.expand_dims(img, 2)
cnt_channels = img.shape[2]
if cnt_channels == 4:
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
elif cnt_channels == 3:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
elif cnt_channels == 1:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
return img
else:
img = cv2.imdecode(image_np_arr, cv2.IMREAD_COLOR) # cv2.imdecode returns BGR always
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
[docs]def write(path: str, img: np.ndarray, remove_alpha_channel: Optional[bool] = True) -> None:
"""
Saves the image to the specified file. It create directory from path if the directory for this path does not exist.
:param path: Path to file.
:type path: str
:param img: Image in numpy array(RGB format).
:type img: np.ndarray
:param remove_alpha_channel: Define remove alpha channel when writing file or not.
:type remove_alpha_channel: bool, optional
:return: None
:rtype: :class:`NoneType`
:Usage example:
.. code-block:: python
import supervisely as sly
path = '/home/admin/work/docs/new_image.jpeg'
sly.image.write(path, image_np)
"""
ensure_base_path(path)
validate_ext(path)
ext = get_file_ext(path)
if ext == ".nrrd":
return nrrd.write(path, img, index_order="C")
res_img = img.copy()
if len(img.shape) == 2:
res_img = np.expand_dims(img, 2)
cnt_channels = res_img.shape[2]
if cnt_channels == 4:
if remove_alpha_channel is True:
res_img = cv2.cvtColor(res_img.astype(np.uint8), cv2.COLOR_RGBA2BGR)
else:
res_img = cv2.cvtColor(res_img.astype(np.uint8), cv2.COLOR_RGBA2BGRA)
elif cnt_channels == 3:
res_img = cv2.cvtColor(res_img.astype(np.uint8), cv2.COLOR_RGB2BGR)
elif cnt_channels == 1:
res_img = cv2.cvtColor(res_img.astype(np.uint8), cv2.COLOR_GRAY2BGR)
return cv2.imwrite(path, res_img)
[docs]def draw_text_sequence(
bitmap: np.ndarray,
texts: List[str],
anchor_point: Tuple[int, int],
corner_snap: Optional[CornerAnchorMode] = CornerAnchorMode.TOP_LEFT,
col_space: Optional[int] = 12,
font: Optional[ImageFont.FreeTypeFont] = None,
fill_background: Optional[bool] = True,
) -> None:
"""
Draws text labels on bitmap from left to right with col_space spacing between labels.
:param bitmap: Image to draw texts in numpy format.
:type bitmap: np.ndarray
:param texts: List of texts to draw on image.
:type texts: List[str]
:param anchor_point: Coordinates of the place on the image where the text will be displayed(row, column).
:type anchor_point: Tuple[int, int]
:param corner_snap: Corner of image to draw texts.
:type corner_snap: CornerAnchorMode, optional
:param col_space: Distance between texts.
:type col_space: int, optional
:param font: Type of text font.
:type font: ImageFont.FreeTypeFont, optional
:param fill_background: Define fill text background or not.
:type fill_background: bool, optional
:return: None
:rtype: :class:`NoneType`
:Usage example:
.. code-block:: python
import supervisely as sly
sly.image.draw_text_sequence(image, ['some_text', 'another_text'], (10, 10))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/wIzrDuf.jpg
After
"""
col_offset = 0
canvas = PILImage.new("RGBA", bitmap.shape[:2][::-1], (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas, "RGBA")
_, max_height = draw.textsize("".join(texts), font=font)
_, text_height = draw.textsize(texts[0], font=font)
middle = text_height // 2
for text in texts:
_, h = draw.textsize(text, font=font)
row_offset = middle - h // 2
y, x = anchor_point[0] - row_offset, anchor_point[1] + col_offset
text_height, text_width = draw_text(
bitmap, text, (y, x), corner_snap, font, fill_background, (0, 0, 0, 255), max_height
)
col_offset += text_width + col_space
[docs]def draw_text(
bitmap: np.ndarray,
text: str,
anchor_point: Tuple[int, int],
corner_snap: Optional[CornerAnchorMode] = CornerAnchorMode.TOP_LEFT,
font: Optional[ImageFont.FreeTypeFont] = None,
fill_background: Optional[bool] = True,
color: Optional[Union[Tuple[int, int, int, int], Tuple[int, int, int]]] = (0, 0, 0, 255),
max_text_height: Optional[int] = None,
) -> Tuple[int, int]:
"""
Draws given text on bitmap image.
:param bitmap: Image to draw texts in numpy format.
:type bitmap: np.ndarray
:param texts: Text to draw on image.
:type texts: str
:param anchor_point: Coordinates of the place on the image where the text will be displayed(row, column).
:type anchor_point: Tuple[int, int]
:param corner_snap: Corner of image to draw texts.
:type corner_snap: CornerAnchorMode, optional
:param font: Type of text font.
:type font: ImageFont.FreeTypeFont, optional
:param fill_background: Define fill text background or not.
:type fill_background: bool, optional
:param color: Text color as a tuple of three or four integers (red, green, blue, alpha)
ranging from 0 to 255.
If alpha is not provided, it defaults to 255 (fully opaque).
:type color: Union[Tuple[int, int, int, int], Tuple[int, int, int]], optional
:param max_text_height: The parameter is necessary to draw neatly the fill background of list of texts with different heights. See `self.draw_text_sequence` for details.
:type max_text_height: bool, optional
:return: Height and width of text
:rtype: :class:`Tuple[int, int]`
:Usage example:
.. code-block:: python
import supervisely as sly
sly.image.draw_text(image, 'your text', (100, 50), color=(0, 0, 0, 255))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/6Ptz8Tf.jpg
After
"""
if not (isinstance(color, (list, tuple)) and len(color) in [3, 4]):
raise TypeError("Color must be list or tuple of three or four elements")
if font is None:
font = get_font()
source_img = PILImage.fromarray(bitmap)
source_img = source_img.convert("RGBA")
canvas = PILImage.new("RGBA", source_img.size, (0, 0, 0, 0))
drawer = ImageDraw.Draw(canvas, "RGBA")
text_width, text_height = drawer.textsize(text, font=font)
rect_top, rect_left = anchor_point
if corner_snap == CornerAnchorMode.TOP_LEFT:
pass # Do nothing
elif corner_snap == CornerAnchorMode.TOP_RIGHT:
rect_left -= text_width
elif corner_snap == CornerAnchorMode.BOTTOM_LEFT:
rect_top -= text_height
elif corner_snap == CornerAnchorMode.BOTTOM_RIGHT:
rect_top -= text_height
rect_left -= text_width
if fill_background:
rect_right = rect_left + text_width
_rect_top = rect_top
_rect_bottom = _rect_top + text_height
if max_text_height is not None:
delta = (max_text_height - text_height) // 2
_rect_bottom += delta
_rect_top -= delta
drawer.rectangle(
((rect_left, _rect_top), (rect_right + 1, _rect_bottom)),
fill=(255, 255, 255, 128),
)
drawer.text((rect_left + 1, rect_top), text, fill=color, font=font)
source_img = PILImage.alpha_composite(source_img, canvas)
source_img = source_img.convert("RGB")
bitmap[:, :, :] = np.array(source_img, dtype=np.uint8)
return (text_height, text_width)
# @TODO: not working with alpha channel
[docs]def write_bytes(img: np.ndarray, ext: str) -> bytes:
"""
Compresses the image and stores it in the byte object.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param ext: File extension that defines the output format.
:type ext: str
:return: Bytes object
:rtype: :class:`bytes`
:Usage example:
.. code-block:: python
import supervisely as sly
bytes = sly.image.write_bytes(image_np, 'jpeg')
print(type(bytes))
# Output: <class 'bytes'>
"""
ext = ("." + ext).replace("..", ".")
if not is_valid_ext(ext):
raise UnsupportedImageFormat(
"Unsupported image format {!r}. Only the following formats are supported: {}".format(
ext, ", ".join(SUPPORTED_IMG_EXTS)
)
)
if ext == ".nrrd":
nrrd_bytes = None
_filename = f"./sly-nrrd-data-bytes-{rand_str(10)}{ext}"
nrrd.write(_filename, img, index_order="C")
with open(_filename, "rb") as nrrd_file:
nrrd_bytes = nrrd_file.read()
silent_remove(_filename)
return nrrd_bytes
img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_RGB2BGR)
encode_status, img_array = cv2.imencode(ext, img)
if encode_status is True:
return img_array.tobytes()
raise RuntimeError("Can not encode input image")
# @TODO: not working with alpha channel
[docs]def get_hash(img: np.ndarray, ext: str) -> str:
"""
Hash input image with sha256 algoritm and encode result by using Base64.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param ext: File extension that defines the output format.
:type ext: str
:return: Hash string
:rtype: :class:`str`
:Usage example:
.. code-block:: python
import supervisely as sly
hash = sly.image.get_hash(im, 'jpeg')
print(hash)
# Output: fTec3RD7Zxg0aYc0ooa5phPfBrzDe01urlFsgi5IzIQ=
"""
return get_bytes_hash(write_bytes(img, ext))
[docs]def crop(img: np.ndarray, rect: Rectangle) -> np.ndarray:
"""
Crop part of the image with rectangle size. If rectangle for crop is out of image area it generates ValueError.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param rect: :class:`Rectangle<supervisely.geometry.rectangle.Rectangle>` object for crop.
:type rect: Rectangle
:return: Cropped image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
# If size of rectangle is more then image shape raise ValueError:
try:
crop_image = sly.image.crop(image_np, sly.Rectangle(0, 0, 5000, 6000))
except ValueError as error:
print(error)
# Output: Rectangle for crop out of image area!
crop_im = sly.image.crop(image_np, sly.Rectangle(0, 0, 500, 600))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/4tNm2GS.jpg
After
"""
img_rect = Rectangle.from_array(img)
if not img_rect.contains(rect):
raise ValueError("Rectangle for crop out of image area!")
return rect.get_cropped_numpy_slice(img)
[docs]def crop_with_padding(img: np.ndarray, rect: Rectangle) -> np.ndarray:
"""
Crop part of the image with rectangle size. If rectangle for crop is out of image area it generates additional padding.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param rect: :class:`Rectangle<supervisely.geometry.rectangle.Rectangle>` object for crop.
:type rect: Rectangle
:return: Cropped image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
crop_with_padding_image = sly.image.crop_with_padding(image_np, sly.Rectangle(0, 0, 1000, 1200))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/Nv1UinH.jpg
After
"""
img_rect = Rectangle.from_array(img)
if not img_rect.contains(rect):
row, col = img.shape[:2]
new_img = cv2.copyMakeBorder(
img,
top=rect.height,
bottom=rect.height,
left=rect.width,
right=rect.width,
borderType=cv2.BORDER_CONSTANT,
)
new_rect = rect.translate(drow=rect.height, dcol=rect.width)
return new_rect.get_cropped_numpy_slice(new_img)
else:
return rect.get_cropped_numpy_slice(img)
[docs]def restore_proportional_size(
in_size: Tuple[int, int],
out_size: Optional[Tuple[int, int]] = None,
frow: Optional[float] = None,
fcol: Optional[float] = None,
f: Optional[float] = None,
) -> Tuple[int, int]:
"""
Calculate new size of the image.
:param in_size: Size of input image (height, width).
:type in_size: Tuple[int, int]
:param out_size: New image size (height, width).
:type out_size: Tuple[int, int], optional
:param frow: Length of output image.
:type frow: float, optional
:param fcol: Height of output image.
:type fcol: float, optional
:param f: Positive non zero scale factor.
:type f: float, optional
:return: Height and width of image
:rtype: :class:`Tuple[int, int]`
"""
if out_size is not None and (frow is not None or fcol is not None) and f is None:
raise ValueError("Must be specified output size or scale factors not both of them.")
if out_size is not None:
if out_size[0] == KEEP_ASPECT_RATIO and out_size[1] == KEEP_ASPECT_RATIO:
raise ValueError("Must be specified at least 1 dimension of size!")
if (out_size[0] <= 0 and out_size[0] != KEEP_ASPECT_RATIO) or (
out_size[1] <= 0 and out_size[1] != KEEP_ASPECT_RATIO
):
raise ValueError("Size dimensions must be greater than 0.")
result_row = (
out_size[0] if out_size[0] > 0 else max(1, round(out_size[1] / in_size[1] * in_size[0]))
)
result_col = (
out_size[1] if out_size[1] > 0 else max(1, round(out_size[0] / in_size[0] * in_size[1]))
)
else:
if f is not None:
if f < 0:
raise ValueError('"f" argument must be positive!')
frow = fcol = f
if (frow < 0 or fcol < 0) or (frow is None or fcol is None):
raise ValueError('Specify "f" argument for single scale!')
result_col = round(fcol * in_size[1])
result_row = round(frow * in_size[0])
return result_row, result_col
# @TODO: reimplement, to be more convenient
[docs]def resize(
img: np.ndarray,
out_size: Optional[Tuple[int, int]] = None,
frow: Optional[float] = None,
fcol: Optional[float] = None,
) -> np.ndarray:
"""
Resize the image to the specified size.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param out_size: New image size (height, width).
:type out_size: Tuple[int, int], optional
:param frow: Length of output image.
:type frow: float, optional
:param fcol: Height of output image.
:type fcol: float, optional
:return: Resize image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
resize_image = sly.image.resize(image_np, (300, 500))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/Xya4yz0.jpg
After
"""
result_height, result_width = restore_proportional_size(img.shape[:2], out_size, frow, fcol)
return cv2.resize(img, (result_width, result_height), interpolation=cv2.INTER_CUBIC)
[docs]def resize_inter_nearest(
img: np.ndarray,
out_size: Optional[Tuple[int, int]] = None,
frow: Optional[float] = None,
fcol: Optional[float] = None,
) -> np.ndarray:
"""
Resize image to match a certain size. Performs interpolation to up-size or down-size images.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param out_size: New image size (height, width).
:type out_size: Tuple[int, int], optional
:param frow: Length of output image.
:type frow: float, optional
:param fcol: Height of output image.
:type fcol: float, optional
:return: Resize image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
resize_image_nearest = sly.image.resize_inter_nearest(image_np, (300, 700))
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/0O6yMDH.jpg
After
"""
import skimage.transform
target_shape = restore_proportional_size(img.shape[:2], out_size, frow, fcol)
resize_kv_args = dict(order=0, preserve_range=True, mode="constant")
if parse_version(skimage.__version__) >= parse_version("0.14.0"):
resize_kv_args["anti_aliasing"] = False
return skimage.transform.resize(img, target_shape, **resize_kv_args).astype(img.dtype)
[docs]def scale(img: np.ndarray, factor: float) -> np.ndarray:
"""
Scales current image with the given factor.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param factor: Scale size.
:type factor: float
:return: Resize image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
scale_image = sly.image.scale(image_np, 0.3)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/NyP8tts.jpg
After
"""
return resize(img, (round(img.shape[0] * factor), round(img.shape[1] * factor)))
[docs]def fliplr(img: np.ndarray) -> np.ndarray:
"""
Flips the current image horizontally.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:return: Flip image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
fliplr_image = sly.image.fliplr(image_np)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/1mqnuZU.jpg
After
"""
return np.flip(img, 1)
[docs]def flipud(img: np.ndarray) -> np.ndarray:
"""
Flips the current image vertically.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:return: Flip image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
flipud_image = sly.image.flipud(image_np)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/LDwRDvm.jpg
After
"""
return np.flip(img, 0)
[docs]def rotate(
img: np.ndarray,
degrees_angle: int,
mode: Optional[RotateMode] = RotateMode.KEEP_BLACK,
) -> np.ndarray:
"""
Rotates current image.
:param img: Image in numpy format(RGB).
:type img: np.ndarray
:param degrees_angle: Angle in degrees for rotating.
:type degrees_angle: int
:param mode: One of RotateMode enum values.
:type mode: RotateMode, optional
:return: Rotate image in numpy format
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
# keep_black mode
rotate_im_keep_black = sly.image.rotate(image_np, 45)
# crop_black mode
rotate_im_crop_black = sly.image.rotate(image_np, 45, sly.image.RotateMode.CROP_BLACK)
# origin_size mode
rotate_im_origin_size = sly.image.rotate(image_np, 45, sly.image.RotateMode.SAVE_ORIGINAL_SIZE) * 255
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/VjiwV4O.jpg
After keep_black mode
- .. figure:: https://i.imgur.com/Rs34eMa.jpg
After crop_black mode
- .. figure:: https://i.imgur.com/ttDWfBE.jpg
After origin_size mode
"""
import skimage.transform
rotator = ImageRotator(imsize=img.shape[:2], angle_degrees_ccw=degrees_angle)
if mode == RotateMode.KEEP_BLACK:
return rotator.rotate_img(img, use_inter_nearest=False) # @TODO: order = ???
elif mode == RotateMode.CROP_BLACK:
img_rotated = rotator.rotate_img(img, use_inter_nearest=False)
return rotator.inner_crop.get_cropped_numpy_slice(img_rotated)
elif mode == RotateMode.SAVE_ORIGINAL_SIZE:
# TODO Implement this in rotator instead.
return skimage.transform.rotate(img, degrees_angle, resize=False)
else:
raise NotImplementedError('Rotate mode "{0}" not supported!'.format(str(mode)))
# Color augmentations
def _check_contrast_brightness_inputs(min_value, max_value):
"""
The function _check_contrast_brightness_inputs checks the input brightness or contrast for correctness.
"""
if min_value < 0:
raise ValueError("Minimum value must be greater than or equal to 0.")
if min_value > max_value:
raise ValueError("Maximum value must be greater than or equal to minimum value.")
[docs]def random_contrast(image: np.ndarray, min_factor: float, max_factor: float) -> np.ndarray:
"""
Randomly changes contrast of the input image.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param min_factor: Lower bound of contrast range.
:type min_factor: float
:param max_factor: Upper bound of contrast range.
:type max_factor: float
:return: Image in numpy format with new contrast
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
rand_contrast_im = sly.image.random_contrast(image_np, 1.1, 1.8)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/4zSNuJU.jpg
After
"""
_check_contrast_brightness_inputs(min_factor, max_factor)
contrast_value = np.random.uniform(min_factor, max_factor)
image_gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
image_mean = round(image_gray.mean())
image = image.astype(np.float32)
image = contrast_value * (image - image_mean) + image_mean
return np.clip(image, 0, 255).astype(np.uint8)
[docs]def random_brightness(image: np.ndarray, min_factor: float, max_factor: float) -> np.ndarray:
"""
Randomly changes brightness of the input image.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param min_factor: Lower bound of brightness range.
:type min_factor: float
:param max_factor: Upper bound of brightness range.
:type max_factor: float
:return: Image in numpy format with new brightness
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
rand_brightness_im = sly.image.random_brightness(image_np, 1.5, 8.5)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/bOYwwYH.jpg
After
"""
_check_contrast_brightness_inputs(min_factor, max_factor)
brightness_value = np.random.uniform(min_factor, max_factor)
image = image.astype(np.float32)
image = image * brightness_value
return np.clip(image, 0, 255).astype(np.uint8)
[docs]def random_noise(image: np.ndarray, mean: float, std: float) -> np.ndarray:
"""
Adds random Gaussian noise to the input image.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param mean: The mean value of noise distribution.
:type mean: float
:param std: The standard deviation of noise distribution.
:type std: float
:return: Image in numpy format with random noise
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
random_noise_im = sly.image.random_noise(image_np, 25, 19)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/EzyEHeM.jpg
After
"""
image = image.astype(np.float32)
image += np.random.normal(mean, std, image.shape)
return np.clip(image, 0, 255).astype(np.uint8)
[docs]def random_color_scale(image: np.ndarray, min_factor: float, max_factor: float) -> np.ndarray:
"""
Changes image colors by randomly scaling each of RGB components. The scaling factors are sampled uniformly from the given range.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param min_factor: Minimum scale factor.
:type min_factor: float
:param max_factor: Maximum scale factor.
:type max_factor: float
:return: Image in numpy format with random color scale
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
random_color_scale_im = sly.image.random_color_scale(image_np, 0.5, 0.9)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/GGUZqlA.jpg
After
"""
image_float = image.astype(np.float64)
scales = np.random.uniform(low=min_factor, high=max_factor, size=(1, 1, image.shape[2]))
res_image = image_float * scales
return np.clip(res_image, 0, 255).astype(np.uint8)
# Blurs
[docs]def blur(image: np.ndarray, kernel_size: int) -> np.ndarray:
"""
Blurs an image using the normalized box filter.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param kernel_size: Blurring kernel size.
:type kernel_size: int
:return: Image in numpy format with blur
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
blur_im = sly.image.blur(image_np, 7)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/wFnBaC6.jpg
After
"""
return cv2.blur(image, (kernel_size, kernel_size))
[docs]def gaussian_blur(image: np.ndarray, sigma_min: float, sigma_max: float) -> np.ndarray:
"""
Blurs an image using a Gaussian filter.
:param image: Image in numpy format(RGB).
:type image: np.ndarray
:param sigma_min: Lower bound of Gaussian kernel standard deviation range.
:type sigma_min: float
:param sigma_min: Upper bound of Gaussian kernel standard deviation range.
:type sigma_max: float
:return: Image in numpy format with gaussian blur
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
gaussian_blur_im = sly.image.gaussian_blur(image_np, 3.3, 7.5)
.. list-table::
* - .. figure:: https://i.imgur.com/BHUALdv.jpg
Before
- .. figure:: https://i.imgur.com/brs6Au0.jpg
After
"""
sigma_value = np.random.uniform(sigma_min, sigma_max)
return cv2.GaussianBlur(image, (0, 0), sigma_value)
[docs]def drop_image_alpha_channel(img: np.ndarray) -> np.ndarray:
"""
Converts 4-channel image to 3-channel.
:param img: Image in numpy format(RGBA).
:type img: np.ndarray
:return: Image in numpy format(RGB)
:rtype: :class:`np.ndarray`
"""
if img.shape[2] != 4:
raise ValueError(
"Only 4-channel RGBA images are supported for alpha channel removal. "
+ "Instead got {} channels.".format(img.shape[2])
)
return cv2.cvtColor(img, cv2.COLOR_RGBA2RGB)
# @TODO: refactor from two separate methods to a single one
# bgra or bgr
[docs]def np_image_to_data_url(img: np.ndarray) -> str:
"""
Convert image to url string.
:param img: Image in numpy format(RGBA or RGB).
:type img: np.ndarray
:return: String with image url
:rtype: :class:`str`
:Usage example:
.. code-block:: python
import supervisely as sly
data_url = sly.image.np_image_to_data_url(im)
print(data_url)
# Output: 'data:image/png;base64,iVBORw0K...'
"""
encode_status, bgra_result_png = cv2.imencode(".png", img)
img_png = bgra_result_png.tobytes()
img_base64 = base64.b64encode(img_png)
data_url = "data:image/png;base64,{}".format(str(img_base64, "utf-8"))
return data_url
[docs]def data_url_to_numpy(data_url: str) -> np.ndarray:
"""
Convert url string to numpy image(RGB).
:param img: String with image url.
:type img: str
:return: Image in numpy format(RGBA)
:rtype: :class:`np.ndarray`
:Usage example:
.. code-block:: python
import supervisely as sly
image_np = sly.image.data_url_to_numpy(data_url)
"""
img_base64 = data_url[len("data:image/png;base64,") :]
img_base64 = base64.b64decode(img_base64)
image = read_bytes(img_base64)
return image
# only rgb
[docs]def np_image_to_data_url_backup_rgb(img: np.ndarray) -> str:
"""
Convert image to url string.
:param img: Image in numpy format(only RGB).
:type img: np.ndarray
:return: String with image url
:rtype: :class:`str`
:Usage example:
.. code-block:: python
import supervisely as sly
data_url = sly.image.np_image_to_data_url_backup_rgb(im)
print(data_url)
# Output: 'data:image/png;base64,iVBORw0K...'
"""
img_base64 = base64.b64encode(write_bytes(img, "png"))
data_url = "data:image/png;base64,{}".format(str(img_base64, "utf-8"))
return data_url