Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
81257f5
added empty ROI widget
niksirbi Jun 18, 2025
4d55ca8
added a basic ROI table connected to a shapes layer
niksirbi Jun 18, 2025
87b9daa
Basic Model/View implementation of an ROI table
niksirbi Jun 18, 2025
bf6b7be
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 18, 2025
6ca4404
display auto-assigned ROI names as text
niksirbi Jun 19, 2025
8021a65
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 19, 2025
c47e8b8
deal with some pre-commit issues
niksirbi Jun 19, 2025
0c80454
clarify when and how ROI names are updated
niksirbi Jun 20, 2025
0f5e4fe
handle multiple ROI layers, each with its own table
niksirbi Jun 25, 2025
35628ab
reorganised roi_widget.py in a more logical structure
niksirbi Jun 25, 2025
e3213d4
use groupboxes and tooltips
niksirbi Jun 25, 2025
bb42d23
select napari shape when correspodning table row is selected
niksirbi Jun 27, 2025
7aa1773
ability to edit ROI names in table
niksirbi Jun 27, 2025
8e15f53
tweaked lables for layer dropdown, button and tooltip
niksirbi Jun 27, 2025
bcb97c6
handle case whare shapes are created before proper layer name is given
niksirbi Jun 27, 2025
5d6a59b
Define a class for ROI styles
niksirbi Jun 30, 2025
8f5bd75
reorganised code in rois_widget.py
niksirbi Jul 3, 2025
2ab8786
assign a color style to each ROIs layer
niksirbi Jul 4, 2025
5c19b1a
reworded tooltip for layer controls
niksirbi Jul 4, 2025
3aab669
clarify difference between boxes and rois layer styles
niksirbi Jul 7, 2025
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
114 changes: 113 additions & 1 deletion movement/napari/layer_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import numpy as np
import pandas as pd
from napari.layers import Shapes
from napari.utils.color import ColorValue
from napari.utils.colormaps import ensure_colormap

DEFAULT_COLORMAP = "turbo"
Expand Down Expand Up @@ -123,7 +125,7 @@

@dataclass
class BoxesStyle(LayerStyle):
"""Style properties for a napari Shapes layer."""
"""Style properties for a napari Shapes layer containing bounding boxes."""

edge_width: int = 3
opacity: float = 1.0
Expand Down Expand Up @@ -190,6 +192,116 @@
self.text["string"] = property


@dataclass
class RoisStyle(LayerStyle):
"""Style properties for a napari Shapes layer containing ROIs.

The same ``color`` is applied to faces, edges, and text.
The face color opacity is hardcoded to 0.25, while edges and text
colors are opaque.
"""

name: str = "ROIs"
color: str | tuple = "red"
edge_width: float = 5.0
opacity: float = 1.0 # applies to the whole layer
text: dict = field(
default_factory=lambda: {
"visible": True,
"anchor": "center",
}
)

@property
def face_color(self) -> ColorValue:
"""Return the face color with transparency applied."""
color = ColorValue(self.color)
color[-1] = 0.25 # this is hardcoded for now
return color

@property
def edge_and_text_color(self) -> ColorValue:
"""Return the opaque color for edges and text."""
color = ColorValue(self.color)
color[-1] = 1.0
return color

def color_current_shape(self, layer: Shapes) -> None:
"""Color the current shape in a napari Shapes layer.

napari uses current_* for new shapes.
"""
# Only proceed if there are valid selected shapes
if hasattr(layer, "selected_data") and layer.selected_data:
valid_selected = {

Check warning on line 236 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L235-L236

Added lines #L235 - L236 were not covered by tests
i for i in layer.selected_data if 0 <= i <= len(layer.data) - 1
}
if not valid_selected:
return

Check warning on line 240 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L239-L240

Added lines #L239 - L240 were not covered by tests

layer.current_face_color = self.face_color
layer.current_edge_color = self.edge_and_text_color
layer.current_edge_width = self.edge_width

Check warning on line 244 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L242-L244

Added lines #L242 - L244 were not covered by tests

def color_all_shapes(self, layer: Shapes) -> None:
"""Color all shapes in a napari Shapes layer, including new ones."""
n_shapes = len(layer.data)
if n_shapes > 0:
layer.face_color = [self.face_color] * len(layer.data)
layer.edge_color = [self.edge_and_text_color] * len(layer.data)
layer.edge_width = [self.edge_width] * len(layer.data)

Check warning on line 252 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L248-L252

Added lines #L248 - L252 were not covered by tests

# Set text properties
layer.text = layer.text.dict().update(self.text)
layer.text.color = self.edge_and_text_color
layer.text.string = "{name}"

Check warning on line 257 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L255-L257

Added lines #L255 - L257 were not covered by tests

self.color_current_shape(layer)

Check warning on line 259 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L259

Added line #L259 was not covered by tests


@dataclass
class RoisColorManager:
"""Manages colors for ROIs layers.

It makes sure that ROIs layers are each assigned a color cyclicly sampled
from a napari colormap.
"""

cmap_name: str = "tab10"
max_layers: int = 10
layer_colors: dict = field(default_factory=dict)
next_color_index: int = 0
colors: list = field(init=False)

def __post_init__(self):
"""Initialize the colors after the dataclass is created."""
self.colors = _sample_colormap(self.max_layers, self.cmap_name)

def get_color_for_layer(self, layer_name: str) -> tuple:
"""Get or assign a color for a layer.

If the layer already has a color assigned, return it.
Otherwise, assign the next color from the cycle.

Parameters
----------
layer_name : str
The name of the layer.

Returns
-------
tuple
The RGBA color tuple for the layer.

"""
if layer_name not in self.layer_colors:
color = self.colors[self.next_color_index % len(self.colors)]
self.layer_colors[layer_name] = color
self.next_color_index += 1

Check warning on line 300 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L297-L300

Added lines #L297 - L300 were not covered by tests

return self.layer_colors[layer_name]

Check warning on line 302 in movement/napari/layer_styles.py

View check run for this annotation

Codecov / codecov/patch

movement/napari/layer_styles.py#L302

Added line #L302 was not covered by tests


def _sample_colormap(n: int, cmap_name: str) -> list[tuple]:
"""Sample n equally-spaced colors from a napari colormap.

Expand Down
12 changes: 10 additions & 2 deletions movement/napari/meta_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from qt_niu.collapsible_widget import CollapsibleWidgetContainer

from movement.napari.loader_widgets import DataLoader
from movement.napari.rois_widget import RoisWidget


class MovementMetaWidget(CollapsibleWidgetContainer):
Expand All @@ -24,5 +25,12 @@ def __init__(self, napari_viewer: Viewer, parent=None):
widget_title="Load tracked data",
)

self.loader = self.collapsible_widgets[0]
self.loader.expand() # expand the loader widget by default
# Add the ROI widget
self.add_widget(
RoisWidget(napari_viewer, parent=self),
collapsible=True,
widget_title="Define ROIs",
)

loader_collapsible = self.collapsible_widgets[0]
loader_collapsible.expand() # expand the loader widget by default
Loading
Loading