
Skeleton analysis of a drosophila trachea#
In this notebook, we will study the elongated branching structure of a drosophila trachea in a 3D confocal image. We will introduce vesselness filters, skeletonization and show how the connected filaments structure can be represented as a graph of nodes and vertices using the Skan package in Python.
Acknowledgements
We kindly acknowledge Lemaitre lab in EPFL for providing the data for this notebook!
Setup#
Check that you have all the necessary packages installed, including napari and Skan. If not, you can use the ! symbol to install them directly from the Jupyter notebook (otherwise, you can use your terminal).
import napari
from skan import Skeleton, summarize
Get the data#
The image we’ll use in this tutorial is available for download on Zenodo (drosophila_trachea.tif).
In the cell below, we use a Python package called pooch to automatically download the image from Zenodo into the data folder of this repository.
import pooch
from pathlib import Path
data_path = Path('.').resolve().parent / 'data'
fname = 'drosophila_trachea.tif'
pooch.retrieve(
url="https://zenodo.org/record/8099852/files/drosophila_trachea.tif",
known_hash="md5:d595ac271779936e255afd0508cca43f",
path=data_path,
fname=fname,
progressbar=True,
)
print(f'Downloaded image {fname} into: {data_path}')
Read the image#
We use the imread function from Scikit-image to read our TIF image.
from skimage.io import imread
image = imread(data_path / 'drosophila_trachea.tif')
print(f'Loaded image in an array of shape: {image.shape} and data type {image.dtype}')
print(f'Intensity range: [{image.min()} - {image.max()}]')
If you run into troubles, don’t hesitate to ask for help 🤚🏽.
Load the image into Napari#
Let’s open a viewer and load our image to have a look at it.
viewer = napari.Viewer(ndisplay=3)
viewer.add_image(image, name="Original image")
Intensity normalization#
Let’s rescale our image to the range 0-1. By doing so, it is also converted to an array of data type float.
from skimage.exposure import rescale_intensity
image_normed = rescale_intensity(image, out_range=(0, 1))
print(f'Intensity range: [{image_normed.min()} - {image_normed.max()}]')
print(f'Array type: {image_normed.dtype}')
Vesselness filtering#
Vesselness filters are designed to highlight elongated structures in an image. Here, we apply the meijering filter from Scikit-image to our normalized image.
from skimage.filters import meijering
image_mei = meijering(image_normed, sigmas=range(1, 3, 1), black_ridges=False)
# Have a look at what the filtered image looks like in Napari:
viewer.add_image(image_mei, name="Vesselness filter", contrast_limits=(0, 1));
Thresholding#
Next, we apply Otsu’s method to binarize the image.
from skimage.filters import threshold_otsu
import numpy as np
labels_binary = (image_mei >= threshold_otsu(image_mei)).astype(np.uint8)
# Have a look at the result
viewer.add_labels(labels_binary, name="Otsu threshold");
Skeletonization#
Finally, we use Scikit-image to skeletonize the image to extract a binary trace at the center of the filaments.
from skimage.morphology import skeletonize_3d
img_skeleton = skeletonize_3d(labels_binary)
viewer.add_image(
img_skeleton,
name="Skeleton",
contrast_limits=(0, 1),
colormap='green',
blending='additive'
)
Analysis of the skeleton#
Using skan.Skeleton, we extract a graph representation of the skeleton in the form of a Pandas DataFrame.
table = summarize(Skeleton(skeleton_image=img_skeleton))
# Let's remove isolated branches (indexed as branch-type zero) from the table.
table.drop(table[table['branch-type'] == 0].index, axis=0, inplace=True)
table.head()
Visualization in Napari#
There is a little bit of extra work involved to prepare a vectors array of shape (N, 2, 3) suitable for Napari. Each row encodes the origin and end points of a 3D vector.
import pandas as pd
# Prepare the vectors for Napari
points_src = table[['image-coord-src-0', 'image-coord-src-1', 'image-coord-src-2']].values
points_dst = table[['image-coord-dst-0', 'image-coord-dst-1', 'image-coord-dst-2']].values
directions = points_dst - points_src
vectors = np.concatenate((points_src[np.newaxis], directions[np.newaxis]), axis=0)
vectors = np.swapaxes(vectors, 0, 1)
vectors.shape
We can finally display our vectors in the viewer!
# Add a `Vectors` layer
viewer.add_vectors(vectors, name="Branches", edge_width=0.6,
features=pd.DataFrame({'branch-type': table['branch-type'].values.astype(float)}),
edge_color='branch-type',
vector_style='line',
)
Conclusion#
In this notebook, we have prototyped a way of extracting the skeleton of an elongated, fibrous structure in a scientific image. We used Napari’s Image, Labels, and Vectors layers to visualize the data resulting from processing the image.