Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Deep learning for segmentation: StarDist

This notebook is inspired by StarDist prediction example notebook.

StarDist is a neural network that predicts for every pixel how likely it is to be the centre of an object and what the shape of that object is. The shape is described as a star-convex polygon: rays going from the centre outwards to the boundary. Because a shape is predicted per object, touching nuclei come out as separate objects. This is what is called instance segmentation.

We use a pretrained StarDist model: the network was already trained on many annotated nuclei, and we only run the prediction from the model.

Libraries

Test on example data

We start with an image from the dataset the model was trained on (DSB2018, a collection of annotated nuclei). We use it first, before trying some other images.

(256, 256) uint8
<Figure size 640x480 with 1 Axes>

Loading a pretrained model

StarDist ships with a few pretrained models. Calling from_pretrained() without arguments prints the list.

There are 4 registered models for 'StarDist2D':

Name                  Alias(es)
────                  ─────────
'2D_versatile_fluo'   'Versatile (fluorescent nuclei)'
'2D_versatile_he'     'Versatile (H&E nuclei)'
'2D_paper_dsb2018'    'DSB 2018 (from StarDist 2D paper)'
'2D_demo'             None

We pick the 2D_versatile_fluo model, which is trained on a variety of fluorescent images. It will download the model from the internet if it is not already present on your computer.

Found model '2D_versatile_fluo' for 'StarDist2D'.
Loading network weights from 'weights_best.h5'.
Loading thresholds from 'thresholds.json'.
Using default values: prob_thresh=0.479071, nms_thresh=0.3.

Normalization

The network was trained on images whose intensities were scaled between 0-1. If we feed it raw microscope images, which are either 0-255 for 8-bit or 0-65535 for a 16-bit image the numbers are far outside the range the model has ever seen, and the predictions get worse.

We run a normalization function to scale the image between 0 and 1. normalize(img, 1, 99.8) sets the 1st percentile of the intensities to 0 and the 99.8th percentile to 1. We use percentiles instead of the minimum and maximum so that a handful of bright pixels do not affect the normalization. The values are not clipped, so the brightest pixels end up slightly above 1.

before normalization: 1 to 127: type uint8
after normalization: -0.01 to 1.33: type float32

Predicting the nuclei

predict_instances() runs the network and turns its output into objects. It returns two things:

  • labels: an image the same size as the input, with 0 for background and 1, 2, 3, ... for the individual nuclei, the same kind of label image that skimage.measure.label() gave us on day 1

  • details: the polygon coordinates and the probability of each detected object

found 29 nuclei
details contains: ['coord', 'points', 'prob']
<Figure size 1000x500 with 2 Axes>

What does StarDist predict?

details holds what the network predicted for each object:

  • points: the centre of each object

  • coord: the end points of the rays, which together form the polygon outline

  • prob: how sure the network is that this is an object

Below we draw the polygons on the image. With show_dist=True the rays from the centre to the boundary are drawn as well.

<Figure size 1000x500 with 2 Axes>

Let’s try with some other images

We have a few images from an experiment studying the formation of DNA repair foci in the nucleus after radiations. Cells were either irradiated (IR) or left untreated (control), fixed 2 hours later, and imaged in two channels:

  • channel 0: DNA damage foci (anti-RAD51 antibody) the bright dots inside the nuclei

  • channel 1: the nuclei (DAPI)

For now we only use the nuclei channel. We will later try to also detect the foci, where we count how many of them sit in each nucleus.

(2, 1024, 1024)
<Figure size 1000x500 with 2 Axes>

The foci in the first channel are not very clear to see. You could check the maximum intensity in the image with img_foci.max(). Then you could use the argument vmax=50 at imshow() at the right place in the code above to scale the maximum intensity shown.

A first attempt

Let us do exactly what we did before: normalize, then predict.

found 238 nuclei
<Figure size 700x700 with 1 Axes>

Improving the result

That is far too many objects, the results show that single nuclei are cut into several pieces.

The reason is the size of the nuclei. The network learned what a nucleus looks like at the pixel size of its training images. In the example image above the nuclei are about 15 pixels across. Our nuclei are around 80 pixels across. Inside the nuclei are nucleoli and those are more the size the model expects.

So we will show the image to the network at a smaller scale. predict_instances() has a scale argument for this: it downscales the image, predicts, and converts the labels back onto the original pixel grid, so labels has the same shape as the input image.

found 38 nuclei
label image shape: (1024, 1024)  (same as the input: (1024, 1024))
<Figure size 1200x600 with 2 Axes>

Exercise: What happens if you do not normalize?

Run predict_instances() on img_nuclei without normalizing it first (keep scale=0.5), and plot the result next to the normalized one.

  • How many objects do you get?

  • Which objects did it find instead of the nuclei?

Exercise: Which scale works best?

We picked scale=0.5 without checking. Try a few values (for example 0.25, 0.4, 0.5, 0.75). You could try and plot the number of detected objects against the scale.

  • Which scale gives a sensible result in the overlay?

  • You could try to load another image of the data set and check that image as well.

Next

We now have nuclei as a label image. In the next notebook we do the same job with Cellpose, which uses a different trick to find objects and asks for the object size in a different way.

Extra: multiplex image

Here we explore how to use StarDist on large 2D images. For example with this multiplex image provided as example in the QuPath software.

Download the file LuCa-7color_[13860,52919]_1x1component_data.tif from https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_fields/ Save it in the data folder.

(8, 1400, 1868)
<Figure size 1200x600 with 8 Axes>

Running stardist or any other AI model on large images (either 2D or 3D) can consume a lot more computer memory and often cannot be done at once. We can therefore split the image up into pieces. We create the label maps of the separate ‘tiles’ with some overlap and put them together again in the end. In StarDist this is done with the n_tiles argument.

100%|█████████████████████████████████████████████████████████████| 16/16 [00:02<00:00,  5.36it/s]
<Figure size 2000x1000 with 2 Axes>

As you can see some of the proteins in the channels above are in the cytoplasm. If we want to measure the intensity in the cytoplasm we can use label expansion: we grow each label by a certain distance, or until it touches another cell. scikit-image has a function for this called expand_labels(). Then we remove the nucleus from each expanded label, so only a ring of cytoplasm around each nucleus is left.

<Figure size 2000x1000 with 2 Axes>
<Figure size 1600x800 with 8 Axes>

Note: If you would like to explore this further you could also download an H&E image from the link above. StarDist also has a model that can segment nuclei from brightfield images.

This analysis is done with an image of 80 MB, however when images get much larger we should think of ways to not store all data in the computer memory at once, for example by storing data in OME-zarr format. This helps to only read the part of the data that we need at that moment or using the dask library to manage the loading of the data in an efficient way.

Here is a notebook that shows these options: examples/other2D/predict_big_data.ipynb

3D? StarDist also has models for 3D data. You can explore the possibilities here: https://github.com/stardist/stardist/tree/main/examples