1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
"""
============================================
Estimate anisotropy in a 3D microscopy image
============================================
In this tutorial, we compute the structure tensor of a 3D image.
For a general introduction to 3D image processing, please refer to
:ref:`sphx_glr_auto_examples_applications_plot_3d_image_processing.py`.
The data we use here are sampled from an image of kidney tissue obtained by
confocal fluorescence microscopy (more details at [1]_ under
``kidney-tissue-fluorescence.tif``).
.. [1] https://gitlab.com/scikit-image/data/#data
"""
import matplotlib.pyplot as plt
import numpy as np
import plotly.express as px
import plotly.io
import skimage as ski
#####################################################################
# Load image
# ==========
# This biomedical image is available through `scikit-image`'s data registry.
data = ski.data.kidney()
#####################################################################
# What exactly are the shape and size of our 3D multichannel image?
print(f'number of dimensions: {data.ndim}')
print(f'shape: {data.shape}')
print(f'dtype: {data.dtype}')
#####################################################################
# For the purposes of this tutorial, we shall consider only the second color
# channel, which leaves us with a 3D single-channel image. What is the range
# of values?
n_plane, n_row, n_col, n_chan = data.shape
v_min, v_max = data[:, :, :, 1].min(), data[:, :, :, 1].max()
print(f'range: ({v_min}, {v_max})')
#####################################################################
# Let us visualize the middle slice of our 3D image.
fig1 = px.imshow(
data[n_plane // 2, :, :, 1],
zmin=v_min,
zmax=v_max,
labels={'x': 'col', 'y': 'row', 'color': 'intensity'},
)
plotly.io.show(fig1)
#####################################################################
# Let us pick a specific region, which shows relative X-Y isotropy. In
# contrast, the gradient is quite different (and, for that matter, weak) along
# Z.
sample = data[5:13, 380:410, 370:400, 1]
step = 3
cols = sample.shape[0] // step + 1
_, axes = plt.subplots(nrows=1, ncols=cols, figsize=(16, 8))
for it, (ax, image) in enumerate(zip(axes.flatten(), sample[::step])):
ax.imshow(image, cmap='gray', vmin=v_min, vmax=v_max)
ax.set_title(f'Plane = {5 + it * step}')
ax.set_xticks([])
ax.set_yticks([])
#####################################################################
# To view the sample data in 3D, run the following code:
#
# .. code-block:: python
#
# import plotly.graph_objects as go
#
# (n_Z, n_Y, n_X) = sample.shape
# Z, Y, X = np.mgrid[:n_Z, :n_Y, :n_X]
#
# fig = go.Figure(
# data=go.Volume(
# x=X.flatten(),
# y=Y.flatten(),
# z=Z.flatten(),
# value=sample.flatten(),
# opacity=0.5,
# slices_z=dict(show=True, locations=[4])
# )
# )
# fig.show()
#####################################################################
# Compute structure tensor
# ========================
# Let us visualize the bottom slice of our sample data and determine the
# typical size for strong variations. We shall use this size as the
# 'width' of the window function.
fig2 = px.imshow(
sample[0, :, :],
zmin=v_min,
zmax=v_max,
labels={'x': 'col', 'y': 'row', 'color': 'intensity'},
title='Interactive view of bottom slice of sample data.',
)
plotly.io.show(fig2)
#####################################################################
# About the brightest region (i.e., at row ~ 22 and column ~ 17), we can see
# variations (and, hence, strong gradients) over 2 or 3 (resp. 1 or 2) pixels
# across columns (resp. rows). We may thus choose, say, ``sigma = 1.5`` for
# the window
# function. Alternatively, we can pass sigma on a per-axis basis, e.g.,
# ``sigma = (1, 2, 3)``. Note that size 1 sounds reasonable along the first
# (Z, plane) axis, since the latter is of size 8 (13 - 5). Viewing slices in
# the X-Z or Y-Z planes confirms it is reasonable.
sigma = (1, 1.5, 2.5)
A_elems = ski.feature.structure_tensor(sample, sigma=sigma)
#####################################################################
# We can then compute the eigenvalues of the structure tensor.
eigen = ski.feature.structure_tensor_eigenvalues(A_elems)
eigen.shape
#####################################################################
# Where is the largest eigenvalue?
coords = np.unravel_index(eigen.argmax(), eigen.shape)
assert coords[0] == 0 # by definition
coords
#####################################################################
# .. note::
# The reader may check how robust this result (coordinates
# ``(plane, row, column) = coords[1:]``) is to varying ``sigma``.
#
# Let us view the spatial distribution of the eigenvalues in the X-Y plane
# where the maximum eigenvalue is found (i.e., ``Z = coords[1]``).
fig3 = px.imshow(
eigen[:, coords[1], :, :],
facet_col=0,
labels={'x': 'col', 'y': 'row', 'facet_col': 'rank'},
title=f'Eigenvalues for plane Z = {coords[1]}.',
)
plotly.io.show(fig3)
#####################################################################
# We are looking at a local property. Let us consider a tiny neighborhood
# around the maximum eigenvalue in the above X-Y plane.
eigen[0, coords[1], coords[2] - 2 : coords[2] + 1, coords[3] - 2 : coords[3] + 1]
#####################################################################
# If we examine the second-largest eigenvalues in this neighborhood, we can
# see that they have the same order of magnitude as the largest ones.
eigen[1, coords[1], coords[2] - 2 : coords[2] + 1, coords[3] - 2 : coords[3] + 1]
#####################################################################
# In contrast, the third-largest eigenvalues are one order of magnitude
# smaller.
eigen[2, coords[1], coords[2] - 2 : coords[2] + 1, coords[3] - 2 : coords[3] + 1]
#####################################################################
# Let us visualize the slice of sample data in the X-Y plane where the
# maximum eigenvalue is found.
fig4 = px.imshow(
sample[coords[1], :, :],
zmin=v_min,
zmax=v_max,
labels={'x': 'col', 'y': 'row', 'color': 'intensity'},
title=f'Interactive view of plane Z = {coords[1]}.',
)
plotly.io.show(fig4)
#####################################################################
# Let us visualize the slices of sample data in the X-Z (left) and Y-Z (right)
# planes where the maximum eigenvalue is found. The Z axis is the vertical
# axis in the subplots below. We can see the expected relative invariance
# along the Z axis (corresponding to longitudinal structures in the kidney
# tissue), especially in the Y-Z plane (``longitudinal=1``).
subplots = np.dstack((sample[:, coords[2], :], sample[:, :, coords[3]]))
fig5 = px.imshow(
subplots, zmin=v_min, zmax=v_max, facet_col=2, labels={'facet_col': 'longitudinal'}
)
plotly.io.show(fig5)
#####################################################################
# As a conclusion, the region about voxel
# ``(plane, row, column) = coords[1:]`` is
# anisotropic in 3D: There is an order of magnitude between the third-largest
# eigenvalues on one hand, and the largest and second-largest eigenvalues on
# the other hand. We could see this at first glance in figure `Eigenvalues for
# plane Z = 1`.
#####################################################################
# The neighborhood in question is 'somewhat isotropic' in a plane (which,
# here, would be relatively close to the X-Y plane): There is a factor of
# less than 2 between the second-largest and largest eigenvalues.
# This description is compatible with what we are seeing in the image, i.e., a
# stronger gradient across a direction (which, here, would be relatively close
# to the row axis) and a weaker gradient perpendicular to it.
#####################################################################
# In an ellipsoidal representation of the 3D structure tensor [2]_,
# we would get the pancake situation.
#
# .. [2] https://en.wikipedia.org/wiki/Structure_tensor#Interpretation_2
|