"""Some basic tools for working with images.
"""
import numpy as np
import cv2
from typing import Tuple, Optional
from shapeflow.maths.coordinates import ShapeCoo
[docs]def ckernel(size: int) -> np.ndarray:
"""Circular/disk kernel.
Parameters
----------
size: int
The size or diameter of the kernel. Should be odd; if an even value is
supplied, it will be decremented.
Returns
-------
np.ndarray
A disk kernel as a ``numpy`` array
"""
if not size % 2:
# size must be odd
size = size - 1
index = int(size / 2)
y, x = np.ogrid[-index:size - index, -index:size - index] # todo: what's this?
r = int(size / 2)
mask = x * x + y * y <= r * r # disc formula
array = np.zeros([size, size], dtype=np.uint8)
array[mask] = 255
return array
[docs]def overlay(frame: np.ndarray, overlay: np.ndarray, alpha: float = 0.5) -> np.ndarray:
"""Overlay `frame` image with `overlay` image.
Both images should be in the BGR color space.
"""
# https://stackoverflow.com/questions/54249728/
return cv2.addWeighted(
overlay, alpha,
frame, 1 - alpha,
gamma=0, dst=frame
)
[docs]def crop_mask(mask: np.ndarray) -> Tuple[np.ndarray, np.ndarray, Tuple[int, int]]:
"""Crop a binary mask image array to its minimal (rectangular) size
to exclude unnecessary regions.
"""
nz = np.nonzero(mask)
row_0 = int(nz[0].min()) # todo: document, it's confusing!
row_1 = int(nz[0].max()+1)
col_0 = int(nz[1].min())
col_1 = int(nz[1].max()+1)
cropped_mask = mask[row_0:row_1, col_0:col_1].copy()
return cropped_mask, \
np.array([row_0, row_1, col_0, col_1]), \
(int((row_0+row_1-1)/2), int((col_0+col_1-1)/2))
[docs]def rect_contains(rect: np.ndarray, point: ShapeCoo) -> bool:
"""Check whether a point is contained in a rectangle.
Parameters
----------
rect: np.ndarray
An 'array rectangle': [first_row, last_row, first_column, last_column]
point: ShapeCoo
A point on a 2D image with known size
Returns
-------
bool
Whether the point is contained in the rectangle or not.
"""
"""Check whether `point` is in `rect`
:param rect: an 'array rectangle': [first_row, last_row, first_column, last_column]
:param point: a coordinate as (row, column)
:return:
"""
return (rect[0] <= point.abs[0] <= rect[1]) \
and (rect[2] <= point.abs[1] <= rect[3])
[docs]def mask(image: np.ndarray, mask: np.ndarray, rect: np.ndarray) -> np.ndarray:
"""Mask and crop off a part of an image.
Parameters
----------
image: np.ndarray
The original image
mask: np.ndarray
The mask. Should be an ``OpenCV``-compatible
binary image, with ``False -> 0`` and ``True -> 255``
rect: np.ndarray
An 'array rectangle': [first_row, last_row, first_column, last_column].
Should correspond to the position of the mask in the original image.
Returns
-------
np.ndarray
The image cropped by ``rect`` and masked by ``mask``
"""
cropped_image = image[rect[0]:rect[1], rect[2]:rect[3]].copy()
return cv2.bitwise_and(cropped_image, mask)
[docs]def area_pixelsum(image: Optional[np.ndarray]) -> Optional[int]:
"""Get the total number of ``True`` pixels in a binary image.
Parameters
----------
image: np.ndarray
An ``OpenCV``-compatible binary image, with
``False -> 0`` and ``True -> 255``.
Returns
-------
int
The number of ``255`` pixels in the image
"""
if image is not None:
return int(np.sum(image > 1))
else:
return None
[docs]def to_mask(image: np.ndarray, kernel: np.ndarray = None) -> np.ndarray:
"""Convert a PNG image to a binary mask array.
Parameters
----------
image: np.ndarray
A ``numpy`` array representing a full-color PNG image
without transparency
kernel: Optional[np.ndarray]
A smoothing kernel.
If set to ``None``, defaults to a
:func:`~shapeflow.maths.images.ckernel` of size 7.
Returns
-------
"""
if kernel is None:
kernel = ckernel(7)
# Convert to grayscale
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Threshold to binary
ret, image = cv2.threshold(image, 254, 255, cv2.THRESH_BINARY)
# Expand binary region to deal with
# 'under-thresholding' due to high setting (254/255)
image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
# The binary threshold does not always map the same binary value
# to the center of the mask (should be the darker tone)
# To circumvent this, we assume that the outer edge is
# not included in the mask (should be ok in normal cases)
# We want to end up with the mask as 255, the background as 0
# Apparently that's not it, do the arithmetic
# in float & convert to uint8 afterwards!
if image[0, 0] == 255: # todo: we're hardcoding pixel 0,0 as background here, this is not ideal!
# Do the arithmetic in float & convert to uint8 afterwards!
return np.array(
np.abs(
np.subtract(
255, np.array(
image, dtype=np.float32
)
)
), dtype=np.uint8
)
else:
return image