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.

Detecting spots with Spotiflow

Adapted from the Spotiflow inference example.

StarDist and Cellpose gave us the outline of the nuclei. For the repair foci in the other channel we do not need an outline: they are small and round, and we only want to know where they are and how many of them are there.

Spotiflow predicts one coordinate per spot instead of a mask. When two spots are close together it can detect them as two spots.

In this notebook we detect the foci, combine them with the nuclei from the previous notebook, and count foci per nucleus in all four images.

Libraries

/var/home/maartenpaul/miniforge3/envs/2026_deep_learning/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Detecting lysosomes in HeLa cells

Before we go to the repair foci, we try Spotiflow on an image we already know: the HeLa cells from the Cellpose notebook. The first channel shows lysosomes, which look like bright dots in the cytoplasm.

<Figure size 800x600 with 1 Axes>

Spotiflow has several pretrained models; general is the one to start with. predict() returns the spot coordinates as (y, x) pairs, plus some details we do not need here.

INFO:spotiflow.model.spotiflow:Loading pretrained model: general
INFO:spotiflow.model.spotiflow:Will use device: cpu
INFO:spotiflow.model.spotiflow:Predicting with prob_thresh = [0.49999999999999994], min_distance = 1
INFO:spotiflow.model.spotiflow:Peak detection mode: fast
INFO:spotiflow.model.spotiflow:Image shape (512, 672)
INFO:spotiflow.model.spotiflow:Predicting with (1, 1) tiles
INFO:spotiflow.model.spotiflow:Normalizing...
INFO:spotiflow.model.spotiflow:Padding to shape (512, 672, 1)
INFO:spotiflow.model.spotiflow:Found 317 spots
found 317 spots
[[481.62335059 279.910313  ]
 [480.03798322 261.92595756]
 [477.06102252 354.63298368]
 [474.25396052 297.89784578]
 [474.01694293 228.01091557]]
<Figure size 800x600 with 1 Axes>

Zoom in on one cell to check the detections.

<Figure size 700x700 with 1 Axes>

Most bright dots are found. Very large, bright blobs are sometimes skipped or get a single spot: Spotiflow looks for small, round spots of a few pixels wide.

What does Spotiflow predict?

The network does not output the points directly. It predicts a heatmap: an image that is bright where a spot is likely to be. The points are the peaks of this heatmap that are higher than a threshold. details holds this heatmap, and in details.prob the value at each spot, the confidence of the detection.

lowest confidence of a detected spot: 0.50
<Figure size 1200x600 with 2 Axes>

Detecting the foci

Now the foci data. We use the first channel of the irradiated image, the one with the DNA damage foci. We can reuse the model we already loaded.

<Figure size 700x700 with 1 Axes>
INFO:spotiflow.model.spotiflow:Will use device: cpu
INFO:spotiflow.model.spotiflow:Predicting with prob_thresh = [0.49999999999999994], min_distance = 1
INFO:spotiflow.model.spotiflow:Peak detection mode: fast
INFO:spotiflow.model.spotiflow:Image shape (1024, 1024)
INFO:spotiflow.model.spotiflow:Predicting with (1, 1) tiles
INFO:spotiflow.model.spotiflow:Normalizing...
INFO:spotiflow.model.spotiflow:Padding to shape (1024, 1024, 1)
INFO:spotiflow.model.spotiflow:Found 1709 spots
found 1709 spots
<Figure size 800x800 with 1 Axes>

Hard to judge at this size, so let us zoom in on one nucleus.

<Figure size 700x700 with 1 Axes>

Which nucleus does each spot belong to?

We segment the nuclei exactly as in the Cellpose notebook. The label image tells us, for every pixel, which nucleus it belongs to — so we can simply look up the label at the position of each spot.

/var/home/maartenpaul/miniforge3/envs/2026_deep_learning/lib/python3.12/site-packages/torch/jit/_script.py:365: FutureWarning: `torch.jit.script_method` is deprecated. Please switch to `torch.compile` or `torch.export`.
  warnings.warn(
/var/home/maartenpaul/miniforge3/envs/2026_deep_learning/lib/python3.12/site-packages/cellpose/dynamics.py:760: UserWarning: Sparse invariant checks are implicitly disabled. Memory errors (e.g. SEGFAULT) will occur when operating on a sparse tensor which violates the invariants, but checks incur performance overhead. To silence this warning, explicitly opt in or out. See `torch.sparse.check_sparse_tensor_invariants.__doc__` for guidance.  (Triggered internally at /__w/pytorch/pytorch/aten/src/ATen/Context.cpp:848.)
  coo = torch.sparse_coo_tensor(pt, torch.ones(pt.shape[1], device=pt.device, dtype=torch.int),
35 nuclei
1427 of 1709 spots are inside a nucleus
foci per nucleus: [ 71  70   0  60   1   9  80  84   2  43   7  22  13  12  86  27  39 102
   2  66   2  19  39  85  58  63  36  97  40  53   8  17  61  31  22]
median: 39
<Figure size 640x480 with 1 Axes>

All four images

We have two irradiated and two control images. Rather than copying the code four times, we can create a function and call it for each image.

What this function does: it detects the spots and the nuclei. Then it reads for every spot which nucleus it belongs to, by checking the value of the label image at that location. Finally it returns only the counts of the spots inside the nuclei: counts[1:] drops label 0, the background.

170 nuclei in total
Loading...
Loading...
<Figure size 640x480 with 1 Axes>

The irradiated cells have many more foci per nucleus than the control cells. That is what we expect: irradiation causes DNA breaks, and the foci are repair proteins gathering at those breaks.

Exercise: How many spots does Spotiflow find outside the nuclei?

We saw that some spots are not inside any nucleus.

  • How many are there, and where are they in the image?

  • Plot them on top of the image and have a look. Are they real foci, and would it be a problem to leave them out?

Exercise: What does the detection threshold change?

predict() takes a prob_thresh argument: the confidence a spot needs before it is kept. The default is the value the model was tuned with (0.5 for the general model).

Run the prediction on img_foci with prob_thresh set to 0.3, 0.5 and 0.7, and plot the number of detected spots. Look at the zoomed-in region from earlier for each setting: which spots appear and disappear?

Bonus exercise: Nuclei on the edge of the image

Some nuclei touch the edge of the image, so we only see part of them. Those nuclei get too few foci and pull down the counts.

Change count_foci_per_nucleus() so it leaves out the nuclei that touch the border, and run the loop over the four images again. sk.segmentation.clear_border(masks) sets those nuclei to 0 (you need import skimage as sk first). Also after removing the labels on the border we can use segmentation.relabel_sequential() to relabel the the labels without gaps.

  • How many nuclei are left?

  • Does the mean number of foci per nucleus change?

Wrapping up

We used three pretrained models without training anything ourselves:

  • StarDist — nuclei as star-convex polygons

  • Cellpose — cells and nuclei from predicted flows

  • Spotiflow — spots as coordinates