Optimizing parameters for membrane-image based cell segmentation#

Workflows for segmenting cells from membrane staining images are often hard to optimize. In this notebook we demonstrate how to automatically optimizing the seeded watershed algorithm, a common approach for this kind of image data.

from napari_workflow_optimizer import JaccardLabelImageOptimizer, Workflow

from skimage.io import imread
import napari_segment_blobs_and_things_with_membranes as nsbatwm
import pyclesperanto_prototype as cle
import matplotlib.pyplot as plt
from the_segmentation_game.metrics import jaccard_index_sparse

We set up a workflow and insert a single operation, Seeded Watershed using local minima as starting points using the Napari plugin napari-segment-blobs-and-things-with-membranes. The algorithm has two parameters: spot_sigma for tuning how close seed points can be and outline_sigma for tuning how precise the membranes should be segmented.

w = Workflow()
w.set("labeled", # result image name
      nsbatwm.thresholded_local_minima_seeded_watershed, # operation
      "input", spot_sigma=2, outline_sigma=2) # parameters
# image data source: scikit-image cells3d example, slice 28
w.set("input", imread("../../data/membranes_2d.tif"))
input_image = w.get("input")
cle.imshow(input_image)
../_images/e582bd9aaa1d965ac4bbf1c6dc70e9c071f0818c0cfc99ea62e7e963029484a6.png

We produce a first segmentation result that is oversegmented, there are obviously too many cells found.

result = w.get("labeled")
cle.imshow(result, labels=True)
../_images/94cbfdd60d9e0413d52cad0a1633de52542aa84530ed4b3fb916eeeb3a0855bb.png

To give the segmentation algorithm some ground truth to compare segmentation results with, we use this sparse annotation image. It typically is enough to annotate some example cells accurately. Better spend time on making good segmentations and do not draw so many.

ground_truth = imread("../../data/membranes_2d_sparse_labels.tif")
cle.imshow(ground_truth, labels=True)
../_images/9db50b653fefde0c26cfabf08d8f73b6e8a880aad8a14aaf0cebcdce1fc1342e.png

We can then initiate the JaccardLabelImageOptimizer. Just for testing we inspect the current starting point for optimization.

jlio = JaccardLabelImageOptimizer(w)
jlio.get_numeric_parameters()
[2, 2, 500]

We then start the optimization and afterwards print out the optimized parameter set.

best_param = jlio.optimize("labeled", ground_truth, maxiter=100)
best_param
array([  2.34307473,   5.6861856 , -74.78749191])

We can also ask the optimizer to set these parameters for us and inspect the resulting label image.

jlio.set_numeric_parameters(best_param)
cle.imshow(w.get("labeled"), labels=True)
../_images/85473129a1e7a7ea7e18a6c348231691b1ff249ea3ce6aecb9e2c2b70cc179c4.png

The quality of this image can be measured by averaging the Jaccard Index of the three ground truth objects. The Segmentation Game library has a function for this.

jaccard_index_sparse(ground_truth, w.get("labeled"))
0.8300891581238193

Sometimes, the result is not perfect and we may want to change one parameter and see if the result can be improved.

new_starting_point = best_param.copy()
new_starting_point[0] = 8

new_starting_point
array([  8.        ,   5.6861856 , -74.78749191])
jlio2 = JaccardLabelImageOptimizer(w)
jlio2.set_numeric_parameters(new_starting_point)
cle.imshow(w.get("labeled"), labels=True)
../_images/1af543a541e502ab71e23fa598db3fdce5e138f7e7e8044945fa209c9839631e.png

We can then start a second attempt.

best_param = jlio.optimize("labeled", ground_truth, maxiter=100)
best_param
array([  9.50775939,   1.39446381, -83.9132378 ])
jlio.set_numeric_parameters(best_param)
cle.imshow(w.get("labeled"), labels=True)
../_images/38c6a0a6aa934ecea8c6ee139e0f341375e2a41ff16f565e519997e3d335ae87.png
jaccard_index_sparse(ground_truth, w.get("labeled"))
0.8905848327576197