Producing figures with multi-channel images

The microplot module contains tools to create figures containing multi-channel images. It offers basic tools to turn 2D images into RGB images using specific color maps, combine multiple channels, create figures, adding labels and scale bars.

#%load_ext autoreload
#%autoreload 2
import matplotlib.pyplot as plt
import numpy as np
import skimage.io

from microfilm import microplot

Basic plotting with microshow

The microshow function should be seen as an “enhanced” version of the Matplotlib imshow function which specifically deals with representing multi-channel images. In this first section, we see how to use that function and apply basic customizations.

Load images

The microshow can take as input a Numpy array or a list of images (more on input format in the Image inputs section). Here we load a multi-dimensional image as a Numpy array, and also extract each channel as an individual stack:

image = skimage.io.imread('../demodata/coli_nucl_ori_ter.tif')
image.shape
(3, 30, 220, 169)

This image has 30 time points and 3 channels. We take now just the time t=10:

multi_channel = image[:,10,:,:]
multi_channel.shape
(3, 220, 169)

We also isolate each channel:

image1 = multi_channel[0]
image2 = multi_channel[1]
image3 = multi_channel[2]
print(f'image shape: {image1.shape}')
print(f'image type: {image1.dtype}')
image shape: (220, 169)
image type: uint16

Creating a default plot

We can use the microshow function to plot our multi-channel image. If no options are passed, by default the image will be represented using the the color-blind friendly magenta-cyan-yellow color combination:

microim = microplot.microshow(images=multi_channel)
../_images/create_plots_12_0.png

However we can easily adjust the color maps or lookup tables with the cmaps option where we can use any Matplotlib colormap or additionally pure_red, pure_green, pure_blue, pure_cyan, pure_yellow and pure_magenta which provide classic black-to-color color maps:

microim = microplot.microshow(images=multi_channel, cmaps=['pure_blue', 'pure_red', 'pure_green'])
../_images/create_plots_14_0.png

2D Image inputs

As shown above, we can use a simple Numpy array as input. The dimension ordering is expected to be CXY i.e. Channel/X/Y. You can use np.swapaxis or np.moveaxis for example to change the dimensions to match this requirement. If you pass a Numpy array, all channels are plotted and therefore you need to provide the corresponding number of color maps (if not using default RGB).

Alternatively you can also pass lists of images. This is useful if for example you don’t want to plot all channels of a Numpy array. For example you could use the following to only plot two out of the three channels:

microim = microplot.microshow(images=[multi_channel[1], multi_channel[2]])
../_images/create_plots_16_0.png

3D image inputs

You also have the possibility to represent 3D data as a projection (to reperesent 3D data as volumes, we recommend using napari. In this case, you need to provide either a Numpy array of dimensions CZXY or a list of 3D images of dimensions ZXY. As microshow cannot know if you want to plot multiple channels or a volume (both arrays have three dimensions), if you want to plot a volume you have two choices. You can pass an additional key-word volume_proj where you have the choices min, max, mean or you can use the volshow function which is identical to microshow but sets volume_proj='max' by default. As an example we load here the following dataset, composed of 20 time points, 11 slices and 2 channesl:

image3d = skimage.io.imread('../microfilm/dataset/tests/test_folders/coli_nucl_ori_3d.tif')
image3d.shape
(2, 20, 11, 200, 200)
microplot.microshow(image3d[:,0,:,:,:], volume_proj='mean');
../_images/create_plots_19_0.png
microplot.volshow(image3d[:,0,:,:,:]);
../_images/create_plots_20_0.png

Options at creation

When we create the image, we have a large set of options that we can use to adjust the plot. For example, in addition to selecting specific color maps with cmaps, we can also choose a color projection type, add a scale bar and a label etc.:

microim = microplot.microshow(images=[image1, image2], cmaps=['pure_cyan','pure_magenta'], proj_type='sum',
                              unit='um', scalebar_unit_per_pix=0.065, scalebar_size_in_units=4,
                              scalebar_font_size=15, scalebar_thickness=0.02,
                              label_text='A', label_font_size=30);
../_images/create_plots_22_0.png

We can also add the channel labels as titles:

microim = microplot.microshow(images=[image1, image2], cmaps=['pure_cyan','pure_magenta'],
                              channel_label_show=True, channel_label_size=0.05,
                              channel_names=['CFP', 'RFP'])
../_images/create_plots_24_0.png

Colorbar

If you plot an image with a single channel, you can add a colorbar to your plot. The limits of intensities correspond to the limits usde during plotting and specified e.g. with the limits and rescale_type options:

micro_colorbar = microplot.microshow(images=[image1], cmaps=['pure_cyan'],
                              channel_label_show=True, channel_label_size=0.05,
                              channel_names=['CFP'], show_colorbar=True)
../_images/create_plots_26_0.png

Microimage object

When calling microshow, actually a Microimage instance is returned, which gives access to both specific functions of that instance as well as to the underlying Matplotlib objects.

Microimage methods

In the examples above, we always passed all inputs at time of figure creation. Alternatively to this, one can also first create a Microimage object and then use its attached methods such as add_label and add_scalebar to modify the plot. This can make the code slightly more readable:

microim = microplot.microshow(images=[image1, image2], cmaps=['pure_red', 'pure_green'])
microim.add_scalebar(unit='um', scalebar_unit_per_pix=0.065, scalebar_size_in_units=4,
                     scalebar_color='lightgreen', scalebar_font_size=15)
microim.add_label(label_text='A', label_font_size=30);
../_images/create_plots_29_0.png

Relation to Matplotlib

In the same spirit as e.g. the seaborn library, the microplot module stays very close to Matplotlib, so that you can integrate the image plots into larger figures. There are two ways in which microplot and Matplotlib are related.

First, the Microimage object gives access to the axis of the figure which allows you to use any Matplotlib customization on your plot. For example, you can turn the axes back on:

microim = microplot.microshow(images=[image1, image2])
microim.ax.set_axis_on()
../_images/create_plots_31_0.png

Second, you can integrate and microplot in existing Matplotlib figures again by using the axis. For that you can for example first create a subplot and re-use the axis reference as parameter in microshow to integrate the plot in that figure:

fig, ax = plt.subplots(1,2)
microim = microplot.microshow(images=[image1, image2], ax=ax[1]);
../_images/create_plots_33_0.png

We can then of course use these approaches together. For example, we can first create Matplotlib subplots, add the micrplot in that figure, and then add labels to each microplot:

fig, ax = plt.subplots(1,2)
micro1 = microplot.microshow(images=[image1, image2], cmaps=['pure_blue', 'pure_green'],
                              scalebar_unit_per_pix=0.065, scalebar_size_in_units=4, unit='um', 
                    scalebar_font_size=15, scalebar_color='white', ax=ax[0]);
micro2 = microplot.microshow(images=[image1, image3], cmaps=['pure_blue', 'pure_red'],
                              scalebar_unit_per_pix=0.065, scalebar_size_in_units=4, unit='um',
                    scalebar_font_size=None, scalebar_color='lightgreen', ax=ax[1])#, scalebar_kwargs={'scale_loc':'top'});

micro1.add_label('A', label_location='upper left', label_font_size=20, label_color='orange')
micro2.add_label('B', label_location='lower left', label_font_size=30);
../_images/create_plots_35_0.png

Copying figures

Matplotlib makes it impossible to re-use a given plot in multiple figures. Once you have created a microplot with annotations you might however want to re-use it in another larger figure. The microimage object allows you to do that since it stores all the relevant information and can easily be recreated. For example let’s create a plot:

micro1 = microplot.microshow(images=[image1, image2], cmaps=['pure_blue', 'pure_green'],
                             scalebar_unit_per_pix=0.065, scalebar_size_in_units=4, unit='um',
                             scalebar_font_size=15, scalebar_color='white');
../_images/create_plots_37_0.png

And now we create a new figure with plt.subplots() and we can add our plot from above to it using the update method:

fig2, ax2 = plt.subplots()
newmicro1 = micro1.update(ax=ax2, copy=True)
newmicro1.add_label('A');
../_images/create_plots_39_0.png

As an alternative to creating subplots yourself, you can also create a Micropanel object and add to it a series of microimages. Find more information in this section.

Lower level functions

When calling microshow, several internal functions are used, e.g. to rescale the image intensity, combined multiple colormaps etc. You also have access to these functions individually from the colorify module.

from microfilm import colorify

Convert image to display with chosen colormap

Using the colorify_by_name function, you can turn a single 2D array into a RGB image with a certain colormap. You can use any colormap from Matplotlib. Additionally, since Matplotlib doesn’t offer plain colormaps from black to a given color (used very often in microcopy), you can also specify one of pure_green, pure_red, pure_blue, pure_cyan, pure_magenta, pure_yellow:

image2
array([[ 98,  99, 100, ..., 100, 100, 100],
       [ 99,  99,  99, ..., 100,  99,  99],
       [ 99,  98,  99, ...,  99,  99,  99],
       ...,
       [100, 100, 100, ...,  99,  99,  99],
       [100,  99,  99, ...,  99,  99,  99],
       [ 99,  99,  99, ...,  99, 100, 100]], dtype=uint16)
im_green, cmap, min_max = colorify.colorify_by_name(image2, cmap_name='pure_green', flip_map=False, rescale_type='min_max')
plt.imshow(im_green);
../_images/create_plots_45_0.png

By default the image intensity is rescaled using the min-max values. This is often sub-optimal (e.g. if single pixels are way out of the distribution) and you can specify another rescaling. For example you can explicitly provide lower and upper limits:

im_green, cmap, min_max = colorify.colorify_by_name(image2, cmap_name='pure_green', flip_map=False, rescale_type='limits',
                                       limits=[100,105])
plt.imshow(im_green);
../_images/create_plots_47_0.png

colormap hex

Alternatively to the keyword based map, you can also use a hexadecimal encoding of a color to create an colormap ranging from black to that color. This gives more flexibility:

heximage, cmap, min_max = colorify.colorify_by_hex(image1, cmap_hex='#00ffff')
plt.imshow(heximage);
../_images/create_plots_49_0.png

Combining images

After having converted images to a given colormap, you can combine them into a single image with overlayed colors. Currently you can use a maximum projection (default) or a sum to replicate the Fiji behavior. You can simply use the combine_image function for that:

im1_mapped, _, _ = colorify.colorify_by_name(image1, cmap_name='pure_red')
im2_mapped, _, _ = colorify.colorify_by_hex(image2, cmap_hex='#00ffff')


im1_mapped_b, _, _ = colorify.colorify_by_name(image1, cmap_name='pure_red')
im2_mapped_b, _, _ = colorify.colorify_by_name(image2, cmap_name='pure_green')

combined = colorify.combine_image([im1_mapped, im2_mapped])
combined_b = colorify.combine_image([im1_mapped_b, im2_mapped_b])

fig, ax = plt.subplots(1,2)
ax[0].imshow(combined)
ax[1].imshow(combined_b);
../_images/create_plots_51_0.png

In the plots below, we compare the two available projections, maximum and sum. The difference is mostly visible when using colormaps that have large overlap, e.g. here with summer and cool. With pure_green and pure_red there would for example be no visual difference.

im1_mapped_b, _, _ = colorify.colorify_by_name(image1, cmap_name='cool')
im2_mapped_b, _, _ = colorify.colorify_by_name(image2, cmap_name='summer')

combined = colorify.combine_image([im1_mapped_b, im2_mapped_b], proj_type='max')
combined_b = colorify.combine_image([im1_mapped_b, im2_mapped_b], proj_type='sum')

fig, ax = plt.subplots(1,2, figsize=(7,7))
ax[0].imshow(combined)
ax[1].imshow(combined_b);
../_images/create_plots_53_0.png

Direct conversion

To save some steps you can also directly use a list of images and of colormaps to create a combined image:

converted, _, _ = colorify.multichannel_to_rgb(images=[image1, image2], cmaps=['pure_red', 'pure_green'], flip_map=[True, False])
plt.imshow(converted);
../_images/create_plots_55_0.png

Most of these functions also work directly with Numpy arrays:

converted, _, _ = colorify.multichannel_to_rgb(images=[image1, image2], cmaps=['magma', 'pure_green'])
plt.imshow(converted);
../_images/create_plots_57_0.png

The scalebar

We have seen above, that you could add a scalebar to the image. These scalebars are added via the matplotlib-scalebar package.

The only parameters needed for the scalebar are the unit that you use e.g. um, the size per pixel unit_per_pixel e.g. 0.5um/pixels, and the size of the scale bar scalebar_size_in_units in your unit e.g. 40um. In addition you can pass options like scalebar_color or scalebar_font_size to adjust the rendering. Note that you can pass a dictionary scalebar_kwargs with all possible options avaialble in the original ScaleBar object:

microim = microplot.microshow(images=[image1, image2], cmaps=['pure_blue', 'pure_green'],
                              unit='um', scalebar_unit_per_pix=0.065, scalebar_size_in_units=4,
                              scalebar_color='lightgreen', scalebar_font_size=15,
                              scalebar_kwargs={'scale_loc':'left'});
../_images/create_plots_59_0.png

Export

As the figure is simply a Matplotlib figure, you can just use the regular savefig function (and use this trick to avoid white space around the plot). You could access it via microim.fig.savefig but we also directly wrap it here in the microim object:

microim = microplot.microshow(images=[image1, image2], cmaps=['pure_blue', 'pure_green'],
                              unit='um', scalebar_unit_per_pix=0.065, scalebar_size_in_units=4,
                              scalebar_color='lightgreen', scalebar_font_size=15);
microim.savefig('single.png', bbox_inches = 'tight', pad_inches = 0, dpi=600)
../_images/create_plots_61_0.png

And for more complex plots, you can just use the regular figure creation:

fig, ax = plt.subplots(1,2)
micro1 = microplot.microshow(images=[image1, image2], cmaps=['pure_green', 'pure_blue'],
                             scalebar_unit_per_pix=0.065, scalebar_size_in_units=4, unit='um',
                             scalebar_font_size=15, scalebar_color='white', ax=ax[0]);
micro2 = microplot.microshow(images=[image1, image2], cmaps=['pure_red', 'pure_blue'],
                             scalebar_unit_per_pix=0.065, scalebar_size_in_units=4, unit='um',
                             scalebar_font_size=None, scalebar_color='lightgreen', ax=ax[1]);

micro1.add_label('A', label_location='upper left', label_font_size=20, label_color='orange')
micro2.add_label('B', label_location='lower left', label_font_size=30);
../_images/create_plots_63_0.png
fig.savefig('figure.png', bbox_inches = 'tight', pad_inches = 0, dpi=600)
fig.savefig('figure.pdf', bbox_inches = 'tight', pad_inches = 0, dpi=600)