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

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