Image Processing III

(c) 2020 Justin Bois. This work is licensed under a Creative Commons Attribution License CC-BY 4.0. All code contained herein is licensed under an MIT license.

This document was prepared at Caltech with support financial support from the Donna and Benjamin M. Rosen Bioengineering Center.

This tutorial was generated from an Jupyter notebook. You can download the notebook here.


In [1]:
import glob

import numpy as np
import pandas as pd

import skimage.io

import bi1x

import bokeh.io
import bokeh.plotting

bokeh.io.output_notebook()

notebook_url = 'localhost:8888'
Loading BokehJS ...

In this tutorial, we will will work out an image processing pipeline for processing the time lapse movies of the delay oscillator. It is important that we all follow this pipeline exactly, because we will all share data.

Image segmentation

Movies of bacterial colonies growing and divided are typically segmented using more sophisticated techniques that what we used for the E. coli growth module. We do not have time to discuss these techniques, but will instead do segmentation by hand. To do this, you can use some Bokeh-based tools in the bi1x package. First, we can load in an image.

In [2]:
# Load in image
im = skimage.io.imread('test_oscillator_images/img_000000011_Brightfield_000.tif')

Next, we can record clicks to make an ROI, or region of interest. To do so, we use the bi1x.viz.draw_rois() function. Note the two keyword arguments in its function call below. The flip=False keyword argument keeps the coordinates of the image matrix-like, which is what we expect further down our image processing pipeline. Next, the notebook_url keyword argument specifies the URL of the notebook you are using. This is important to know so that the clicks are appropriately recorded. Once the function is called, a Bokeh plot appears with extra tools. To find out the URL, look in your browser. You will enter something like 'localhost:8888'. For convenience, I set a variable with the notebook URL in the import commands at the beginning of the notebook.

To select an ROI, select the polygon tool. To begin drawing an ROI, double-click the first part of the image. Each subsequent click designates a vertex of a polygon. To finish the polygon, double-click again. (In the HTML version of this notebook, the interactive image will not appear below.)

In [3]:
rois = bi1x.viz.draw_rois(im, flip=False, notebook_url=notebook_url)

The polygon is stored in a Bokeh object that we should convert to a Pandas DataFrame. To do so, use the bi1x.viz.roicds_to_df() function.

In [4]:
df_roi = bi1x.viz.roicds_to_df(rois)

df_roi
Out[4]:
roi x y
0 0 1070.364661 560.809713
1 0 1055.945732 563.736413
2 0 1046.600129 566.397048
3 0 1032.715234 569.589811
4 0 1022.034546 571.452256
5 0 1011.620875 572.516511
6 0 1007.348599 574.112892
7 0 1003.343341 579.966291
8 0 1004.411410 584.755435
9 0 1005.746496 588.214262
10 0 1010.018771 590.342770
11 0 1015.893150 590.608834
12 0 1025.238752 589.278516
13 0 1055.144680 584.223308
14 0 1067.694489 581.296609
15 0 1078.909212 578.369909
16 0 1082.647453 574.911083
17 0 1082.914470 570.121938
18 0 1080.778332 565.066730
19 0 1075.437988 561.075777

Finally, you should store the file in a text file with a name like img_000000000_Brightfield_000_cell_0.csv (see the naming convention below). For ease of loading in the ROI vertices in the next step, be sure to only write out columns 'x' and 'y' without a header of an index.

In [5]:
df_roi[["x", "y"]].to_csv(
    "img_000000011_Brightfield_000_cell_0.csv", header=None, index=False
)

Note that we will be doing our segmentation using the brightfield channel. It is generally a good idea to use a channel different from the one you are quantifying for segmentation. Then, you are not relying on a possible weak signal to locate the cells. Nonetheless, for the delay oscillator experiment, the fluorescence is almost always high enough to segment in the fluorescent channel.

Cell naming convention

You will segment cells from a single growing colony. It is important to keep track of the lineage of cells. To this end, we will use a unique binary identifier code for each cell in a time lapse movie. The binary code is constructed as follows.

  1. The first digit is the original mother, always 0.
  2. The next digit specifies which of the two daughter cells from the division of the original mother. After the first division, the left-or-top-most daughter cell gets a label of 0, and right-or-bottom-most is 1.
  3. Continue in this manner through the generations.

As an example, a third-generation cell that is the left-most daughter of a division that came from the right-most daughter of the original mother is 010.

You would store the coordinates, for example for frame number 15, as img_000000015_Brightfield_000_cell_010.csv. If you want to use the function that follows, the cell binary code must be between the last underscore and dot in the file name.

Compute the median fluorescence intensity

For each cell we segment, we want to compute the median fluorescence intensity. We want to compute the median, as opposed to total, intensity because as the cells grow, the plasmids multiply, so we are interested in an approximate per-plasmid fluorescence. We choose the median instead of the mean because the median is less sensitive to noise and to errors in segmentation (for example, making the boundary around a cell too large or too small.

Given the stored coordinates, we need to determine what pixels belong to a given cell. The bi1x.image.verts_to_roi() function does this. For example, let's consider the image img_000000011_Brightfield_000.csv with the set of vertices of a polygon defining an ROI stored in img_000000011_Brightfield_000_cell_01.csv. First, let's plot the image with the stored vertices. I will use the flip=False kwarg when I show the image using bi1x.viz.imshow() because we stored the ROIs with the origin at the top left of the image. We also keep the units in pixels because this is the units that the coordinates of the segmentation are stored in.

In [6]:
# Load in image
im = skimage.io.imread(
    "test_oscillator_images/img_000000011_Brightfield_000.tif"
)

# Load in the vertices defining the ROI (note comma delimiter)
verts = np.loadtxt(
    "test_oscillator_images/img_000000011_Brightfield_000_cell_01.csv",
    delimiter=",",
)

# Display the image
p = bi1x.viz.imshow(im, flip=False)
p.line(*verts.T, color="orange")
bokeh.io.show(p)