Sholl analysis in Skan#

Skan provides a function to perform Sholl analysis, which counts the number of processes crossing circular (2D) or spherical (3D) shells from a given center point. Commonly, the center point is the soma, or cell body, of a neuron, but the method can be used to compare general skeleton structures when a root or center point is defined.

%matplotlib inline
%config InlineBackend.figure_format='retina'

import matplotlib.pyplot as plt
import numpy as np
import zarr

neuron = np.asarray(zarr.open('../example-data/neuron.zarr.zip'))

fig, ax = plt.subplots()
ax.imshow(neuron, cmap='gray')
ax.scatter(57, 54)
ax.set_axis_off()
plt.show()
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
Cell In[2], line 4
      1 import numpy as np
      2 import zarr
----> 4 neuron = np.asarray(zarr.open('../example-data/neuron.zarr.zip'))
      6 fig, ax = plt.subplots()
      7 ax.imshow(neuron, cmap='gray')

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/_compat.py:43, in _deprecate_positional_args.<locals>._inner_deprecate_positional_args.<locals>.inner_f(*args, **kwargs)
     41 extra_args = len(args) - len(all_args)
     42 if extra_args <= 0:
---> 43     return f(*args, **kwargs)
     45 # extra_args > 0
     46 args_msg = [
     47     f"{name}={arg}"
     48     for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:], strict=False)
     49 ]

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/api/synchronous.py:193, in open(store, mode, zarr_version, zarr_format, path, storage_options, **kwargs)
    155 @_deprecate_positional_args
    156 def open(
    157     store: StoreLike | None = None,
   (...)    164     **kwargs: Any,  # TODO: type kwargs as valid args to async_api.open
    165 ) -> Array | Group:
    166     """Open a group or array using file-mode-like semantics.
    167 
    168     Parameters
   (...)    191         Return type depends on what exists in the given store.
    192     """
--> 193     obj = sync(
    194         async_api.open(
    195             store=store,
    196             mode=mode,
    197             zarr_version=zarr_version,
    198             zarr_format=zarr_format,
    199             path=path,
    200             storage_options=storage_options,
    201             **kwargs,
    202         )
    203     )
    204     if isinstance(obj, AsyncArray):
    205         return Array(obj)

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/core/sync.py:163, in sync(coro, loop, timeout)
    160 return_result = next(iter(finished)).result()
    162 if isinstance(return_result, BaseException):
--> 163     raise return_result
    164 else:
    165     return return_result

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/core/sync.py:119, in _runner(coro)
    114 """
    115 Await a coroutine and return the result of running it. If awaiting the coroutine raises an
    116 exception, the exception will be returned.
    117 """
    118 try:
--> 119     return await coro
    120 except Exception as ex:
    121     return ex

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/api/asynchronous.py:315, in open(store, mode, zarr_version, zarr_format, path, storage_options, **kwargs)
    286 """Convenience function to open a group or array using file-mode-like semantics.
    287 
    288 Parameters
   (...)    311     Return type depends on what exists in the given store.
    312 """
    313 zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
--> 315 store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options)
    317 # TODO: the mode check below seems wrong!
    318 if "shape" not in kwargs and mode in {"a", "r", "r+", "w"}:

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/storage/_common.py:309, in make_store_path(store_like, path, mode, storage_options)
    305         store = FsspecStore.from_url(
    306             store_like, storage_options=storage_options, read_only=_read_only
    307         )
    308     else:
--> 309         store = await LocalStore.open(root=Path(store_like), read_only=_read_only)
    310 elif isinstance(store_like, dict):
    311     # We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings.
    312     # By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate.
    313     store = await MemoryStore.open(store_dict=store_like, read_only=_read_only)

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/abc/store.py:83, in Store.open(cls, *args, **kwargs)
     67 """
     68 Create and open the store.
     69 
   (...)     80     The opened store instance.
     81 """
     82 store = cls(*args, **kwargs)
---> 83 await store._open()
     84 return store

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/zarr/storage/_local.py:105, in LocalStore._open(self)
    103 async def _open(self) -> None:
    104     if not self.read_only:
--> 105         self.root.mkdir(parents=True, exist_ok=True)
    106     return await super()._open()

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/pathlib.py:1311, in Path.mkdir(self, mode, parents, exist_ok)
   1307 """
   1308 Create a new directory at this given path.
   1309 """
   1310 try:
-> 1311     os.mkdir(self, mode)
   1312 except FileNotFoundError:
   1313     if not parents or self.parent == self:

FileExistsError: [Errno 17] File exists: '../example-data/neuron.zarr.zip'

This is the skeletonized image of a neuron. The cell body, or soma, has been manually annotated by a researcher based on the source image. We can use the function skan.sholl_analysis to count the crossings of concentric circles, centered on the cell body, by the cell’s processes.

import pandas as pd
from skan import Skeleton, sholl_analysis

# make the skeleton object
skeleton = Skeleton(neuron)
# define the neuron center/soma
center = np.array([54, 57])
# define radii at which to measure crossings
radii = np.arange(4, 45, 4)
# perform sholl analysis
center, radii, counts = sholl_analysis(
        skeleton, center=center, shells=radii
        )
table = pd.DataFrame({'radius': radii, 'crossings': counts})
table

We can visualize this using functions from skan.draw and matplotlib.

from skan import draw

# make two subplots
fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))

# draw the skeleton
draw.overlay_skeleton_2d_class(
        skeleton, skeleton_colormap='viridis_r', vmin=0, axes=ax0
        )
# draw the shells
draw.sholl_shells(center, radii, axes=ax0)
# fiddle with plot visual aspects
ax0.autoscale_view()
ax0.set_facecolor('black')
ax0.set_ylim(75, 20)
ax0.set_xlim(20, 80)
ax0.set_aspect('equal')

# in second subplot, plot the Sholl analysis
ax1.plot('radius', 'crossings', data=table)
ax1.set_xlabel('radius')
ax1.set_ylabel('crossings')

plt.show()