The data¶
# Libraries
# Plotting libraries
import matplotlib.pyplot as plt
import seaborn as sns
# Library to load tiff files
import tifffile as tiff
# General math libraries
import numpy as np
from scipy import stats
# Image analysis libraries/modules
import skimage as sk
from scipy import ndimageTracking a kinase translocation reporter (KTR)¶
To assess the activity of kinases in single live cells, which can e.g. activate and deactivate proteins, researchers have developed kinase translocation reporters (KTRs). These are fluorescently tagged proteins (or protein domains) whose location inside the cell changes (translocates) depending on whether a specific kinase is active (see also figure 1 of Kudo et al.).
The paper Chavez-Abiega et al. presents an image analysis pipeline to analyze KTR signals from single cells (and uses it to study kinase behavior). We’ll use their data, and try to reproduce part of that analysis.

Diagram (left) and Fig 1b of Kudo et al. (right). When kinase activity is high, KTRs predominantly localize to the cytoplasm due to reduced nuclear localization signal and increased nuclear export signal, and vice versa.
Example of data analysis¶

Figure above. The data contain many timepoints. After observing the cells for some time, a stimulant was added (blue line) that activated a kinase was added. This is reflected by the fact that the ratio of cytoplasmic fluorescent intensity (C) over the nuclear fluorescent intensity (N) of the KTR sensor increases.
Loading KTR data¶
We do this the same way as before, but now we also load
the intensity data from the KTR sensor, which is
stored in the 3rd (i=2) channel.
# Obtain data
img_nuclei = tiff.imread("images/KTR_t0.tif")[0,:,:]
img_KTR = tiff.imread("images/KTR_t0.tif")[2,:,:]
# Let's get our nuclei mask again using triangle method
thresh_triangle = sk.filters.threshold_triangle(img_nuclei)
mask_nuclei = img_nuclei > thresh_triangle
mask_nuclei_clean = sk.morphology.opening(mask_nuclei, footprint=sk.morphology.disk(3))
_ = plt.imshow(mask_nuclei_clean[0:200,0:200])
Now let’s also look at the data of the KTR sensor:
_ = plt.title("Fluorescent signal of KTR sensor")
_ = plt.imshow(img_KTR[0:200,0:200])
_ = plt.contour(mask_nuclei_clean[0:200,0:200], colors="w")
Data analysis aims¶
We’re going to reproduce a small part of the analysis from the original paper.
Specifically, we want to be able to:
Identify the nuclei
Identify representative cytoplasm
Calculate intensity ratios
Background processing and correction¶
When calculating ratios, a background signal has an undesired contribution to the calculated ratio.
As this is the case, we want to correct for background artifacts in the intensity image.
Different approaches for different challenges¶
Globally uniform background
Subtract a constant estimate of the background
Common approaches:
use the mode
average of known background region
Spatially varying background
Estimate the background pattern, which is likely more coarse-grained
Approaches:
Rolling ball
skimage.restoration.rolling_ball()returns the background → subtract it yourself
https://
scikit -image .org /docs /stable /auto _examples /segmentation /plot _rolling _ball .html
Top hat
skimage.morphology.white_tophat()returns the already-corrected image
https://
scikit -image .org /docs /stable /auto _examples /filters /plot _tophat .html
In case knowable/measurable background or shading effects (e.g. camera offset, uneven illumination)
Measure the artificial contribution
To counter camera offset ..
Dark image: image taken with no light, same exposure & camera settings
Alternative: blank image of an empty sample (e.g. slide + medium, no cells) → also captures glass/medium background
.. and uneven illumination
Flat image: image of a uniform sample (e.g. a dye solution), same optics/filters → captures location-dependent illumination
Combine as: corrected = (raw − dark) / (flat − dark) × mean(flat − dark)
Background subtraction using the mode¶
_ = plt.hist(img_KTR.ravel(), bins=100)
# Note that the data is discontinuous, perhaps indicating
# a data type conversion.
# calculating the mode, using stats module
the_mode1 = stats.mode(img_KTR.ravel())[0]
# calculate the mode, alternative method through bincount
the_mode2 = np.bincount(img_KTR.ravel()).argmax()
the_mode1, the_mode2(np.uint16(480), np.int64(480))_ = plt.hist(img_KTR.ravel(), bins=100)
_ = plt.axvline(the_mode1, color="r")
# Visualize what is considered background
img_KTR_crop = img_KTR[0:200, 0:200]
_ = plt.imshow(img_KTR_crop)
_ = plt.imshow(img_KTR_crop<the_mode1, alpha=(img_KTR_crop<the_mode1)*1.0, cmap="gray")
# Calculate the background subtracted image
img_KTR_corr = img_KTR.astype(float)-the_mode1
_ = plt.title("Background-corrected KTR intensity")
_ = plt.imshow(img_KTR_corr[0:200, 0:200])
_ = plt.title("Histogram of the background-corrected image")
_ = plt.hist(img_KTR_corr.ravel(), bins=100)
Obtaining a nuclear and cytoplasmic mask¶
To analyze the cytoplasm, we’ll take a small ring around the nucleus.
# Dilate to include area around the nucleus
mask_nuclei_clean_dilated = sk.morphology.dilation(mask_nuclei_clean, sk.morphology.disk(2))
# Now "subtract" the nucleus from the mask
mask_cytoplasm = mask_nuclei_clean_dilated & ~mask_nuclei_clean
_ = plt.imshow(mask_cytoplasm)
What’s the ratio?¶
overall_ratio = np.mean(img_KTR[mask_cytoplasm]) / np.mean(img_KTR[mask_nuclei])
overall_rationp.float64(0.5011731486917071)But we want single object resolution!¶
Let’s look at how to analyze separate ROIs in an image.
Masks, labels and regions¶
Encode different ROIs: the labeled map¶
We’d like the computer to encode different ROIs. This can be done by using arrays of integer numbers, where 0 is the background and 1 .. N different ROIs of interest.
How to make such a map: connected components¶
A common way to create such a mask is to base it on the binary mask.
Each foreground region (with a value of 1) that is surrounded by background (value 0), is given a unique number.
Let’s see this in action.
# Label connected components
mask_nuclei_labeled = sk.measure.label(mask_nuclei_clean, connectivity=1)
_ = plt.imshow(mask_nuclei_labeled)
print(mask_nuclei_labeled[0:20,0:20])[[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 23 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 23 23 23 23 23 23 23 23 23 0 0 0 0 0 0 0 0]
[ 0 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0]
[23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 0 0 0 0 0]]
print(mask_nuclei_labeled[30:50,35:55])[[ 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 49 49 49 49 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 49 49 49 49 49 49 49 49 0 0 0 0 0 0 0 0 0]
[ 0 49 49 49 49 49 49 49 49 49 49 49 0 0 0 0 0 0 0 0]]
As you can see, different ROIs can now be easily identified by their unique number.
_ = plt.title("Nucleus 23")
_ = plt.imshow((mask_nuclei_labeled==23)[0:200,0:200])
This is great, we can now analyse single nuclei.
For our dataset however, we also want to have cytoplasm ROIs. We can use morphology operations on the labeled map to obtain these.
# Obtain labeled rings
# Dilate to include area around the nucleus
mask_cytoplasm_labeled = sk.morphology.dilation(mask_nuclei_labeled, sk.morphology.disk(2))
# Now "subtract" the nucleus from the mask
mask_cytoplasm_labeled[mask_nuclei_clean] = 0
_ = plt.imshow(mask_cytoplasm_labeled)
Obtaining the C/N ratios¶
And we can use the labeled maps of the nuclei and cytoplasmic areas to access the corresponding KTR intensity values. This, in turn, allows us to calculate the ratio of KTR in cytoplasm versus the nuclei, which was what we’re interested in.
# Calculate ratios manually
ratios = []
for idx in range(1, np.max(mask_cytoplasm_labeled)):
ratios.append(np.mean(img_KTR[mask_cytoplasm_labeled==idx])/np.mean(img_KTR[mask_nuclei_labeled==idx]))
# And show
_ = plt.hist(ratios)
_ = plt.xlabel("Cytoplasm/Nucleus ratio")
_ = plt.ylabel("Times observed")
Properties of regions (“connected components”)¶
Often, during an analysis, we’ll be interested in properties of the ROIs (e.g., what size are the nuclei?).
regionprops is a convenient function that gives access to a host of information based on labeled maps.
# Get properties of each of the regions
rprops_nuclei = sk.measure.regionprops(mask_nuclei_labeled)
# Examples of properties we now have access to:
print(rprops_nuclei[0].bbox)
print(rprops_nuclei[0].area)
# see https://scikit-image.org/docs/0.25.x/api/skimage.measure.html#skimage.measure.regionprops for more information(0, 156, 22, 173)
270.0
# Visualizing an ROI
IDX = 100
the_bbox = rprops_nuclei[IDX].bbox
_ = plt.imshow(mask_nuclei_labeled[the_bbox[0]:the_bbox[2], the_bbox[1]:the_bbox[3]])
We can also map intensity data using the regionprops function. We can also calculate our ratios that way:
# Accessing intensity properties
rprops_KTR_nuclei = sk.measure.regionprops(mask_nuclei_labeled, intensity_image=img_KTR)
rprops_KTR_cyto = sk.measure.regionprops(mask_cytoplasm_labeled, intensity_image=img_KTR)
# Calculate the ratios
KTR_nuclei_intensities = [r.intensity_mean for r in rprops_KTR_nuclei]
KTR_cyto_intensities = [r.intensity_mean for r in rprops_KTR_cyto]
KTR_ratios = [c/n for c,n in zip(KTR_cyto_intensities, KTR_nuclei_intensities)]
# Calculate the ratios (alternative shorter method)
KTR_ratios2 = [rn.intensity_mean / cn.intensity_mean for rn, cn in zip(rprops_KTR_cyto, rprops_KTR_nuclei)]
print(KTR_ratios[0:10])
print(KTR_ratios2[0:10])[np.float64(0.5376960963574011), np.float64(0.5567022853975222), np.float64(0.5620778235060323), np.float64(0.5349040191788493), np.float64(0.5941152087702692), np.float64(0.5287349711068803), np.float64(0.5271857178227263), np.float64(0.8759388038942976), np.float64(0.4086213144636655), np.float64(0.5080422794117647)]
[np.float64(0.5376960963574011), np.float64(0.5567022853975222), np.float64(0.5620778235060323), np.float64(0.5349040191788493), np.float64(0.5941152087702692), np.float64(0.5287349711068803), np.float64(0.5271857178227263), np.float64(0.8759388038942976), np.float64(0.4086213144636655), np.float64(0.5080422794117647)]

Exercise: Is there a relationship between nuclear size and C/N ratio?¶
Can you make a scatter plot to investigate this?

Optional exercise: Can you measure bacterial sizes?¶
img_ecoli = np.invert(tiff.imread('images/microcolony_ecoli.tif'))
_ = plt.imshow(img_ecoli)
Can you think of ways to obtain one ROI (ie a labeled mask) for each of the bacteria? And then visualize the distribution of their sizes?
A labeled mask would e.g. look like the following image:

Note: this is probably rather challening. Don’t worry if you don’t manage.
- Kudo, T., Jeknić, S., Macklin, D. N., Akhter, S., Hughey, J. J., Regot, S., & Covert, M. W. (2017). Live-cell measurements of kinase activity in single cells using translocation reporters. Nature Protocols, 13(1), 155–169. 10.1038/nprot.2017.128
- Chavez-Abiega, S., Grönloh, M. L. B., Gadella, T. W. J., Bruggeman, F. J., & Goedhart, J. (2022). Single-cell imaging of ERK and Akt activation dynamics and heterogeneity induced by G-protein-coupled receptors. Journal of Cell Science, 135(6). 10.1242/jcs.259685