Classic Segmentation

In this notebook, we will segment the cells image using a traditional ImageJ segmentation workflow:

  1. Preprocess the image

  2. Apply an auto threshold

  3. Create and manipulate a mask

  4. Create and transfer a selection from the mask to the original image

  5. Analyze the resulting data

💡 See the “Segmentation with ImageJ” living workshop for a primer on segmentation in ImageJ.

We will do the same analysis twice: once with ImageJ, and then again with ImageJ2.

Segmentation workflow with original ImageJ functions

import imagej
import scyjava as sj

# initialize ImageJ
ij = imagej.init('sc.fiji:fiji', mode='interactive')
print(f"ImageJ version: {ij.getVersion()}")
ImageJ version: 2.14.0/1.54f
import skimage
cells = skimage.data.cells3d()

Because this sample image is a NumPy array, but not an xarray, it does not have dimensional axis labels. However, scikit-image has defined conventions for the order of dimensions as follows:

(t, pln, row, col, ch)

Where t is time, pln is plane/Z, row is row/Y, col is column/X, and ch is channel.

Now that we are armed with that knowledge, notice that cells actually has a slightly different dimension order, with planar rather than interleaved channels: (pln, ch, row, col). Let’s construct an xarray from this image that includes the correct dimensional axis labels:

import xarray
xcells = xarray.DataArray(cells, name='xcells', dims=('pln', 'ch', 'row', 'col'))

# print some basic info
print(f"name: {xcells.name}\ndimensions: {xcells.dims}\nshape: {xcells.shape}")
name: xcells
dimensions: ('pln', 'ch', 'row', 'col')
shape: (60, 2, 256, 256)
# convert xcells image to ImagePlus
imp = ij.py.to_imageplus(xcells)
imp.setTitle("cells")
# slice out an image plane.
c, z, t = 2, 36, 1
Duplicator = sj.jimport('ij.plugin.Duplicator')
imp2d = Duplicator().run(imp, c, c, z, z, t, t)
imp2d.setTitle("cells-slice")
ij.py.show(imp2d)
_images/f9a85a308dafdef1bffa4d7842b90d163401a8ab6bbc131373a34565e2225c44.png
# preprocess with edge-preserving smoothing
ij.IJ.run(imp2d, "Kuwahara Filter", "sampling=10") # Look ma, a Fiji plugin!
ij.py.show(imp2d)
_images/0b03ee2927188057fc40568c1f942626d8ee90fbd8ceeb3041f48e8b3306fbab.png
# threshold to binary mask
Prefs = sj.jimport('ij.Prefs')
Prefs.blackBackground = True
ij.IJ.setAutoThreshold(imp2d, "Otsu dark")
ImagePlus = sj.jimport("ij.ImagePlus")
mask = ImagePlus("cells-mask", imp2d.createThresholdMask())
ij.IJ.run(imp2d, "Close", "")
ij.py.show(mask)
_images/8ac7475f7dcbcfed95f9ae4c7937b4eb0c41f3d70fd954a5257a5b38a5c4d56c.png
# clean up the binary mask.
ij.IJ.run(mask, "Dilate", "")
ij.IJ.run(mask, "Fill Holes", "")
ij.IJ.run(mask, "Watershed", "")
ij.py.show(mask)
_images/ff6357f7906dd7143de7a6ccd9ffffcae62f06b3f04c5f1c75923e81203a87d4.png
# Save the mask as a selection (a.k.a. ROI).
ij.IJ.run(mask, "Create Selection", "")
roi = mask.getRoi()
ij.IJ.run(mask, "Close", "")

# Split the ROI into cells.
# This works because cells are disconnected due to the watershed.
rois = roi.getRois()
print(len(rois), "cells detected")
32 cells detected
# Calculate statistics.

from collections import defaultdict
from pandas import DataFrame

# Make sure to measure the same slice as segmented!
imp.setPosition(c, z, t)

# Measure each cell, accumulating results into a dictionary.
ij.IJ.run("Set Measurements...", "area mean min centroid median skewness kurtosis redirect=None decimal=3");
results = ij.ResultsTable.getResultsTable()
stats_ij = defaultdict(list)
for cell in rois:
    imp.setRoi(cell)
    ij.IJ.run(imp, "Measure", "")
    for column in results.getHeadings():
        stats_ij[column].append(results.getColumn(column)[-1])
# Display the results.
df_ij = DataFrame(stats_ij)
# pandas thinks the keys/col names are multi-level
# see pandas docs for more info: https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html
df_ij.columns = [''.join(col) for col in df_ij.columns.values]
df_ij
Area Mean Min Max X Y Median Skew Kurt
0 8.0 14267.750000 12045.0 16929.0 251.000000 1.000000 14748.0 0.120347 -1.164583
1 317.0 11242.823344 3177.0 17925.0 82.749211 4.159306 11428.0 -0.321965 -0.205385
2 78.0 10362.525641 3983.0 16455.0 223.102564 3.551282 10480.0 -0.332912 0.246449
3 275.0 11151.727273 4884.0 17783.0 206.525455 6.129091 11476.0 -0.247095 -0.353334
4 37.0 15679.405405 10480.0 22620.0 254.635135 25.121622 16028.0 0.038666 -0.293946
5 1672.0 12202.776316 4884.0 22525.0 150.371411 18.663876 12045.0 0.283178 0.083946
6 1722.0 11502.835656 3936.0 24801.0 25.405923 20.048200 11428.0 0.306471 0.691912
7 1833.0 12482.276596 2893.0 27409.0 80.939171 32.763502 12329.0 0.446239 1.154749
8 1673.0 14284.469815 4173.0 28500.0 181.779139 49.275254 14179.0 0.131862 0.171080
9 1899.0 11739.664034 4031.0 23331.0 229.052923 49.528436 11713.0 0.276388 0.335017
10 451.0 23749.004435 4552.0 57331.0 61.349224 74.575388 19585.0 0.607546 -0.938931
11 1968.0 11891.547256 3983.0 29543.0 111.681402 72.947663 11713.0 0.446164 0.863751
12 483.0 18892.378882 3699.0 58754.0 48.357143 98.303313 14558.0 1.095616 0.364968
13 704.0 14242.279830 4552.0 36846.0 246.704545 100.444602 13942.0 0.973815 2.667452
14 1916.0 13704.077244 3936.0 29306.0 161.163883 97.252610 13657.0 0.012281 0.339191
15 1882.0 16040.547821 4220.0 41113.0 41.580234 138.865037 15981.0 0.454669 1.680102
16 2465.0 13935.406085 3272.0 32483.0 189.067140 141.803448 13657.0 0.394919 0.642032
17 1714.0 14435.262544 3272.0 30918.0 238.876313 146.781797 14368.0 0.223114 0.132397
18 91.0 10135.989011 5169.0 16502.0 3.467033 176.972527 10290.0 -0.089693 -0.125921
19 1818.0 16649.069857 4552.0 35186.0 111.142464 157.982948 16360.0 0.455061 0.653352
20 156.0 10152.826923 3746.0 20201.0 4.993590 192.743590 10195.0 0.347712 0.322143
21 1959.0 12970.475242 3557.0 32720.0 51.679684 184.038540 12614.0 0.527686 0.422364
22 48.0 9182.729167 4884.0 12282.0 2.937500 206.500000 9342.0 -0.480570 -0.059195
23 2317.0 15265.962883 4363.0 38648.0 174.615235 202.260898 14795.0 0.629945 0.713419
24 1071.0 19807.867414 3983.0 49365.0 244.153595 221.430906 19016.0 0.390848 0.213681
25 9.0 10285.000000 7967.0 13562.0 234.500000 249.500000 10243.0 0.716592 0.241600
26 2718.0 13820.101913 3983.0 36656.0 82.051141 223.022811 13230.0 0.814798 1.284255
27 1755.0 17002.144160 4268.0 55766.0 133.132479 238.070370 16265.0 1.087034 3.580292
28 53.0 13213.301887 4789.0 22525.0 188.084906 252.594340 13088.0 0.080961 -0.401084
29 738.0 11566.728997 4078.0 23236.0 209.639566 244.134146 11239.0 0.567227 0.447262
30 6.0 9349.500000 7824.0 9911.0 234.500000 255.000000 9863.0 -1.313139 0.259016
31 8.0 9952.250000 6022.0 11760.0 239.000000 255.000000 11381.0 -0.927799 -0.757281

Notice that we avoided using the RoiManager, so that the workflow still works in headless mode.

Segmentation workflow with ImageJ2

# slice out an image plane.
c, z = 1, 35
cells_slice = xcells[z,c,:,:]
ij.py.show(cells_slice)
jslice = ij.py.to_java(cells_slice)
_images/f9a85a308dafdef1bffa4d7842b90d163401a8ab6bbc131373a34565e2225c44.png
# preprocess with edge-preserving smoothing.
HyperSphereShape = sj.jimport("net.imglib2.algorithm.neighborhood.HyperSphereShape")
smoothed = ij.op().run("create.img", jslice)
ij.op().run("filter.median", ij.py.jargs(smoothed, cells_slice, HyperSphereShape(5)))
ij.py.show(smoothed)
_images/712bb6aa8f2264f8c6e1f5d6c5c799d82f50129068b6fb6c419de700a2582d90.png
# threshold to binary mask.
mask = ij.op().run("threshold.otsu", smoothed)
ij.py.show(mask)
_images/1a19685777a46d652010106754bb0a099eaf6041f130e1850e87a9f6bd0fd341.png
# clean up the binary mask.
mask = ij.op().run("morphology.dilate", mask, HyperSphereShape(1))
mask = ij.op().run("morphology.fillHoles", mask)
ij.py.show(mask)
_images/adfa38dc1901099cf1fd3efc5812e7aafb6d123a38de6a0778d688cd44d38ed1.png
# Watershed: mask to labeling.
use_eight_connectivity = True
draw_watersheds = False
sigma = 10
args = ij.py.jargs(None, mask, use_eight_connectivity, draw_watersheds, sigma, mask)
labeling = ij.op().run("image.watershed", args)
ij.py.show(labeling.getIndexImg(), cmap='tab10')
_images/70c868474a2bfb610dc5cac08f11c32df93bd5a26a107bfe9d80b502444501ac.png
# calculate statistics.

from collections import defaultdict
from pandas import DataFrame

Regions = sj.jimport("net.imglib2.roi.Regions")
LabelRegions = sj.jimport("net.imglib2.roi.labeling.LabelRegions")

def compute_stats(region, img, stats):
    samples = Regions.sample(region, img)
    stats["area"].append(ij.op().run("stats.size", samples).getRealDouble())
    stats["mean"].append(ij.op().run("stats.mean", samples).getRealDouble())
    min_max = ij.op().run("stats.minMax", samples)
    stats["min"].append(min_max.getA().getRealDouble())
    stats["max"].append(min_max.getB().getRealDouble())
    centroid = ij.op().run("geom.centroid", region)
    stats["centroid"].append(tuple(centroid.getDoublePosition(d) for d in range(centroid.numDimensions())))
    stats["median"].append(ij.op().run("stats.median", samples).getRealDouble())
    stats["skewness"].append(ij.op().run("stats.skewness", samples).getRealDouble())
    stats["kurtosis"].append(ij.op().run("stats.kurtosis", samples).getRealDouble())

regions = LabelRegions(labeling)
stats_ops = defaultdict(list)
for region in regions:
    compute_stats(region, jslice, stats_ops)
df_ops = DataFrame(stats_ops)
df_ops
area mean min max centroid median skewness kurtosis
0 1860.0 14358.090860 2845.0 30918.0 (238.75, 143.9758064516129) 14368.0 0.129771 3.085133
1 1869.0 16544.414660 3177.0 55766.0 (132.11824505082933, 237.0904226859283) 16028.0 0.846920 5.862000
2 2728.0 13762.175953 3841.0 36656.0 (81.17192082111437, 222.23717008797655) 13183.0 0.795826 4.241153
3 2445.0 14010.395092 3414.0 32483.0 (188.73824130879345, 141.48711656441716) 13704.0 0.454090 3.666324
4 2364.0 15095.145516 3367.0 38648.0 (173.9784263959391, 201.498730964467) 14700.0 0.560561 3.622262
5 1688.0 11611.678318 4220.0 24801.0 (24.873815165876778, 19.488744075829384) 11476.0 0.413529 3.775973
6 1652.0 12272.721550 4884.0 22525.0 (149.73365617433413, 18.008474576271187) 12045.0 0.339055 3.104162
7 1077.0 19755.374188 3841.0 49365.0 (243.6146703806871, 220.35468895078924) 18968.0 0.403299 3.173040
8 1894.0 13802.092397 3936.0 29306.0 (160.63991552270326, 96.76663146779303) 13704.0 0.090845 3.354490
9 1952.0 12997.042008 3557.0 32720.0 (51.01485655737705, 183.14241803278688) 12661.0 0.522687 3.443799
10 1889.0 16312.916887 4315.0 35186.0 (110.6913710958179, 157.39332980412917) 16170.0 0.295547 3.487587
11 1912.0 12057.073745 5121.0 29543.0 (111.23640167364017, 72.53190376569037) 11831.5 0.592143 4.013051
12 1857.0 11899.947765 4600.0 23331.0 (228.12493268712979, 48.758212170166935) 11760.0 0.405282 3.393002
13 1707.0 14147.489748 4268.0 28500.0 (181.02929115407147, 48.438195664909195) 14084.0 0.093980 3.090271
14 1772.0 12716.510722 4315.0 27409.0 (80.12810383747178, 32.261851015801355) 12519.0 0.696379 4.398843
15 595.0 14126.727731 4837.0 36846.0 (246.10588235294117, 97.0890756302521) 13657.0 1.145941 6.003222
16 1922.0 15867.754422 4410.0 41113.0 (41.289281997918835, 138.36732570239334) 15838.0 0.401893 4.441288
17 807.0 11452.887237 3841.0 23236.0 (210.40644361833952, 244.70755885997522) 11049.0 0.647504 3.617672
18 302.0 11532.900662 5785.0 17925.0 (82.06622516556291, 3.4768211920529803) 11594.5 -0.122937 2.561317
19 69.0 12439.275362 3936.0 22525.0 (188.2463768115942, 252.02898550724638) 12377.0 0.160562 2.404485
20 1.0 9247.000000 9247.0 9247.0 (195.0, 253.0) 9247.0 NaN NaN
21 938.0 21366.135394 5027.0 58754.0 (55.124733475479744, 85.94456289978677) 16858.0 0.884682 2.610502
22 7.0 10391.857143 6544.0 14843.0 (230.0, 242.0) 10622.0 -0.013143 1.421185
23 344.0 11232.008721 4884.0 17783.0 (209.86627906976744, 4.953488372093023) 11333.0 -0.090939 2.805479
24 32.0 11217.812500 6876.0 15459.0 (50.125, 254.15625) 11618.0 -0.200546 1.948018
25 68.0 13067.132353 5216.0 22620.0 (253.4264705882353, 24.176470588235293) 13182.5 -0.210502 2.198048
26 234.0 10364.376068 5928.0 20201.0 (2.7051282051282053, 189.76495726495727) 10053.0 0.733560 4.018682
27 31.0 12326.258065 5453.0 16929.0 (252.41935483870967, 1.7741935483870968) 12472.0 -0.574606 2.739545