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