Source code for isimple.maths.images

import numpy as np
import cv2

from typing import Tuple, Union

from isimple.maths.coordinates import Coo, ShapeCoo
from isimple.util import timed


[docs]def ckernel(size: int) -> np.ndarray: """Circular filter kernel """ 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 = nz[0].min() # todo: document, it's confusing! row_1 = nz[0].max()+1 col_0 = nz[1].min() col_1 = 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 `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): cropped_image = image[rect[0]:rect[1], rect[2]:rect[3]].copy() return cv2.bitwise_and(cropped_image, mask)
[docs]def area_pixelsum(image): """Calculate area in px^2 by summing pixels :param image: Binary input image (numpy array). Should already be masked and filtered. :return: Area as # of pixels """ if image is not None: return np.sum(image > 1)
[docs]def to_mask(image: np.ndarray, kernel: np.ndarray = ckernel(7)) -> np.ndarray: """Convert a .png image to a binary mask """ # 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.float ) ) ), dtype=np.uint8 ) else: return image