Source code for pyautocv.segmentation

# import relevant libraries
import cv2
import matplotlib.pyplot as plt
from scipy import ndimage
from skimage.io import imread_collection, imread
from skimage.transform import resize
from skimage import filters
from os import pathsep, path
import numpy as np
from itertools import chain
import glob


[docs]def gray_images(images): """ :param images: A list of color images that should be grayed/greyed. :return: Returns grayed images. Currently supports only blue-green-red to gray conversion """ if not isinstance(images, list): raise TypeError(f"Expected a list of images not {type(images).__name__}") return list(map(lambda x: cv2.cvtColor(x, cv2.COLOR_BGR2GRAY), images))
[docs]class Segmentation(object): def __init__(self, directory=None, image_suffix="png", color_mode="rgb"): """ :param directory: A directory containing images(currently only supports .jpg images :param image_suffix: Suffix of images in directory. For mixed types(png and jpg), set suffix as png. \ Defaults to png. :param color_mode: Specifies the nature of input images. Defaults to rgb implying not grayscale :type directory: str :return: An object of class Segmentation """ self.directory = directory if self.directory is None: self.directory = "." # check that the directory actually exists if not path.isdir(self.directory): raise NotADirectoryError(f"{self.directory} is not a valid directory in the current path.") self.image_suffix = image_suffix if self.image_suffix not in ["png", "jpg", "tif"]: raise ValueError("Only png, jpg, and tif are supported") self.color_mode = color_mode
[docs] def read_images(self, other_directory=None): """ :param other_directory: Use if images exist in sub-folders or another folder. Only for jpg and png suffixes. :return: Returns an n-D array of images. """ if self.image_suffix == "tif": images_list = sorted(glob.glob(self.directory + "/*.tif")) return [imread(x, plugin='pil') for x in images_list] else: read_path = self.directory + "/*.jpg" + pathsep + self.directory + "/*.png" if other_directory is None: pass else: read_path = read_path + pathsep + other_directory + "/*.png" + pathsep + other_directory + "/*.jpg" return list(imread_collection(read_path))
[docs] def smooth(self, mask="box", kernel_shape=(5, 5), **kwargs): """ :param mask: A low pass filter method to use for noise reduction :param kernel_shape: A tuple specifying the shape of the kernel. Defaults to (3, 3) :return: Images convolved with a low pass filter to reduce noise """ if mask not in ["mean", "gaussian", "box", "median"]: raise ValueError("mask should be one of mean, median, box, and gaussian") if not isinstance(kernel_shape, tuple): raise TypeError(f"Expected a tuple not {type(kernel_shape).__name__}") image_list = self.read_images() mask_list = {'gaussian': lambda x: ndimage.gaussian_filter(x, **kwargs), 'box': lambda x: cv2.blur(x, ksize=kernel_shape), 'median': lambda x: cv2.medianBlur(x, ksize=kernel_shape[0])} # alias box with mean, would be great to have a .alias method mask_list.update({"mean": mask}) # convolve images with low pass filter print(f"Smoothing with {mask}") low_pass_filtered = list(map(mask_list[mask], image_list)) return low_pass_filtered
[docs] def threshold_images(self, threshold_method="binary", use_threshold=127, use_max=255): """ :param threshold_method: Threshold method to use. :param use_threshold: Threshold value :param use_max: Maximum value of threshold :return: Thresholded images """ # use cv2's threshold instead since this is more mature, don't reinvent # There must be a different way than a list-map approach threshold_methods = {'binary': lambda x: cv2.threshold(x, use_threshold, use_max, cv2.THRESH_BINARY), 'binary_inverse': lambda x: cv2.threshold(x, use_threshold, use_max, cv2.THRESH_BINARY_INV), 'thresh_to_zero': lambda x: cv2.threshold(x, use_threshold, use_max, cv2.THRESH_TOZERO), 'otsu': lambda x: cv2.threshold(x, use_threshold, use_max, cv2.THRESH_BINARY + cv2.THRESH_OTSU)} # TODO # Automate key error messages # Support user defined methods if threshold_method not in threshold_methods.keys(): raise ValueError(f"Thresholding with {threshold_method} is not supported") image_list = self.read_images() if self.color_mode == "rgb": image_list = gray_images(image_list) thresholded_images = list(map(threshold_methods[threshold_method], image_list)) # drop ret val thresholded_images = [image[1] for image in thresholded_images] return thresholded_images
[docs] def detect_edges(self, operator="sobel_vertical", kernel_size=3, optional_mask=None, **kwargs): """ :param optional_mask: See skimage.filters.scharr_v for details. :param kernel_size: int size to use for edge detection kernels :param operator: One of sobel_vertical, sobel_horizontal,prewitt_horizontal,prewitt_vertical or laplace. :return: Detected edges """ available_operators = ["sobel_horizontal", "sobel_vertical", "prewitt_horizontal", "prewitt_vertical", "laplace", "roberts_cross_neg", "roberts_horizontal", "scharr_vertical", "scharr_horizontal", "canny", "roberts"] if operator not in available_operators: raise ValueError(f"Edge detection with {operator} not supported.") kernels = {'sobel_horizontal': lambda x: cv2.Sobel(x, cv2.CV_64F, 1, 0, ksize=kernel_size), 'sobel_vertical': lambda x: cv2.Sobel(x, cv2.CV_64F, 0, 1, ksize=kernel_size), 'roberts': lambda x: filters.roberts(x, optional_mask), 'roberts_cross_neg': lambda x: ndimage.convolve(x, np.array([[0, -1], [1, 0]])), 'roberts_cross_pos': lambda x: filters.roberts_pos_diag(x, optional_mask), 'laplace': lambda x: cv2.Sobel(x, cv2.CV_64F, 1, 0, ksize=kernel_size), 'prewitt_horizontal': lambda x: filters.prewitt_h(x, optional_mask), 'prewitt_vertical': lambda x: filters.prewitt_v(x, optional_mask), 'scharr_horizontal': lambda x: filters.scharr_h(x, optional_mask), 'scharr_vertical': lambda x: filters.scharr_v(x, optional_mask)} print(f"Detecting edges with the {operator} operator") # denoise and gray if self.color_mode == "gray": denoised = self.smooth(**kwargs) else: denoised = gray_images(self.smooth(**kwargs)) return list(map(kernels[operator], denoised))
[docs]def show_images(original_images=None, processed_images=None, cmap="gray", number=None, figure_size=(20, 20), custom_titles=None): """ :param custom_titles: A list of length 2 for titles to use. Defaults to ['original','processed'] :param figure_size: Size of the plot shown. Defaults to (20,20) :param original_images: Original Images from read_images() :param processed_images: Images that have been converted eg from detect_edges() :param cmap: Color cmap from matplotlib. Defaults to gray :param number: optional Number of images to show :return: A matplotlib plot of images """ if number is None: # This assumes that both lists will be the same length # TODO: test for length equivalency, error on incompatible lengths number = len(original_images) image_list = reshape_images(original_images[:number]) if custom_titles is None: custom_titles = ['original'] if processed_images is not None: processed_images = reshape_images(processed_images[:number]) image_list = list(chain(*zip(image_list, processed_images))) custom_titles = ["original", "processed"] # Currently only considering divisibility by three and two for image visualization # This is mostly necessary for single list visualization number_of_columns = len(image_list) / 3 number_of_rows = 3 if len(image_list) % 2 == 0: number_of_columns = len(image_list) / 2 number_of_rows = 2 custom_titles = custom_titles * len(image_list) fig, axes = plt.subplots(nrows=int(number_of_rows), ncols=int(number_of_columns), figsize=figure_size) print(f"Showing {number} image(s)") for ind, image in enumerate(image_list): axes.ravel()[ind].imshow(image, cmap=cmap) axes.ravel()[ind].set_title(f'{custom_titles[ind]}') axes.ravel()[ind].set_axis_off()
[docs]def resize_images(image_list=None, target_size=None): """ :param image_list: A list of images or image that needs to be resized :param target_size: New target image size :return: Image or list of images that have been resized. """ if image_list is None or target_size is None: raise ValueError("Please provide both an image list and a target size") if not isinstance(target_size, tuple): raise TypeError(f"Expected a tuple in target_size not {type(target_size).__name__}") return [resize(x, target_size) for x in image_list]
[docs]def reshape_images(image_list=None): """ :param image_list: A list of images to reshape for plotting :return: Images that can be plotted with show_images """ if image_list is None: raise ValueError("Please provide a list of images to reshape.") final_list = [img[:, :, 0] if len(img.shape) == 3 and img.shape[2] != 3 else img for img in image_list] return final_list
[docs]def stack_images(list_one=None, list_two=None, direction="horizontal"): """ :param list_one: List containing image arrays to stack together :param list_two: Another list of image arrays :param direction: Stacking direction. One of horizontal and vertical,defaults to horizontal :return: Returns a list of images stacked together as requested. """ if any(input_list is None for input_list in [list_one, list_two]): raise ValueError("Please provide two lists to stack") if not all(isinstance(input_list, list) for input_list in [list_one, list_two]): raise TypeError("Both list_one and list_two should be lists") if direction not in ["horizontal", "vertical", "h", "v"]: raise ValueError(f"direction should be one of horizontal, vertical, h, v not {direction}") stack_direction = {"horizontal": np.hstack, "vertical": np.vstack, "h": np.hstack, "v": np.vstack} stacked_images = [] for img_x, img_y in zip(list_one, list_two): stacked_images.append(stack_direction[direction]((img_x, img_y))) return stacked_images
[docs]def plot_hist(input_image=None, lim=None, color_mode="gray"): """ :param input_image: An image representation(array) whose histogram is required. :param lim: A list to define the range of the x-axis, defaults to [0, 256] :param color_mode: One of gray or rgb. This determines the number of plots shown. :return: A histogram plot """ if lim is None: lim = [0, 256] channel = "b" if color_mode == "rgb": channel = ('b', 'g', 'r') for i, col in enumerate(channel): calculated_hist = cv2.calcHist([input_image], [i], None, [256], [0, 256]) plt.plot(calculated_hist, color=col) plt.xlim(lim)