Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eadab7d
added empty ROI widget
niksirbi Jun 18, 2025
ae445fc
added a basic ROI table connected to a shapes layer
niksirbi Jun 18, 2025
ce6aa1b
Basic Model/View implementation of an ROI table
niksirbi Jun 18, 2025
7f92b48
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 18, 2025
b6c7c6e
display auto-assigned ROI names as text
niksirbi Jun 19, 2025
13ccf15
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 19, 2025
607193f
deal with some pre-commit issues
niksirbi Jun 19, 2025
57223ad
clarify when and how ROI names are updated
niksirbi Jun 20, 2025
331cf2b
handle multiple ROI layers, each with its own table
niksirbi Jun 25, 2025
8ad14c5
reorganised roi_widget.py in a more logical structure
niksirbi Jun 25, 2025
3b68834
use groupboxes and tooltips
niksirbi Jun 25, 2025
8fd1360
select napari shape when correspodning table row is selected
niksirbi Jun 27, 2025
1fd13de
ability to edit ROI names in table
niksirbi Jun 27, 2025
a70a048
tweaked lables for layer dropdown, button and tooltip
niksirbi Jun 27, 2025
a0bf5bf
handle case whare shapes are created before proper layer name is given
niksirbi Jun 27, 2025
2995d76
Define a class for ROI styles
niksirbi Jun 30, 2025
ea56e47
reorganised code in rois_widget.py
niksirbi Jul 3, 2025
fbcf41b
assign a color style to each ROIs layer
niksirbi Jul 4, 2025
3f92be3
reworded tooltip for layer controls
niksirbi Jul 4, 2025
ef1c886
clarify difference between boxes and rois layer styles
niksirbi Jul 7, 2025
be8b2bf
Fix bug in RoisStyle.color_all_shapes that set layer.text to None
niksirbi Jan 9, 2026
51e61f5
Fix AttributeError when disconnecting signals from deleted layer
niksirbi Jan 9, 2026
7abbc01
Fix memory leaks from uncleaned signal connections
niksirbi Jan 9, 2026
2107bd1
Fix auto-naming for multiple shapes added simultaneously
niksirbi Jan 9, 2026
09dd6de
Use metadata and case-insensitive matching for ROI layer detection
niksirbi Jan 9, 2026
8dc842d
Fix handling of empty/None names in _auto_assign_roi_names
niksirbi Jan 9, 2026
09d7269
Remove redundant max_layers parameter
niksirbi Jan 9, 2026
5cfaccb
Add comment explaining ValueError suppression in name parsing
niksirbi Jan 9, 2026
99a7c8d
Use deterministic hash for ROI layer color assignment
niksirbi Jan 9, 2026
bc8f010
Add tests for RoisStyle color methods achieving 100% coverage
niksirbi Jan 9, 2026
106f743
Add contextual tooltips to ROIs table view
niksirbi Jan 9, 2026
dfa0348
Use sequential colors for default ROI layer names
niksirbi Jan 9, 2026
4fdc508
Fix ValidationError when adding empty shapes layer
niksirbi Jan 9, 2026
b21cd51
Harmonise docstrings across rois_widget.py file
niksirbi Jan 9, 2026
cc77cb4
Move _update_roi_names to module-level function
niksirbi Jan 12, 2026
3055915
first draft of tests for roi widget
niksirbi Jan 12, 2026
f5801d9
WIP refactoring tests
niksirbi Jan 12, 2026
041df13
Refactor ROIs widget tests: flatten classes and use parametrization
niksirbi Jan 12, 2026
6a63d0c
remove overly defensive trimming
niksirbi Jan 12, 2026
14766e7
Enforce unique ROI names within each layer
niksirbi Jan 13, 2026
f932310
Use "Un-named" as the default ROI name in the widget
niksirbi Jan 13, 2026
16de977
Rename ROI to Region in napari widget
niksirbi Jan 13, 2026
c355eb9
Handle copy-paste via set_data event and preserve copied names
niksirbi Jan 15, 2026
81862b0
Fix test_add_empty_shapes_layer to cover max_frame_idx < 0 branch
niksirbi Jan 19, 2026
afdfde6
Improve test coverage for RegionsWidget and mark unreachable code
niksirbi Jan 19, 2026
63e41eb
Apply testing suggestions from code review
niksirbi Mar 11, 2026
85403c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 11, 2026
52a5eab
fix pre-commit issues in tests
niksirbi Mar 11, 2026
7de2d12
Simplify test fixtures and fix bugs in test_regions_widget
niksirbi Mar 11, 2026
00b1ad6
Remove unreachable invalid-index guard in color_current_shape
niksirbi Mar 11, 2026
387d459
Improve color manage hash tests
niksirbi Mar 11, 2026
94541a0
Rename color_current_shape and color_all_shapes for clarity
niksirbi Mar 11, 2026
a27128b
remove feature details from module-level and class-level docstrings
niksirbi Mar 11, 2026
0b057a5
Define DROPDOWN_PLACEHOLDER constant and update placeholder text
niksirbi Mar 11, 2026
df0b77a
rename methods for UI layout setup
niksirbi Mar 11, 2026
39e2003
Combine tooltip tests into one parametrized test
niksirbi Mar 11, 2026
0153987
Combine stale index tests into one parametrized test
niksirbi Mar 11, 2026
a1d5372
clarify that 'model' refers to table model in test names
niksirbi Mar 11, 2026
f85c3c7
Claridy some test fixture docstrings
niksirbi Mar 11, 2026
02781a3
Reuse regions_widget fixture in regions_widget_with_layer
niksirbi Mar 11, 2026
4ccdc67
Filter empty layers upfront in _update_frame_slider_range
niksirbi Mar 12, 2026
0d20f60
Added clarifying comments to test_auto_assign_names_pads_missing_name…
niksirbi Mar 12, 2026
51ce0e5
Refactor test_table_model_data_returns_correct_values
niksirbi Mar 12, 2026
edd2873
Assert prior state in test_table_model_with_stale_index
niksirbi Mar 12, 2026
ffd962b
removed parameter types in docstring following rebase
niksirbi Mar 12, 2026
c2ea24b
Add bidirectional shape-table row selection sync in RegionsWidget
niksirbi Mar 12, 2026
46d9478
Sync dropdown and table when region layer selected in napari layer list
niksirbi Mar 12, 2026
e8ea69e
Can no longer create a 'movement regions layer' through renaming
niksirbi Mar 12, 2026
b6d993e
Auto-assign unique names to regions
niksirbi Mar 12, 2026
1c9fab9
text visibility is off by default
niksirbi Mar 12, 2026
15c23fd
Simplify colour management for regions layers
niksirbi Mar 12, 2026
232ba5c
WIP tether text colour to edge colour
niksirbi Mar 13, 2026
2e8cde7
Refactored tests for the draw-delete-redraw workflow on shapes
niksirbi Mar 18, 2026
6331e26
remove redundant methods and comments
niksirbi Mar 18, 2026
4bbe922
Add tests for invalid index guards in table model
niksirbi Mar 18, 2026
f51ffb7
Apply layer opacity in set_color_all_shapes
niksirbi Mar 18, 2026
93d1c40
fix sonarcloud issues
niksirbi Mar 18, 2026
759153c
Adde napari plugin development guidelines including model/view
niksirbi Mar 19, 2026
9700767
make layer and regions names lowercase
niksirbi Mar 19, 2026
e9471fb
Draft user guide for regions widget
niksirbi Mar 19, 2026
e2b7858
Added new widget screenshots and updated existing GUI screenshots
niksirbi Mar 20, 2026
066742b
Fix drawn shapes inheriting name from selected shape
niksirbi Mar 20, 2026
40e351f
use 'regions layer' (plural) in user guide
niksirbi Mar 20, 2026
7d94e35
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2026
fc3a86f
Apply suggestions from code review
niksirbi Mar 20, 2026
53a78be
clarify terminology about regions layers
niksirbi Mar 20, 2026
8e4c170
add links to qt6 classes
niksirbi Mar 20, 2026
8b2fc5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2026
16f4051
update regions screenshots in GUI guide
niksirbi Mar 20, 2026
c6f1d94
use underscores to uniquify region names
niksirbi Mar 20, 2026
e946e09
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2026
3c2c303
fix link to movement/napari folder on github
niksirbi Mar 22, 2026
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
1 change: 1 addition & 0 deletions docs/make_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
EXCLUDE_MODULES = {
"movement.cli_entrypoint",
"movement.napari.loader_widgets",
"movement.napari.regions_widget",
"movement.napari.meta_widget",
}

Expand Down
Binary file removed docs/source/_static/napari_bboxes_layer.png
Binary file not shown.
Binary file added docs/source/_static/napari_bboxes_layers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/napari_drawing_regions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/napari_new_region_layer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/source/_static/napari_plugin_data_tracks.png
Binary file not shown.
Binary file removed docs/source/_static/napari_plugin_video_slider.png
Binary file not shown.
Binary file removed docs/source/_static/napari_points_layer_tooltip.png
Binary file not shown.
Binary file added docs/source/_static/napari_points_tooltip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/napari_poses_layers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file added docs/source/_static/napari_tracks_sliders.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/napari_video_slider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions docs/source/community/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,76 @@ SourceSoftware: TypeAlias = Literal[
]
```

### Developing the napari plugin

The `movement` plugin for `napari` is built following the
[napari plugin guide](napari:plugins/building_a_plugin/index.html).
All widgets subclass `qtpy.QtWidgets.QWidget` (see the
[napari guide on widgets](napari:plugins/building_a_plugin/guides.html)).

The plugin lives in [`movement.napari`](movement-github:tree/main/movement/napari)
and is structured as follows:

- `movement.napari.meta_widget`: the top-level
container widget registered as the `napari` plugin entry point,
which brings together all other subwidgets:
- `movement.napari.loader_widgets`: a Qt form widget for
loading tracked datasets from supported file formats as
points, tracks and boxes.
- `movement.napari.regions_widget`: a Qt table widget for managing named
regions of interest drawn as `napari` shapes layers.
See the next section for more details on this widget's architecture.
- {mod}`movement.napari.layer_styles`: dataclasses that encapsulate visual
properties for each layer type.
- {mod}`movement.napari.convert`: functions for converting `movement`
datasets into the NumPy arrays and properties DataFrames
that `napari` layer constructors expect.

#### Qt Model/View architecture

`movement.napari.regions_widget` follows
[Qt's Model/View pattern](qt6:model-view-programming.html)
to separate the data (what is stored) from the display (how it is shown).
Understanding this pattern is helpful before making changes to this module,
and for creating new widgets that follow the same design principles.

The three components are:

- `RegionsTableModel` (subclasses
[`QAbstractTableModel`](qt6:qabstracttablemodel.html)):
wraps a `napari` [shapes layer](napari:howtos/layers/shapes.html) and
exposes its data to Qt (i.e., region names from `layer.properties["name"]`
and shape types). It listens to `napari` layer
[events](napari:guides/events_reference.html) and emits Qt signals
when the data changes.
- `RegionsTableView` (subclasses [`QTableView`](qt6:qtableview.html)):
renders the model's data as a table and handles user interactions
(e.g. row selection, inline name editing). Keeps table row selection
in sync with `napari`'s current shape selection.
- `RegionsWidget`: connects the table model and table view. It manages
layer selection, creates and links models to views, and handles layer
lifecycle events.

Data flows in both directions:

```
napari shapes layer <-> RegionsTableModel <-> RegionsTableView <-> User
```

:::{dropdown} Preventing circular updates in bidirectional syncs
:color: success
:icon: light-bulb

Bidirectional syncing between `napari` and the Qt table can cause circular
updates. For example, selecting a `napari` shape selects the corresponding
table row, which would then re-trigger shape selection.

Guard flags such as `_syncing_row_selection` and `_syncing_layer_selection`
break this cycle: while a sync is in progress, the corresponding flag is set
to `True` and any events that would re-trigger it are ignored. Preserve this
pattern when adding new two-way sync logic.
:::

### Continuous integration
All pushes and pull requests will be built by [GitHub actions](github-docs:actions).
This will usually include linting, testing and deployment.
Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
"uv": "https://docs.astral.sh/uv/{{path}}#{{fragment}}",
"attrs": "https://www.attrs.org/en/stable/{{path}}#{{fragment}}",
"pytest-benchmark": "https://pytest-benchmark.readthedocs.io/en/latest/{{path}}#{{fragment}}",
"qt6": "https://doc.qt.io/qt-6/{{path}}#{{fragment}}",
}

intersphinx_mapping = {
Expand Down
131 changes: 124 additions & 7 deletions docs/source/user_guide/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ The `movement` graphical user interface (GUI), powered by our custom plugin for
[napari](napari:), makes it easy to view and explore `movement`
motion tracks. Currently, you can use it to
visualise 2D [movement datasets](target-poses-and-bboxes-dataset)
as points, tracks, and rectangular bounding boxes (if defined) overlaid on video frames.
as points, tracks, and rectangular bounding boxes (if defined) overlaid on
video frames, as well as to define regions of interest (RoIs).

:::{warning}
The GUI is still in early stages of development but we are working on ironing
Expand Down Expand Up @@ -61,7 +62,7 @@ plugin—and click `OK`. You can optionally select to remember this reader
for all files with the same extension.


![napari widget video reader](../_static/napari_plugin_video_reader.png)
![napari video reader](../_static/napari_video_reader.png)


`napari-video` will load the video as an image stack with a slider
Expand All @@ -70,7 +71,7 @@ You may also use the left and right arrow keys to navigate
frame-by-frame.


![napari widget video slider](../_static/napari_plugin_video_slider.png)
![napari video slider](../_static/napari_video_slider.png)

Clicking on the play button will start the video playback at a default
rate of 10 frames per second. You can adjust the playback speed by right-clicking on the
Expand Down Expand Up @@ -199,11 +200,11 @@ For a poses dataset, you will see a view similar to this:

(target-widget-screenshot)=

![napari widget with poses dataset loaded](../_static/napari_plugin_data_tracks.png)
![napari with poses dataset loaded](../_static/napari_poses_layers.png)

And for a bounding boxes dataset, you will see a view more like the one below:

![napari widget with shapes loaded](../_static/napari_bboxes_layer.png)
![napari with bboxes dataset loaded](../_static/napari_bboxes_layers.png)


Note the additional bounding boxes layer that is loaded for bounding boxes datasets. For both poses and bounding boxes datasets, you can toggle the visibility of any of these layers by clicking on the eye icon.
Expand All @@ -230,7 +231,7 @@ and the time in seconds (calculated based on the frame number and
the `fps` value).


![napari points layer tooltip](../_static/napari_points_layer_tooltip.png)
![napari points tooltip](../_static/napari_points_tooltip.png)



Expand Down Expand Up @@ -276,7 +277,7 @@ selected layer shows the trajectories of the keypoints from the current frame un
end of the video.


![napari tracks layer head length](../_static/napari_tracks_layer_head_length.png)
![napari tracks sliders](../_static/napari_tracks_sliders.png)


You can also use the [tracks layer](napari:howtos/layers/tracks.html) controls panel to
Expand Down Expand Up @@ -324,3 +325,119 @@ You can find all the [keyboard shortcuts](napari:guides/preferences.html#shortcu
`napari` window, under `Preferences > Shortcuts`.

:::

## Define regions of interest

The `Define regions of interest` menu allows you to draw shapes on the
viewer and use them as regions of interest (RoIs) for analysis.

Each shape you draw represents a static region
that remains fixed across all frames.

:::{admonition} Terminology
:class: note

**Shape**
: Any geometric object you draw on the `napari` canvas
(e.g. a polygon or a line). Shapes are grouped together in
`napari` [shapes layers](napari:howtos/layers/shapes.html).

**Region**
: A named `napari` shape that `movement` recognises as a
region of interest (RoI) for analysis.

**Region layer**
: A `napari` [shapes layers](napari:howtos/layers/shapes.html) managed
by `movement`, whose shapes are treated as **regions**.

:::


### Create a region layer

To define a region for your data, start by creating a region layer,
which will serve as a container. To do this:

1. Ensure a [background layer](target-load-video) is loaded so that you
can see where you are drawing.
2. Expand the `Define regions of interest` menu on the right-hand side
of the `napari` window.
3. Click the `Add new layer` button.

This will create a new region layer in the layer list and select it.
The layer also appears in the dropdown next to the `Add new layer` button.
The layer's default name is `regions`, but you can change it by double-clicking
on it in the layer list (this is the case for any `napari` layer).

![napari new region layer](../_static/napari_new_region_layer.png)

### Draw and edit regions

With the region layer selected, you can use the native `napari` shape tools
in the layer controls panel to draw shapes on the canvas.
Convenient keyboard shortcuts are available to select the shape drawing tools,
for example:

- `R`: rectangle tool
- `E`: ellipse tool
- `P`: polygon tool
- `L`: line tool

Tools are also provided for selecting, moving, transforming,
and deleting shapes, as well as for editing their vertices.
You can also copy-paste shapes within a layer.
See the [napari shapes layer guide](napari:howtos/layers/shapes.html) for
details on the drawing tools and how to use them.

Each shape you draw is automatically added to the widget's table of regions
on the right-hand side and auto-assigned a unique name
(e.g. `region`, `region_1`, `region_2`, etc.).

![napari drawing regions](../_static/napari_drawing_regions.png)

You can interact with the table in the following ways:

- **Select**: clicking a row in the table selects the corresponding shape
on the canvas, and vice versa. This makes it easy to identify which
shape corresponds to which table entry.
- **Rename**: double-click the name cell of a region to edit its name.
Press `Enter` to confirm the change or `Escape` to cancel.
- **Delete**: select a shape on the canvas and press `Delete` (or
`Backspace`) to remove it. The corresponding row will be removed from
the table.

:::{admonition} Changing shape appearance
:class: tip

You can change the edge colour, face colour, edge width, and opacity of
shapes by selecting a shape (or multiple shapes)
and using the `napari` layer controls panel.
Once a shape's appearance has been adjusted, all subsequent shapes drawn in
the same layer will inherit those properties by default.

To display region names on the canvas, tick the `display text` checkbox
in the layer controls panel. The text colour follows the shape's edge
colour.
:::

### Save and load regions

Save and load functionality for region layers is coming soon.
You will be able to save a region layer to a GeoJSON file,
and load it back later or share it with others. This will also allow you
to import drawn regions into your Python code using the
{func}`~movement.roi.load_rois` function.

**Stay tuned!**

### Working with multiple region layers

You can use region layers to group related regions together.
For example, you might use one layer for "nesting zones" and another for
"foraging zones".

To add another region layer, click the `Add new layer` button again.
You can switch between region layers using the layers dropdown, or by
selecting the layer directly in the `napari` layer list—both are kept in sync.
The regions table updates to show only the shapes
belonging to the selected layer.
68 changes: 67 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 @@ def set_color_by(self, property: str, cmap: str | None = None) -> None:

@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,70 @@ def set_text_by(self, property: str) -> None:
self.text["string"] = property


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

The same ``color`` is applied to faces, edges, and text.
The face color alpha is hardcoded to 0.25, while edges and text are
fully opaque. Overall layer opacity is set to 1.0.
"""

name: str = "Regions"
color: str | tuple = "red"
edge_width: float = 5.0
opacity: float = 1.0
text: dict = field(
default_factory=lambda: {
"visible": False,
"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 set_style_for_new_shapes(self, layer: Shapes) -> None:
"""Set the style that napari will apply to newly drawn shapes.

napari uses current_* properties to style shapes as they are drawn.
"""
layer.current_face_color = self.face_color
layer.current_edge_color = self.edge_and_text_color
layer.current_edge_width = self.edge_width

def set_color_all_shapes(self, layer: Shapes) -> None:
"""Set colors on all existing shapes in a napari Shapes layer."""
# Configure text appearance
text_dict = layer.text.dict()
text_dict.update(self.text)
layer.text = text_dict
layer.text.color = self.edge_and_text_color

# Set layer opacity and per-shape face/edge colors
layer.opacity = self.opacity
n_shapes = len(layer.data)
if n_shapes > 0:
layer.face_color = [self.face_color] * n_shapes
layer.edge_color = [self.edge_and_text_color] * n_shapes
layer.edge_width = [self.edge_width] * n_shapes
layer.text.string = "{name}"
layer.text.refresh(layer.features)

self.set_style_for_new_shapes(layer)


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

Expand Down
9 changes: 8 additions & 1 deletion movement/napari/loader_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,19 @@ def _update_frame_slider_range(self):
with all NaN values, the frame slider range will not reflect
the full range of frames.
"""

def _layer_has_data(layer):
if isinstance(layer, Shapes):
return len(layer.data) > 0
return layer.data.shape[0] > 0

# Only update the frame slider range if there are layers
# that are Points, Tracks, Image or Shapes
# that are Points, Tracks, Image or Shapes with data
list_layers = [
ly
for ly in self.viewer.layers
if isinstance(ly, Points | Tracks | Image | Shapes)
and _layer_has_data(ly)
]
if len(list_layers) > 0:
# Get the maximum frame index from all candidate layers
Expand Down
Loading
Loading