-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add widget to generate shape layer and colour by features (#201)
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
1 parent
d9ff273
commit b61b24a
Showing
6 changed files
with
197 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |