-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add widget to generate shape layer and colour by features #201
Changes from 35 commits
8505544
703d220
cf7b958
a936bed
63fae5c
9fe3990
fb2f432
6df682d
033d4fd
03d5d3c
a6e489b
7ec6609
7549de5
887592f
82f9de7
9576aaf
5ca99da
4507a5a
940a34b
33940ac
77c9a23
878d54e
7700ceb
9d06751
4d7ca36
8df5eea
6cbf153
18c5483
7b92a19
13eec9d
45ad999
b76844a
6f39d02
e350344
ca3bc9c
36995ed
f831f8b
71ce5ff
6f8fea8
6d92c67
73cb522
dc82a8a
a7ad01e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ python_requires = >=3.8 | |
include_package_data = True | ||
install_requires = | ||
imageio>=2.10.1 | ||
magicgui @ git+https://[email protected]/pyapp-kit/magicgui.git | ||
matplotlib>=3.4 | ||
networkx>=2.7 | ||
numba>=0.53 | ||
|
@@ -71,8 +72,10 @@ all = | |
testing = | ||
coverage | ||
hypothesis | ||
napari[pyqt5]!=0.4.18 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment here about why we blocked 0.4.18? If you remember, I've totally forgotten! 😅 |
||
pytest | ||
pytest-cov | ||
pytest-qt | ||
seaborn<1.0 | ||
tifffile | ||
docs = | ||
|
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 get_skeleton, SkeletonizeMethod | ||
import numpy as np | ||
|
||
viewer = napari.Viewer() | ||
horse = np.logical_not(data.horse().astype(bool)) | ||
|
||
labels_layer = viewer.add_labels(horse) | ||
|
||
ldt = get_skeleton(labels_layer, SkeletonizeMethod.zhang) | ||
(skel_layer,) = viewer._add_layer_from_data(*ldt) | ||
|
||
dw, widget = viewer.window.add_plugin_dock_widget( | ||
'skan', 'Color Skeleton Widg...' | ||
) | ||
|
||
napari.run() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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:get_skeleton | ||
- 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 Widg... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you update the names here? |
||
autogenerate: true | ||
- command: skan.color_widget | ||
display_name: Color Skeleton Widg... | ||
|
||
|
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 get_skeleton( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's rename this something more meaningful, say, |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably should be private? (ie prefixed with |
||
"""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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from skan.napari_skan import get_skeleton, _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, _ = get_skeleton(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, _ = get_skeleton(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 = get_skeleton(labels_layer, SkeletonizeMethod.zhang) | ||
(skel_layer,) = viewer._add_layer_from_data(*ldt) | ||
|
||
dw, widget = viewer.window.add_plugin_dock_widget( | ||
'skan', 'Color Skeleton Widg...' | ||
) | ||
widget.feature_name.value = "euclidean-distance" | ||
widget() | ||
layer = viewer.layers[-1] | ||
assert layer.edge_colormap.name == 'viridis' | ||
assert len(layer.data) == 24 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh this can now be
magicgui>=0.7.3