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.
Show 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
Currently, the contours are not following strict left- or right-handedness, and that is something I would like to improve.