Read and Visualize a WSI¶

Click to open in: [GitHub][Colab]

About this demo¶

This demo reads a whole slide image (WSI) using tiatoolbox. We load a sample WSI, gather some key information, and then extract some image patches. We demo our modules wsireader details and slide_info details.

Set Up Plotting¶

Here we import matplotlib and set some options for plotting results to make the output look nice.

"""Import modules required to run the Jupyter notebook."""

# Clear logger to use tiatoolbox.logger
import logging

if logging.getLogger().hasHandlers():
    logging.getLogger().handlers.clear()

# Pretty print for nice dictionary printing
from pprint import pprint

import matplotlib as mpl
import matplotlib.pyplot as plt

from tiatoolbox import logger
from tiatoolbox.data import small_svs
from tiatoolbox.wsicore.wsireader import WSIReader

mpl.rcParams["figure.dpi"] = 150  # for high resolution figure in notebook
mpl.rcParams["figure.facecolor"] = "white"  # To make sure text is visible in dark mode
plt.rcParams.update({"font.size": 5})

Reading in a WSI¶

We load a small WSI, specified with the help of the data module. Here we include a function to download a small sample SVS image from our server. This function then returns the path to the downloaded file which is stored in your system’s temporary directory, e.g. /tmp on UNIX-like systems.

file_path = small_svs()
logger.info(file_path)
Hide code cell output
|2023-07-27|09:01:29.461| [INFO] /tmp/CMU-1-Small-Region.svs

Our code shields the user from the incompatible formats produced by different models of scanners from different vendors. The function WSIReader.open has as input a particular WSI, with a particular image format, and outputs an object reader, whose base class is WSIReader, and whose derived class depends on the image format (see details). The reader reader provides important information about the WSI. Member functions obtain pixel- or patch-level information, using format-independent code, as we illustrate below.

reader = WSIReader.open(file_path)
print(reader)  # noqa: T201
<tiatoolbox.wsicore.wsireader.OpenSlideWSIReader object at 0x7a54a3f5ddb0>

First, let’s check the basic WSI information, such as magnification, dimension, etc. (mpp = microns per pixel).

info_dict = reader.info.as_dict()
pprint(info_dict)  # noqa: T203
{'axes': 'YXS',
 'file_path': PosixPath('/tmp/CMU-1-Small-Region.svs'),
 'level_count': 1,
 'level_dimensions': ((2220, 2967),),
 'level_downsamples': [1.0],
 'mpp': (0.499, 0.499),
 'objective_power': 20.0,
 'slide_dimensions': (2220, 2967),
 'vendor': 'aperio'}

Thumbnail¶

To see a thumbnail of the WSI, we use the slide_thumbnail method of reader. We load the thumbnail at $\times1.25$ objective power as follows:

thumbnail = reader.slide_thumbnail(resolution=1.25, units="power")
plt.imshow(thumbnail)
plt.axis("off")
plt.show()
../../_images/82420247c8d7cb6d65b01d12149b3c22b0fae5157490695b38bf2fbb0f3cf363.png

Reading WSI regions¶

i) Read Rect¶

We can read a region of the whole slide image at a given location and size using the read_rect method. Location can be stated as (x, y) tuple giving the top left pixel in the baseline (level 0) reference frame, and size as (width, height) tuple giving the desired output image size.

Reads can be performed at different resolutions by supplying a pair of arguments for the resolution and the units of resolution. resolution can be given as integer or float or tuple(float), default = 0. Either a single number or a sequence of two numbers for x and y as tuple are valid. This value is in terms of the corresponding units. For example: resolution=0.5 and units=’’mpp’’ will read the slide at 0.5 microns per-pixel, and resolution=3, units=’’level’’ will read at level at pyramid level / resolution layer 3. Supported units are: microns per pixel (‘’mpp’’), objective power (‘’power’’), pyramid / resolution level (‘’level’’), pixels per baseline pixel (‘’baseline’’), default =’’level’’.

# Location coordinates in (x,y)
location = (1000, 1000)

# Size of the region in (width, height)
size = (256, 256)

# read the region using wsi reader's read rect at 0.5 mpp
img = reader.read_rect(
    location,
    size,
    resolution=0.5,
    units="mpp",
)

plt.imshow(img)
plt.axis("off")
plt.show()
../../_images/688b7746b8220143147c5a6d2249706c14cc88ed134eca844f82e669d1998041.png

ii) Read bound¶

Read a region of the whole slide image within given bounds. Bounds is a tuple of (start_x, start_y, end_x, end_y) i.e., (left, top, right, bottom) of the region in baseline reference frame. However, with coord_space=’’resolution’’, the bound is expected to be at the requested resolution system.

Reads can be performed at different resolutions by supplying a pair of arguments for the resolution and the units of resolution. resolution can be given as integer or float or tuple(float), default = 0. Either a single number or a sequence of two numbers for x and y as tuple are valid. This value is in terms of the corresponding units. For example: resolution=0.5 and units=’’mpp’’ will read the slide at 0.5 microns per-pixel, and resolution=3, units=’’level’’ will read at level at pyramid level / resolution layer 3. Supported units are: microns per pixel (‘’mpp’’), objective power (‘’power’’), pyramid / resolution level (‘’level’’), pixels per baseline pixel (‘’baseline’’), default =’’level’’.

coord_space = ‘’baseline’’ by default, this is a flag to indicate if the input bounds is in the baseline coordinate system (‘’baseline’’) or is in the requested resolution system (‘’resolution’’).

# Specify the bounds in terms of rectangle (left, top, right, bottom)
bounds = [1000, 1000, 2000, 2000]

# Read the region using wsi reader's read bounds at level 0
img = reader.read_bounds(bounds, resolution=0, units="level")

plt.imshow(img)
plt.axis("off")
plt.show()
../../_images/34be77f2a97dcb0ff80d7f73973ed3f3b582273eeeb90e07d1eac490bc63bee4.png

Tissue Masking¶

It is often useful to distinguish between tissue and glass (no tissue) in the image. For example, to avoid running computation expensive analysis for regions where is no tissue content. We compute a mask, by which we mean a binary colouring of the pixels to either black=glass or white=tissue. The white area is deliberately made a little larger than tissue area, as this will be appropriate for our task. We provide a function tissue_mask on a WSIReader binarises the thumbnail on the basis of pixel intensity, using the Otsu method. Morphological operations improve the result.

Note that the tissue_mask function returns a VirtualWSIReader object rather than a NumPy array (although this can be accessed via the .image attribute). The returned VirtualWSIReader object also has some metadata copied from the original WSI. This allows the lower resolution mask image to be read as it it were the full size WSI. This is particularly useful as it allows for using the same coordinates to read from the mask as from the original WSI.

mask_reader = reader.tissue_mask(resolution=4, units="mpp")
mask_thumbnail = mask_reader.slide_thumbnail(resolution=1.25, units="power")

plt.imshow(mask_thumbnail)
plt.show()
../../_images/769075cf550a11475fdc9f3152c8981490eb26e36ebe29422566c49f740bf1f2.png
mask_reader = reader.tissue_mask("morphological", resolution=4, units="mpp")
mask_thumbnail = mask_reader.slide_thumbnail(resolution=1.25, units="power")

plt.imshow(mask_thumbnail)
plt.show()
../../_images/402be6e907e21f5a4525797fc8d7df598df5488d107ad6459b1074c6d878806d.png

Here we show reading part of the tissue from reader and also the corresponding part of the mask.

tissue_region = reader.read_rect(
    location=(1500, 1500),
    size=(500, 500),
    resolution=0.5,
    units="mpp",
)
mask_region = mask_reader.read_rect(
    location=(1500, 1500),
    size=(500, 500),
    resolution=0.5,
    units="mpp",
)
_, axs = plt.subplots(1, 2)
axs[0].imshow(tissue_region)
axs[1].imshow(mask_region, vmin=0, vmax=1)
plt.show()
../../_images/3029cc9c14707f2c4dbad07ab71c6247f866a103d1076257b27ff1f0934de49e.png

Notice that the above two function calls to the two reader objects have identical arguments. This could also be written more concisely as follows:

# Try modifying the parameters and see how the mask changes
location, size = (1500, 1500), (500, 500)
tissue_region, mask_region = (
    r.read_rect(location, size, 0.5, "mpp") for r in (reader, mask_reader)
)
_, axs = plt.subplots(1, 2)
axs[0].imshow(tissue_region)
axs[1].imshow(mask_region, vmin=0, vmax=1)
plt.show()
../../_images/3029cc9c14707f2c4dbad07ab71c6247f866a103d1076257b27ff1f0934de49e.png