from typing import Any, List, Tuple, Union # noqa F401
from cv_depot.core.types import AnyColor # noqa F401
import copy
from lunchbox.enforce import Enforce
import cv2
import numpy as np
from cv_depot.core.channel_map import ChannelMap
from cv_depot.core.color import BasicColor, Color
from cv_depot.core.image import BitDepth, Image
import cv_depot.ops.channel as cvchan
import cv_depot.ops.draw as cvdraw
import cv_depot.ops.filter as cvfilt
# ------------------------------------------------------------------------------
[docs]
def swatch(
shape, # type: Union[Tuple[int, int, int], List[int]]
color, # type: AnyColor
fill_value=0.0, # type: float
bit_depth=BitDepth.FLOAT32 # type: BitDepth
):
# type: (...) -> Image
'''
Creates an image of the given shape and color.
Args:
shape (tuple[int]): List of 3 ints.
color (Color): Color of swatch RGB or L channels.
fill_value (float, optional): Value to fill additional channels with.
Default: 0.
bit_depth (BitDepth): Bit depth of swatch. Default: BitDepth.FLOAT32
Raises:
EnforceError: If shape is not a tuple of 3 integers.
EnforceError: If shape has any zero dimensions.
Returns:
Image: Color swatch.
'''
msg = f'Shape must be an tuple or list of 3 integers. Given shape: {shape}. '
msg += f'Given type: {type(shape)}.'
Enforce(shape.__class__.__name__, 'in', ['tuple', 'list'], message=msg)
Enforce(len(shape), '==', 3, message=msg)
for item in shape:
Enforce(item, 'instance of', int, message=msg)
msg = 'Illegal shape: Each shape dimension must be greater than 0. '
msg += f'Given shape: {shape}.'
Enforce(min(shape), '>', 0, message=msg)
# --------------------------------------------------------------------------
w, h, c = shape
if isinstance(color, str):
color = BasicColor.from_string(color)
if isinstance(color, BasicColor):
color = Color.from_basic_color(color)
color = Color.from_array(
color.to_array(), num_channels=c, fill_value=fill_value
)
output = np.ones((h, w, c), np.float32) * color.to_array()
output = Image.from_array(output).to_bit_depth(bit_depth)
return output
[docs]
def grid(image, shape, color=BasicColor.CYAN.name, thickness=1):
# type: (Image, Tuple[int, int], AnyColor, int) -> Image
'''
Draws a grid on a given image.
Args:
image (Image): Image to be drawn on.
shape (tuple[int]): Width, height tuple.
color (Color, optional): Color of grid. Default: BasicColor.CYAN.
thickness (int, optional): Thickness of grid lines in pixels.
Default: 1.
Raises:
EnforceError: If image is not an Image instance.
EnforceError: If shape is not of length 2.
EnforceError: If width or height of shape is less than 0:
EnforceError: If color is not an instance of Color or BasicColor.
EnforceError: If thickness is not greater than 0.
Returns:
Image: Image with grid.
'''
msg = 'Illegal image. {a.__class__.__name__} is not an instance of Image.'
Enforce(image, 'instance of', Image, message=msg)
msg = f'Illegal shape. Expected (w, h). Found: {shape}.'
Enforce(len(shape), '==', 2, message=msg)
msg = 'Shape width must be greater than or equal to 0. {a} < 0.'
Enforce(shape[0], '>=', 0, message=msg)
msg = 'Shape height must be greater than or equal to 0. {a} < 0.'
Enforce(shape[1], '>=', 0, message=msg)
msg = 'Color type must be in {b}. Found: {a}.'
Enforce(color.__class__.__name__, 'in', ['Color', 'BasicColor'], message=msg)
msg = 'Line thickness must be an integer. Found: {a.__class__.__name__}.'
Enforce(thickness, 'instance of', int, message=msg)
msg = 'Line thickness must be greater than 0. {a} !> 0.'
Enforce(thickness, '>', 0)
# --------------------------------------------------------------------------
w0, h0 = shape
w, h = image.width_and_height
bit_depth = image.bit_depth
img = image.to_bit_depth(BitDepth.FLOAT32).data.copy()
if isinstance(color, str):
color = BasicColor.from_string(color)
if isinstance(color, BasicColor):
color = Color.from_basic_color(color)
clr = color.to_array().tolist() # type: Any
# vertical lines
w_step = int(round(w / w0, 0))
for x in range(w_step, w, w_step):
img = cv2.line(img, (x, 0), (x, h), clr, thickness)
# horizontal lines
h_step = int(round(h / h0, 0))
for y in range(h_step, h, h_step):
img = cv2.line(img, (0, y), (w, y), clr, thickness)
output = Image.from_array(img).to_bit_depth(bit_depth)
return output
[docs]
def checkerboard(tiles_wide, tiles_high, tile_shape=(10, 10)):
# type: (int, int, Tuple[int, int]) -> Image
'''
Draws a checkerboard of given width, height and tile shape.
Args:
tiles_wide (int): Number of tiles wide checkerboard will be.
tiles_high (int): Number of tiles high checkerboard will be.
tile_shape (tuple[int], optional): Width, height tuple of tile shape.
Default: (10, 10).
Raises:
EnforceError: If tiles_wide is not greater than 0.
EnforceError: If tiles_high is not greater than 0.
EnforceError: If tile width is not greater than 0.
EnforceError: If tile height is not greater than 0.
Returns:
Image: Checkerboard image.
'''
w, h = tile_shape
shape = (w, h, 3)
msg = 'Tiles_wide must be greater than 0. {a} !> 0.'
Enforce(tiles_wide, '>', 0, message=msg)
msg = 'Tiles_high must be greater than 0. {a} !> 0.'
Enforce(tiles_high, '>', 0, message=msg)
msg = 'Tile width must be greater than 0. {a} !> 0.'
Enforce(w, '>', 0, message=msg)
msg = 'Tile height must be greater than 0. {a} !> 0.'
Enforce(h, '>', 0, message=msg)
# --------------------------------------------------------------------------
black = swatch(shape, BasicColor.BLACK).data
white = swatch(shape, BasicColor.WHITE).data
even = []
odd = []
for i in range(0, tiles_wide):
if i == 0 or i % 2 == 0:
even.append(black)
odd.append(white)
else:
even.append(white)
odd.append(black)
even = np.concatenate(even, axis=1)
odd = np.concatenate(odd, axis=1)
rows = []
for i in range(0, tiles_high):
if i == 0 or i % 2 == 0:
rows.append(even)
else:
rows.append(odd)
output = Image.from_array(np.concatenate(rows, axis=0))
return output
[docs]
def highlight(
image, mask='a', opacity=0.5, color=BasicColor.CYAN2.name, inverse=False
):
# type: (Image, str, float, AnyColor, bool) -> Image
'''
Highlight a masked portion of a given image according to a given channel.
Args:
image (Image): Image to be highlighted.
mask (str, optional): Channel to be used as mask. Default: alpha.
opacity (float, optional): Opacity of highlight overlayed on image.
Default: 0.5
color (Color or BasicColor, optional): Color of highlight.
Default: BasicColor.CYAN2.
inverse (bool, optional): Whether to invert the highlight.
Default: False.
Raises:
EnforceError: If image is not an instance of Image.
EnforceError: If mask is not an instance of str.
EnforceError: If mask not found in image channels.
EnforceError: If opacity is < 0 or > 1.
EnforceError: If color is not instance of Color.
EnforceError: If inverse is not a boolean.
Returns:
Image: Highlighted image.
'''
if isinstance(color, str):
color = BasicColor.from_string(color)
if isinstance(color, BasicColor):
color = Color.from_basic_color(color)
Enforce(image, 'instance of', Image)
Enforce(mask, 'instance of', str)
msg = 'Mask channel: {a} not found in image channels: {b}.'
Enforce(mask, 'in', image.channels, message=msg)
Enforce(opacity, '>=', 0)
Enforce(opacity, '<=', 1)
Enforce(color, 'instance of', Color)
Enforce(inverse, 'instance of', bool)
# --------------------------------------------------------------------------
img = image.to_bit_depth(BitDepth.FLOAT32)
channels = copy.deepcopy(image.channels)
matte = cvchan.remap_single_channel(img[:, :, mask], channels)
imatte = cvchan.invert(matte)
if inverse:
matte, imatte = imatte, matte
swatch = cvdraw.swatch(image.shape, color, fill_value=1.0)
data = (image.data * imatte.data) + (swatch.data * matte.data)
output = Image.from_array(data).to_bit_depth(image.bit_depth)
output = cvchan \
.mix(image, output, amount=1 - opacity) \
.set_channels(image.channels)
return output
[docs]
def outline(image, mask='a', width=10, color=BasicColor.CYAN2.name):
# type (Image, str, int, AnyColor) -> Image
'''
Use a given mask to outline a given image.
Args:
image (Image): Image with mask channel.
mask (str, optional): Mask channel. Default: alpha.
width (int, optional): Outline width. Default: 10.
color (Color or BasicColor, optional): Color of outline.
Default: BasicColor.CYAN2
Raises:
EnforceError: If image is not an instance of Image.
EnforceError: If channel not found in image channels.
EnforceError: If width is not >= 0.
EnforceError: If color is not an instance of Color or BasicColor.
Returns:
Image: Image with outline.
'''
Enforce(image, 'instance of', Image)
msg = 'Mask channel: {a} not found in image channels: {b}.'
Enforce(mask, 'in', image.channels, message=msg)
Enforce(width, '>=', 0)
# --------------------------------------------------------------------------
cmap = ChannelMap({c: f'0.{c}' for c in image.channels})
cmap[mask] = '1.l'
w = int(round(width / 2, 0))
edge = cvfilt.canny_edges(image[:, :, mask], size=w)
output = cvchan.remap([image, edge], cmap)
output = highlight(output, mask=mask, opacity=1.0, color=color)
return output
[docs]
def annotate(
image, # type: Image
mask='a', # type: str
opacity=0.5, # type: float
width=10, # type: int
color=BasicColor.CYAN2.name, # type: AnyColor
inverse=False, # type: bool
keep_mask=False, # type: bool
):
# type (...) -> Image
'''
Annotate a given image according to a given mask channel.
Args:
image (Image): Image with mask channel.
mask (str, optional): Mask channel. Default: alpha.
opacity (float, optional): Opacity of annotation. Default: 0.5
width (int, optional): Outline width. Default: 10.
color (Color or BasicColor, optional): Color of outline.
Default: BasicColor.CYAN2
inverse (bool, optional): Whether to invert the annotation.
Default: False.
keep_mask (bool, optional): Whether to keep the mask channel.
Default: False.
Returns:
Image: Image with outline.
'''
output = highlight(
image, mask=mask, opacity=opacity, color=color, inverse=inverse
)
output = outline(output, mask=mask, width=width, color=color)
if not keep_mask:
channels = copy.deepcopy(image.channels)
channels.remove(mask)
output = output[:, :, channels]
return output