Skip to content

Commit

Permalink
Add widget to generate shape layer and colour by features (#201)
Browse files Browse the repository at this point in the history
Making a widget to allow users to select layer to skeletonise and colour
by features

---------

Co-authored-by: Juan Nunez-Iglesias <[email protected]>
  • Loading branch information
jamesyan-git and jni authored Oct 12, 2023
1 parent d9ff273 commit b61b24a
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 23 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,19 @@ jobs:
with:
python-version: ${{ matrix.python }}

# these libraries enable testing on Qt on linux
- uses: tlambert03/setup-qt-libs@v1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install 'setuptools<50.0'
python -m pip install .[all,testing]
- name: Test
run: |
pytest --pyargs skan --doctest-modules
uses: aganders3/headless-gui@v1
with:
run: pytest --pyargs skan --doctest-modules -s -v

- name: Coverage
if: runner.os == 'Linux' && matrix.python == '3.10'
Expand Down
19 changes: 19 additions & 0 deletions doc/examples/skeletonize_horse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import napari
from skimage import data, morphology
from skan import Skeleton
from skan.napari_skan import labels_to_skeleton_shapes, SkeletonizeMethod
import numpy as np

viewer = napari.Viewer()
horse = np.logical_not(data.horse().astype(bool))

labels_layer = viewer.add_labels(horse)

ldt = labels_to_skeleton_shapes(labels_layer, SkeletonizeMethod.zhang)
(skel_layer,) = viewer._add_layer_from_data(*ldt)

dw, widget = viewer.window.add_plugin_dock_widget(
'skan', 'Color Skeleton Widget'
)

napari.run()
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ python_requires = >=3.8
include_package_data = True
install_requires =
imageio>=2.10.1
magicgui>=0.7.3
matplotlib>=3.4
networkx>=2.7
numba>=0.53
Expand All @@ -71,8 +72,10 @@ all =
testing =
coverage
hypothesis
napari[pyqt5]!=0.4.18
pytest
pytest-cov
pytest-qt
seaborn<1.0
tifffile
docs =
Expand Down
18 changes: 12 additions & 6 deletions src/skan/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ name: skan
schema_version: 0.1.0
contributions:
commands:
- id: skan.skeletonize_labels
title: Skeletonize labels...
python_name: skan.napari_skan:skeletonize_labels

- id: skan.skeletonize
title: Make Skeleton
python_name: skan.napari_skan:labels_to_skeleton_shapes
- id: skan.color_widget
title: Color Widget
python_name: skan.napari_skan:color_by_feature
widgets:
- command: skan.skeletonize_labels
display_name: Skeletonize labels...
- command: skan.skeletonize
display_name: Skeleton Widget
autogenerate: true
- command: skan.color_widget
display_name: Color Skeleton Widget


109 changes: 94 additions & 15 deletions src/skan/napari_skan.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,108 @@
from magicgui import magic_factory
import numpy as np
from enum import Enum
from skimage.morphology import skeletonize
from skan import summarize, Skeleton

CAT_COLOR = "tab10"
CONTINUOUS_COLOR = "viridis"


class SkeletonizeMethod(Enum):
Zhang = "zhang"
Lee = "lee"
"""Use enum for method choice for easier use with magicgui."""
zhang = "zhang"
lee = "lee"


def skeletonize_labels(labels: "napari.types.LabelsData", method: SkeletonizeMethod) -> "napari.types.LabelsData":
"""Takes a labels layer and a skimage skeletonize method and generates a skeleton representation
def labels_to_skeleton_shapes(
labels: "napari.layers.Labels", choice: SkeletonizeMethod
) -> "napari.types.LayerDataTuple":
"""Skeletonize a labels layer using given method and export as Shapes.
Parameters
----------
labels : napari.types.LabelsData
A labels layer containing data to skeletonize
method : SkeletonizeMethod
Enum denoting the chosen skeletonize method method
labels : napari.layers.Labels
Labels layer containing data to skeletonize
choice : SkeletonizeMethod
Enum corresponding to skeletonization method
Returns
-------
napari.types.LabelsData
Labels layer depecting the extracted skeleton
napari.types.LayerDataTuple
Shapes layer data with skeleton
"""
binary_labels = (labels.data > 0).astype(np.uint8)
binary_skeleton = skeletonize(binary_labels, method=choice.value)

skeleton = Skeleton(binary_skeleton)

all_paths = [skeleton.path_coordinates(i) for i in range(skeleton.n_paths)]

# option to have main_path = True (or something) changing header
paths_table = summarize(skeleton)
layer_kwargs = {
'shape_type': 'path',
'edge_colormap': 'tab10',
'features': paths_table,
'metadata': {'skeleton': skeleton},
}

return all_paths, layer_kwargs, 'shapes'


def _populate_feature_choices(color_by_feature_widget):
"""Update feature names combobox when source layer is changed.
This runs on widget initialization and on every change of Shapes layer
thereafter.
Parameters
----------
color_by_feature_widget : function that takes widget as input
Function that takes in the widget and modifies the choices in-place.
"""
color_by_feature_widget.shapes_layer.changed.connect(
lambda _: _update_feature_names(color_by_feature_widget)
)
_update_feature_names(color_by_feature_widget)


def _update_feature_names(color_by_feature_widget):
"""Search for a shapes layer with appropriate metadata for skeletons
Parameters
----------
color_by_feature_widget : magicgui Widget
widget that contains reference to shapes layers
"""
shapes_layer = color_by_feature_widget.shapes_layer.value

def get_choices(features_combo):
"""Closure to use the current shapes layer to update given combobox."""
return shapes_layer.features.columns

color_by_feature_widget.feature_name.choices = get_choices


@magic_factory(
widget_init=_populate_feature_choices,
feature_name={"widget_type": "ComboBox"}
)
def color_by_feature(shapes_layer: "napari.layers.Shapes", feature_name):
"""Check the currently selected feature and update edge colors
TODO: allow selecting a colormap.
Parameters
----------
shapes_layer : napari.layers.Shapes
A napari Shapes layer.
feature_name : String
A string corresponding to a feature column present in the Shapes layer.
"""
binary_labels = (labels > 0).astype(np.uint8)
# skeletonize returns a binary array, so we can just multiply it with the
# labels to get appropriate colors
skeletonized = skeletonize(binary_labels, method=method.value) * labels
return skeletonized
current_column_type = shapes_layer.features[feature_name].dtype
if current_column_type == "float64":
shapes_layer.edge_colormap = CONTINUOUS_COLOR
else:
shapes_layer.edge_colormap = CAT_COLOR
shapes_layer.edge_color = feature_name
63 changes: 63 additions & 0 deletions src/skan/test/test_napari_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from skan.napari_skan import labels_to_skeleton_shapes, _update_feature_names
from skimage import data, morphology
from napari.layers import Labels
import numpy as np
from skan.napari_skan import SkeletonizeMethod
from skan.csr import Skeleton
import pandas as pd
import napari


def make_trivial_labels_layer():
label_data = np.zeros(shape=(10, 10), dtype=int)
label_data[5, 1:9] = 1
labels_layer = Labels(label_data)
return labels_layer


def test_get_skeleton_simple():
labels_layer = make_trivial_labels_layer()
skeleton_type = SkeletonizeMethod.zhang
shapes_data, layer_kwargs, _ = labels_to_skeleton_shapes(
labels_layer, skeleton_type
)

assert type(layer_kwargs["metadata"]["skeleton"]) is Skeleton
np.testing.assert_array_equal(
shapes_data[0],
[[5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [5, 6], [5, 7], [5, 8]]
)
assert len(shapes_data) == 1
assert 'features' in layer_kwargs
assert type(layer_kwargs['features']) is pd.DataFrame


def test_get_skeleton_horse():
horse = np.logical_not(data.horse().astype(bool))
labels_layer = Labels(horse)
skeleton_type = SkeletonizeMethod.zhang
shapes_data, layer_kwargs, _ = labels_to_skeleton_shapes(
labels_layer, skeleton_type
)
assert len(shapes_data) == 24 # 24 line segments in the horse skeleton
assert 'features' in layer_kwargs
assert type(layer_kwargs["features"]) is pd.DataFrame


def test_gui(make_napari_viewer):
viewer = make_napari_viewer()
horse = np.logical_not(data.horse().astype(bool))

labels_layer = viewer.add_labels(horse)

ldt = labels_to_skeleton_shapes(labels_layer, SkeletonizeMethod.zhang)
(skel_layer,) = viewer._add_layer_from_data(*ldt)

dw, widget = viewer.window.add_plugin_dock_widget(
'skan', 'Color Skeleton Widget'
)
widget.feature_name.value = "euclidean-distance"
widget()
layer = viewer.layers[-1]
assert layer.edge_colormap.name == 'viridis'
assert len(layer.data) == 24

0 comments on commit b61b24a

Please sign in to comment.