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.

Hide code cell source
 1from pathlib import Path
 2from cellpose_omni import utils, plot, models, io, dynamics
 3import os, sys, io
 4import numpy as np
 5import matplotlib.pyplot as plt
 6plt.style.use('dark_background')
 7import matplotlib as mpl
 8%matplotlib inline
 9mpl.rcParams['figure.dpi'] = 600
10
11# Save a reference to the original stdout stream
12old_stdout = sys.stdout
13
14# Redirect stdout to a StringIO object
15sys.stdout = io.StringIO()
16
17
18import omnipose
19from omnipose.plot import imshow
20import tifffile
21omnidir = Path(omnipose.__file__).parent.parent
22basedir = os.path.join(omnidir,'docs','_static')
23nm = 'ftsZ'
24masks = tifffile.imread(os.path.join(basedir,nm+'_masks.tif'))
25mnc = omnipose.plot.apply_ncolor(masks)
26
27f = 1
28c = [0.5]*3
29fontsize=10
30dpi = mpl.rcParams['figure.dpi']
31Y,X = masks.shape[-2:]
32szX = max(X//dpi,2)*f
33szY = max(Y//dpi,2)*f
34
35# T = [50,80,100,150,180,240]
36T = range(0,len(masks),45)
37titles = ['Frame {}'.format(t) for t in T]
38ims = [mnc[t] for t in T]
39N = len(titles)
40
41fig, axes = plt.subplots(1,N, figsize=(szX*N,szY))  
42fig.patch.set_facecolor([0]*4)
43
44for i,ax in enumerate(axes):
45    ax.imshow(ims[i])
46    ax.axis('off')
47    ax.set_title(titles[i],c=c,fontsize=fontsize,fontweight="bold")
48
49plt.subplots_adjust(wspace=0.1)
50plt.show()
51
52# Restore the original stdout stream
53sys.stdout = old_stdout
_images/90f7135246eda26ac7f8ffd97251248283940ffc407909148c6eb4152f294560.png

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).

Hide code cell source
 1import fastremap
 2n = len(masks)
 3diam_old = []
 4diam_new = []
 5cell_num = []
 6x = range(n)
 7for k in x:
 8    m = masks[k]
 9    fastremap.renumber(m,in_place=True)
10    cell_num.append(m.max())
11    diam_old.append(utils.diameters(m,omni=False)[0])
12    diam_new.append(utils.diameters(m,omni=True)[0])
13
14
15from omnipose.utils import sinebow
16golden = (1 + 5 ** 0.5) / 2
17sz = 4
18labelsize = 5
19
20%matplotlib inline
21
22plt.style.use('dark_background')
23mpl.rcParams['figure.dpi'] = 300 
24
25axcol = [0.5]*3+[1]
26N = 3
27colors = sinebow(N,offset=0)
28background_color = [0]*4
29
30fig = plt.figure(figsize=(sz, sz/golden),frameon=False) 
31fig.patch.set_facecolor(None)
32
33ax = plt.axes()
34
35ax.plot(range(n),diam_old,c=colors[1],label='Cellpose')
36ax.plot(range(n),diam_new,c=colors[N],label='Omnipose')
37
38ax.legend(loc='best', frameon=False,labelcolor=axcol, fontsize = labelsize)
39ax.tick_params(axis='both', which='major', labelsize=labelsize,length=3, direction="out",colors=axcol,bottom=True,left=True)
40ax.tick_params(axis='both', which='minor', labelsize=labelsize,length=3, direction="out",colors=axcol,bottom=True,left=True)
41ax.set_ylabel('Diameter metric', fontsize = labelsize,c=axcol)
42ax.set_xlabel('Frame number', fontsize = labelsize, c=axcol)
43ax.set_facecolor(background_color)
44
45for spine in ax.spines.values():
46    spine.set_color(axcol)
47    
48ax.spines['top'].set_visible(False)
49ax.spines['right'].set_visible(False)
50
51plt.show()
_images/81ff9cc3344a738fbceb835a911fa0c1fd6829a99be88581cdae844c5cd04cf7.png