Skip to content
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

Merged
merged 43 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8505544
chenged result to match labels colour
jamesyan-git Feb 28, 2023
703d220
added option to change feature to colormap
jamesyan-git Mar 24, 2023
cf7b958
merge main
jamesyan-git Mar 24, 2023
a936bed
reworked button and dropdowns
jamesyan-git Apr 11, 2023
63fae5c
added skeleton to metada (broke a few things)
jamesyan-git May 3, 2023
9fe3990
fixed recolouring error
jamesyan-git May 3, 2023
fb2f432
add skeletonize method choice
jamesyan-git May 10, 2023
6df682d
pre magicgui refactor commit
jamesyan-git May 12, 2023
033d4fd
refactored Skeletonize to magic gui
jamesyan-git May 17, 2023
03d5d3c
use magicgui.create_widget
jamesyan-git May 17, 2023
a6e489b
refactor analyze skeleton widget
jamesyan-git May 17, 2023
7ec6609
remove napari dependency
jamesyan-git Jun 6, 2023
7549de5
compute features when building skeleton
jamesyan-git Jun 8, 2023
887592f
set colourmap based on data type, added comments
jamesyan-git Jun 8, 2023
82f9de7
added magicgui to requirements for github tests
jamesyan-git Jun 8, 2023
9576aaf
refactored into magic_factory widget
jamesyan-git Jun 12, 2023
5ca99da
removed old widget
jamesyan-git Jun 16, 2023
4507a5a
added runner script for debug
jamesyan-git Jun 16, 2023
940a34b
better testing script
jamesyan-git Jun 16, 2023
33940ac
get_choices can be a callable
jamesyan-git Jun 23, 2023
77c9a23
Merge branch 'main' of https://github.com/jni/skan into colour-features
jamesyan-git Aug 11, 2023
878d54e
Add some docs, test stub, run script
jamesyan-git Aug 11, 2023
7700ceb
Merge branch 'jni:main' into colour-features
jamesyan-git Aug 11, 2023
9d06751
Merge branch 'colour-features' of https://github.com/jamesyan-git/ska…
jamesyan-git Aug 11, 2023
4d7ca36
easy tests are done
jamesyan-git Aug 11, 2023
8df5eea
Move features out of metadata
jni Aug 11, 2023
6cbf153
Add yapf formatting to napari_skan
jni Aug 11, 2023
18c5483
Update tests for get_skeleton
jni Aug 11, 2023
7b92a19
Update docstrings and comments
jni Aug 11, 2023
13eec9d
Refactor some code for cleaner autoformatting
jni Aug 11, 2023
45ad999
Remove unnecessary lines now that features are 1st
jni Aug 11, 2023
b76844a
Remove unused imports
jni Aug 11, 2023
6f39d02
Add magicgui main as dependency for now
jni Aug 11, 2023
e350344
Use github https rather than ssh for easier access
jni Aug 11, 2023
ca3bc9c
Add gui test and test dependencies
jamesyan-git Aug 11, 2023
36995ed
address comments
jamesyan-git Oct 8, 2023
f831f8b
add qt-libs and headless to test.yml
jamesyan-git Oct 8, 2023
71ce5ff
move examle to example folders
jamesyan-git Oct 9, 2023
6f8fea8
update tests
jamesyan-git Oct 9, 2023
6d92c67
move qt-lib set up to before testing
jamesyan-git Oct 12, 2023
73cb522
reorder dependency installation
jamesyan-git Oct 12, 2023
dc82a8a
fix syntax in test.yml
jamesyan-git Oct 12, 2023
a7ad01e
remove redundant lines
jamesyan-git Oct 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @ git+https://[email protected]/pyapp-kit/magicgui.git
Copy link
Owner

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

matplotlib>=3.4
networkx>=2.7
numba>=0.53
Expand All @@ -71,8 +72,10 @@ all =
testing =
coverage
hypothesis
napari[pyqt5]!=0.4.18
Copy link
Owner

Choose a reason for hiding this comment

The 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 =
Expand Down
19 changes: 19 additions & 0 deletions src/skan/color-by-feature.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 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()
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: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...
Copy link
Owner

Choose a reason for hiding this comment

The 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...


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 get_skeleton(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this something more meaningful, say, 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):
Copy link
Owner

Choose a reason for hiding this comment

The 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
59 changes: 59 additions & 0 deletions src/skan/test/test_napari_plugin.py
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