Cell diameter¶
The idea of an average cell diameter sounds intuitive, but the standard implementation of this idea fails to capture that intuition. The go-to method (adopted in Cellpose) is to calculate the cell diameter as the diameter of the circle of equivalent area. As I will demonstrate, this fails for anisotropic (non-circular) cells. As an alternative, I devised the following simple diameter metric:
diameter = 2*(dimension+1)*np.mean(distance_field)
Because the distance field represents the distance to the closest boundary point, it naturally captures the intrinsic 'thickness' of a region (in any dimension). Averaging the field over the region (the first moment of the distribution) distills this information into a number that is intuitively proportional to the thickness of the region. For example, if a region is made up of a bungle of many thin fragments, its mean distance is far smaller than the mean distance of the circle of equivalent area. But to call it a 'diameter', I wanted this metric to match the diameter of a sphere in any dimension. So, by calculating the average of distance field of an n-sphere, we get the above expression for the the diameter of an n-sphere given the average of the distance field over the volume.
Example cells¶
Filamenting bacterial cells often exhibit constant width but increasing length. This dataset comes from the deletion of the essential gene ftsN in Acinetobacter baylyi.
Show code cell source
1%%capture --no-display
2
3from pathlib import Path
4from cellpose_omni import utils, plot, models, io, dynamics
5import os, sys, io
6import numpy as np
7import matplotlib.pyplot as plt
8plt.style.use('dark_background')
9import matplotlib as mpl
10%matplotlib inline
11mpl.rcParams['figure.dpi'] = 600
12
13# Save a reference to the original stdout stream
14old_stdout = sys.stdout
15
16# Redirect stdout to a StringIO object
17sys.stdout = io.StringIO()
18
19
20import omnipose
21from omnipose.plot import imshow
22import tifffile
23omnidir = Path(omnipose.__file__).parent.parent.parent
24basedir = os.path.join(omnidir,'docs','_static')
25nm = 'ftsZ'
26masks = tifffile.imread(os.path.join(basedir,nm+'_masks.tif'))
27mnc = omnipose.plot.apply_ncolor(masks)
28
29f = 1
30c = [0.5]*3
31fontsize=10
32dpi = mpl.rcParams['figure.dpi']
33Y,X = masks.shape[-2:]
34szX = max(X//dpi,2)*f
35szY = max(Y//dpi,2)*f
36
37# T = [50,80,100,150,180,240]
38T = range(0,len(masks),45)
39titles = ['Frame {}'.format(t) for t in T]
40ims = [mnc[t] for t in T]
41N = len(titles)
42
43fig, axes = plt.subplots(1,N, figsize=(szX*N,szY))
44fig.patch.set_facecolor([0]*4)
45
46for i,ax in enumerate(axes):
47 ax.imshow(ims[i])
48 ax.axis('off')
49 ax.set_title(titles[i],c=c,fontsize=fontsize,fontweight="bold")
50
51plt.subplots_adjust(wspace=0.1)
52plt.show()
53
54# Restore the original stdout stream
55sys.stdout = old_stdout
Compare diameter metrics¶
By plotting the mean diameter (averaged over all cells after being computed per-cell, of course), we find that
the 'circle diameter metric' used in Cellpose rises drastically with cell length, but the 'distance diameter metric' of Omnipose remains nearly constant. If we tried to use the former to train a SizeModel(), images would get downsampled
heavily to the point of cells being too thin to segment, and that is assuming that the model can reliably detect the highly nonlocal property of cell length in an image instead of the local property of
cell width (at least, what we humans would point to and call cell width).
Of course, we also want to measure accurate cell widths, and it turns out that the Omnipose metric scales linearly with width perfectly spherical objects and quadratically for infinite rods. Real cells fall somewhere between these two extremes, so we have also recently developed another metric for a pill (rod with hemispherical caps) that computes the cap radius R and rod length L from the area and integrated distance field.
Manual inspection of these masks shows that the diameter of the cells stays at a roughly constant 8px. The measured pill diameter 2R matches well with this, and the Omnipose metric correlates well but must be corrected with a factor of 0.75 to match the absolute scale.
Assuming the strict cell geometry bothers me, and my intuition tells me that we may yet be able to construct a morphology-independent measure of cell width from the distance field. My best attempt so far is to weight the distance field inverse to the magnitude of its gradient; this is equivalent to integrating the distance field over the skeleton / medial axis. This too gives us an accurate measure of cell width. This ends up pulling in some pixels close to the poles and is sensitive to pixelization artifacts of small cells (you can see this in the dips corresponding to division events). However, this sensitivity may actually tell us something real about the cell morphology, such as the non-circularity of the poles and the pinching of the septum. I could be convinced, for example, that this measurement really does correspond to the cells being a bit fatter at t=0 than t=-1.
Show code cell source
1import fastremap, edt
2n = len(masks)
3diam_old = np.zeros(n)
4cell_num = np.zeros(n)
5x = range(n)
6pL = np.zeros(n)
7pR = np.zeros(n)
8oL = np.zeros(n)
9oD = np.zeros(n)
10
11rL = np.zeros(n)
12rR = np.zeros(n)
13for k in x:
14 m = masks[k]
15 fastremap.renumber(m,in_place=True)
16 cell_num[k] = m.max()
17 diam_old[k] = utils.diameters(m,omni=False)[0]
18 pR[k], pL[k] = omnipose.core.diameters(m,pill=True)
19 oD[k], oL[k] = omnipose.core.diameters(m,pill=False,return_length=True)
20
21 # weight by flow magnitude
22 d = edt.edt(m)
23 bin0 = m>0
24 D = np.sum(d[bin0])
25 dP = np.stack(np.gradient(d))
26 w = np.sqrt(np.sum(dP**2,axis=0))<0.6
27 rR[k] = (np.sum(w[bin0]*d[bin0])/np.sum(w[bin0]))-1
28 rL[k] = (3*D - np.pi*(rR[k]**4)) / (rR[k]**3)
29
Show code cell source
1from omnipose.utils import sinebow
2golden = (1 + 5 ** 0.5) / 2
3sz = 4
4labelsize = 5
5
6%matplotlib inline
7
8plt.style.use('dark_background')
9mpl.rcParams['figure.dpi'] = 300
10
11axcol = [0.5]*3+[1]
12f = 0.75
13labels = ['Cellpose','Omnipose','Pill',f'Omnipose**{f}','Weighted Distance']
14lines = [diam_old,
15 oD,
16 pR*2,
17 oD**f,
18 rR*2,
19 ]
20
21N = len(labels)
22colors = sinebow(N,offset=0)
23background_color = [0]*4
24
25fig = plt.figure(figsize=(sz, sz/golden),frameon=False)
26fig.patch.set_facecolor(None)
27
28ax = plt.axes()
29maxnorm = 0
30minmaxnorm = 0
31log = 0
32for line,label,color in zip(lines,
33 labels,
34 [colors[i+1] for i in range(N)]):
35 l = line.copy()
36 if maxnorm:
37 l /= l.max()
38 elif minmaxnorm:
39 l = omnipose.utils.rescale(l)
40
41 ax.plot(x,l,c=color,label=label)
42
43ax.hlines(8,x[0],x[-1],[0.75]*3,'--',label = '8px', linewidth = 0.5)
44
45
46ax.legend(loc='best', frameon=False,labelcolor=axcol, fontsize = labelsize)
47ax.tick_params(axis='both', which='major', labelsize=labelsize,length=3, direction="out",colors=axcol,bottom=True,left=True)
48ax.tick_params(axis='both', which='minor', labelsize=labelsize,length=3, direction="out",colors=axcol,bottom=True,left=True)
49ax.set_ylabel('Diameter metric', fontsize = labelsize,c=axcol)
50ax.set_xlabel('Frame number', fontsize = labelsize, c=axcol)
51ax.set_facecolor(background_color)
52
53for spine in ax.spines.values():
54 spine.set_color(axcol)
55
56ax.spines['top'].set_visible(False)
57ax.spines['right'].set_visible(False)
58ax.grid(False)
59if log: ax.set_yscale('log')
60
61
62
63plt.show()