Cell contours

The boundary of a closed and bounded 2D region may be parameterized as a function of one variable. Instance masks, or more specifically, affinity graphs, allow us to traverse the boundary of a cell and index each pixel from 0 to N-1, where N is the number of boundary pixels. Indexed/parameterized boundaries are called contours.

Example cells

The previous section demonstrated the generation of cell contours in the context of self-contact, where a cyclic color map was applied to visualize the cell boundaries. Here we demonstrate how to convert contours to vector outlines with a simpler set of cell masks. We first preprocess the masks to remove any "spurs" (boundary pixels sharing only one face with a neighbor), then generate an affinity graph, compute the contours, and finally make a spline to interpolate a smooth outline through the contour pixels.

Hide code cell source
 1%%capture --no-display 
 2
 3from scipy.interpolate import splprep, splev
 4from skimage.segmentation import find_boundaries
 5import matplotlib.pyplot as plt
 6import matplotlib.path as mpath
 7import matplotlib.patches as mpatches
 8
 9
10import matplotlib as mpl
11%matplotlib inline
12
13mpl.rcParams['figure.dpi'] = 600
14import matplotlib.pyplot as plt
15from matplotlib import cm
16plt.style.use('dark_background')
17plt.rcParams['image.cmap'] = 'gray'
18plt.rcParams['image.interpolation'] = 'nearest'
19plt.rcParams['figure.frameon'] = False
20
21
22import omnipose
23import cellpose_omni
24import numpy as np
25
26from omnipose.plot import imshow
27
28from pathlib import Path
29import os, re
30from cellpose_omni import io
31import fastremap
32
33omnidir = Path(omnipose.__file__).parent.parent.parent
34basedir = os.path.join(omnidir,'docs','_static')
35# name = 'ecoli_phase'
36name = 'ecoli'
37ext = '.tif'
38image = io.imread(os.path.join(basedir,name+ext))
39masks = io.imread(os.path.join(basedir,name+'_labels'+ext))
40slc = omnipose.measure.crop_bbox(masks>0,pad=0)[0]
41masks = fastremap.renumber(masks[slc])[0]
42image = image[slc]
43
44# set up dimensions
45msk = masks
46dim = msk.ndim
47shape = msk.shape
48steps,inds,idx,fact,sign = omnipose.utils.kernel_setup(dim)
49
50# remove spur points - this method is way easier than running core._despur() on the priginal affinity graph
51bd = find_boundaries(msk,mode='inner',connectivity=2)
52msk, bounds, _ = omnipose.core.boundary_to_masks(bd,binary_mask=msk>0,connectivity=1) 
53
54# generate affinity graph
55coords = np.nonzero(msk)
56affinity_graph =  omnipose.core.masks_to_affinity(msk, coords, steps, inds, idx, fact, sign, dim)
57neighbors = omnipose.utils.get_neighbors(tuple(coords),steps,dim,shape) # shape (d,3**d,npix)
58
59# find contours 
60contour_map, contour_list, unique_L = omnipose.core.get_contour(msk,
61                                                                affinity_graph,
62                                                                coords,
63                                                                neighbors,
64                                                                cardinal_only=1)
65
66# make a figure
67dpi=90
68fig = imshow([image,masks,bd,bounds,contour_map,image],
69             titles=['image','masks','boundary','despurred boundary','contour','vector outline'], 
70             dpi=dpi, hold=True)
71
72# plot vector outlines on last panel 
73ax = fig.get_axes()[-1]
74
75# outline params
76smooth_factor = 5 # smaller is smoother, higher follows pixel edges more tightly 
77color = 'r' 
78
79
80for contour in contour_list:
81    if len(contour) > 1:
82        pts = np.stack([c[contour] for c in coords]).T
83        tck, u = splprep(pts.T, u=None, s=len(pts)/smooth_factor, per=1) 
84        u_new = np.linspace(u.min(), u.max(), len(pts))
85        x_new, y_new = splev(u_new, tck, der=0)
86
87        # Define the points of the polygon
88        points = np.column_stack([y_new, x_new])
89        
90        # Create a Path from the points
91        path = mpath.Path(points, closed=True)
92        
93        # Create a PathPatch from the Path
94        patch = mpatches.PathPatch(path, fill=None, edgecolor=color, linewidth= dpi/72, capstyle='round')
95        
96        ax.add_patch(patch)
97
98fig
_images/66d99bd9ac01890ee92340082052ee26f9407f4abc0d5e982383609876f2b216.png

Currently, the contours are not following strict left- or right-handedness, and that is something I would like to improve.