Source code for DiatomTrack.utils.contours

import cv2 as cv
import numpy as np

from tqdm.tk import tqdm_tk as tqdm


[docs]def contour_filter_process(cnts, limit): """ Filter contours, combine adjacent ones, extract key parameters, and transform them into rotated bounding boxes. Parameters ---------- cnts : dict The dictionary containing the contour data. limit : int The number of frames to process. Returns ------- filtered_contours : dict The dictionary containing the filtered and combined contours. properties : list Tuples of areas and rotated bounding boxes. """ singles = list(cnts['single'].items()) aggregates = list(cnts['aggregate'].values()) filtered_contours = dict() properties = [] for iterator in tqdm( range(len(singles)), desc='Processing detected objects', total=limit if limit!=0 else len(singles)): if iterator == limit and limit != 0: break key, single = singles[iterator] aggregate = aggregates[iterator] new_single, new_aggregate = contour_filter(list(single), list(aggregate)) filtered_contours.update({ key: {'single': new_single, 'aggregate': new_aggregate}}) properties.append( contours_to_properties(key, new_single, new_aggregate)) return filtered_contours, properties
[docs]def contour_filter(single_contours, aggregate_contours, dim=(1536,2048)): """ A filter function for the contours that are output by the segmentation. Combine contours that are directly adjacent into an aggregate contour. Filter contours by size and ratio of length to area. Parameters ---------- single_contours : list Contours of single cells. aggregate_contours : list Contours of aggregates. dim : tuple, optional The image size. The default is (1536,2048). Returns ------- single_contours_filtered : list The filtered contours for the single classification. new_aggregate_contours : list The updated contours of the aggregate classification. """ mask_all = np.zeros(dim, dtype=np.uint8) mask_aggregate = np.zeros(dim, dtype=np.uint8) mask_all_cnts = np.zeros(dim, dtype=np.uint16) areas = [ cv.contourArea(c) for c in single_contours if cv.arcLength(c, closed=True)>5] cv.drawContours(mask_all, single_contours, -1, (1), -1) mean = sum(areas)/len(areas) single_contours_filtered = [ c for a, c in zip(areas, single_contours) if a < 1.3*mean] new_aggregate_contours = aggregate_contours + [ c for a, c in zip(areas, single_contours) if a >= 1.3*mean] cv.drawContours(mask_aggregate, new_aggregate_contours, -1, (1), -1) mask_all += mask_aggregate mask_all[mask_all > 0] = 1 mask_all = cv.morphologyEx(mask_all, cv.MORPH_CLOSE, np.ones((5,5))) all_cnts, _ = cv.findContours(mask_all, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE) all_cnts = list(all_cnts) for idx, contour in enumerate(all_cnts): cv.drawContours(mask_all_cnts, [contour], -1, (idx+1), -1) new_aggregate_mask = cv.drawContours( np.zeros(dim, dtype=np.uint16), new_aggregate_contours, -1, (1)) aggregate_in_all = np.unique(mask_all_cnts[new_aggregate_mask != 0]) aggregate_in_all = set(aggregate_in_all[aggregate_in_all > 0] - 1) new_aggregate_contours = [] single_contours_filtered = [] for i, c in enumerate(all_cnts): if i in aggregate_in_all: new_aggregate_contours.append(c) else: single_contours_filtered.append(c) single_contours_filtered = [ c for c in single_contours_filtered if cv.contourArea(c) > 30] areas = [cv.contourArea(c) for c in new_aggregate_contours] new_aggregate_contours = [ c for c, a in zip(new_aggregate_contours, areas) if 6*mean > a > 30 and (cv.arcLength(c, closed=True)/4)**2 < 2*a] return single_contours_filtered, new_aggregate_contours
[docs]def rotation_boundary(angle): """ Convert degree to radian and enforce the angle to be in range 0 to pi. """ angle = np.deg2rad(angle) while not angle <= np.pi: angle -= np.pi while not angle >= 0: angle += np.pi return angle
[docs]def convert_array_to_rectangle(array): """ Convert a flat numpy array to format of a minAreaRect of opencv. """ x, y, width, height, angle = array angle = rotation_boundary(angle) angle = np.rad2deg(angle) if width < height: angle -= 90 return (x, y), (width, height), angle
[docs]def convert_contour_to_array(contour): """ Convert an opencv contour to flat numpy array containing the information of a minAreaRect. """ rectangle = cv.minAreaRect(contour) (x, y), (width, height), angle = rectangle # minAreaRect returns an angle in [0;90) but 180° is needed related to long side if width < height: angle += 90 angle = rotation_boundary(angle) return np.array([x, y, width, height, angle])
[docs]def contours_to_rectangles(contours, label): """ Take a list of contours and a label and extract the minAreaRect properties. """ result = [] for contour in contours: res = convert_contour_to_array(contour) result.append((*res, label, contour)) return result
[docs]def contours_to_properties(key, contours_single, contours_aggregate): """ Transform a list of single and aggregate contours into a into a list of tuples of the key, the properties (minAreaRect, label, contour) and the areas of the contours. """ property_single = contours_to_rectangles(contours_single, 1) property_aggregate = contours_to_rectangles(contours_aggregate, 2) properties = property_single + property_aggregate areas_single = [cv.contourArea(c) for c in contours_single] areas_aggregate = [cv.contourArea(c) for c in contours_aggregate] areas = areas_single + areas_aggregate return key, properties, areas