Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
341232f
copy orthogonal views code from older version
AnniekStok Apr 3, 2025
2b61231
Create QSplitter to optionally show the orthogonal views next to the …
AnniekStok Apr 3, 2025
23ccd97
separate crosswidget and multiple viewer widget
AnniekStok Apr 3, 2025
4de9b3c
clean up orthogonal views widget
AnniekStok Apr 3, 2025
8fc2e98
wip adjusting the sizing of the orth views with respect to the menu w…
AnniekStok Apr 4, 2025
e41207e
wip refactoring multipleviewer widget
AnniekStok Apr 11, 2025
ee70d45
update the dims order of the orthogonal views based on its own rel_or…
AnniekStok Apr 11, 2025
02e81ff
set default axis labels
AnniekStok Apr 11, 2025
e59a967
get rid of qtviewerwrap
AnniekStok Apr 11, 2025
88fdac4
merge main
AnniekStok Apr 11, 2025
2ae1755
fix bug in naming axes
AnniekStok Apr 22, 2025
f8d36cf
fix syncing issue on normal Labels layers
AnniekStok Apr 22, 2025
3df29e2
remove unused trackviewer from multiple viewer widget
AnniekStok Apr 30, 2025
2ac8429
add tests for multiple viewer widget
AnniekStok Apr 30, 2025
98fe9e7
bind shortcut to action_manager in the right place
AnniekStok Apr 30, 2025
4876b0c
import center_cross_on_mouse function to register its action
AnniekStok Apr 30, 2025
4794525
skip testing the shortcut for center_cross_on_mouse since we only nee…
AnniekStok Apr 30, 2025
06e1a8a
fix line length ruff errors
AnniekStok May 2, 2025
dac4bf6
merge main
AnniekStok Jul 28, 2025
019669e
ensure the orthogonal views also center when selecting a node
AnniekStok Jul 28, 2025
d67b053
remove contour labels
AnniekStok Jul 31, 2025
c51127d
remove remaining contour/group colormap code
AnniekStok Jul 31, 2025
ca37981
merge main
AnniekStok Jul 31, 2025
8ef2aa0
make not showing the orthoviews the default
AnniekStok Jul 31, 2025
bd1ef34
put orthoviews back in tab widget
AnniekStok Jul 31, 2025
e5ba454
viewer model containers should not be dockable (vispy canvas issue)
AnniekStok Aug 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
9 changes: 8 additions & 1 deletion finn/_tests/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ def _assert_shortcuts_exist_for_each_action(type_):
actions = _get_provider_actions(type_)
shortcuts = {name.partition(":")[-1] for name in get_settings().shortcuts.shortcuts}
shortcuts.update(func.__name__ for func in type_.class_keymap.values())

actions_to_skip = {
"center_cross_on_mouse"
} # skip this action as we only bind it if the cross widget is imported

for action in actions:
if action.__name__ in actions_to_skip:
continue # Skip this action
assert action.__name__ in shortcuts, (
f"missing shortcut for action '{action.__name__}' on '{type_.__name__}' is missing"
f"missing shortcut for action '{action.__name__}' on '{type_.__name__}'"
)


Expand Down
4 changes: 1 addition & 3 deletions finn/track_application_menus/main_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ class MainApp(QWidget):

def __init__(self, viewer: finn.Viewer):
super().__init__()
self.viewer = viewer

self.menu_widget = MenuWidget(viewer)
tree_widget = TreeWidget(viewer)

viewer.window.add_dock_widget(tree_widget, area="bottom", name="Tree View")

layout = QVBoxLayout()
layout.addWidget(self.menu_widget)

self.setLayout(layout)
5 changes: 4 additions & 1 deletion finn/track_application_menus/menu_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import finn
from finn.track_application_menus.editing_menu import EditingMenu
from finn.track_data_views.views.view_3d.orthogonal_views import OrthogonalViews
from finn.track_data_views.views_coordinator.tracks_viewer import TracksViewer

# from motile_tracker.motile.menus.motile_widget import MotileWidget
Expand All @@ -17,10 +18,11 @@ def __init__(self, viewer: finn.Viewer):

# motile_widget = MotileWidget(viewer)
editing_widget = EditingMenu(viewer)
self.orth_views = OrthogonalViews(viewer)

self.tabwidget = QTabWidget()

# tabwidget.addTab(motile_widget, "Track with Motile")
self.tabwidget.addTab(self.orth_views, "Orthogonal Views")
self.tabwidget.addTab(tracks_viewer.tracks_list, "Tracks List")
self.tabwidget.addTab(editing_widget, "Edit Tracks")

Expand All @@ -31,3 +33,4 @@ def __init__(self, viewer: finn.Viewer):
self.setWidgetResizable(True)

self.setLayout(layout)
self.setMinimumWidth(300)
183 changes: 113 additions & 70 deletions finn/track_data_views/views/layers/track_labels.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from __future__ import annotations

import random
import time
from typing import TYPE_CHECKING

import numpy as np

import finn
from finn.layers import Labels
from finn.utils import DirectLabelColormap
from finn.utils.action_manager import action_manager
from finn.utils.notifications import show_info, show_warning
from finn.utils.translations import trans

if TYPE_CHECKING:
from finn.track_data_views.views_coordinator.tracks_viewer import TracksViewer
Expand Down Expand Up @@ -50,10 +53,10 @@ def _new_label(layer: TrackLabels, new_track_id=True):
# release the mouse
layer.colormap = DirectLabelColormap(color_dict=layer.colormap.color_dict)
else:
show_info("Calculating empty label on non-numpy array is not supported")
show_info(trans._("Calculating empty label on non-numpy array is not supported"))


class TrackLabels(finn.layers.Labels):
class TrackLabels(Labels):
"""Extended labels layer that holds the track information and emits
and responds to dynamics visualization signals"""

Expand Down Expand Up @@ -85,6 +88,9 @@ def __init__(
)

self.viewer = viewer
self.viewer.dims.events.ndisplay.connect(
lambda: self.update_label_colormap(visible=None)
)

# Key bindings (should be specified both on the viewer (in tracks_viewer)
# and on the layer to overwrite finn defaults)
Expand All @@ -99,34 +105,51 @@ def __init__(
self.bind_key("z")(self.tracks_viewer.undo)
self.bind_key("r")(self.tracks_viewer.redo)

# Connect click events to node selection
@self.mouse_drag_callbacks.append
def click(_, event):
if (
event.type == "mouse_press"
and self.mode == "pan_zoom"
and not (
self.tracks_viewer.mode == "lineage"
and self.viewer.dims.ndisplay == 3
)
): # disable selecting in lineage mode in 3D
# Listen to click, paint events and changing the selected label
self.mouse_drag_callbacks.append(self.click)
self.events.paint.connect(self._on_paint)
self.tracks_viewer.selected_nodes.list_updated.connect(self.update_selected_label)
self.events.selected_label.connect(self._ensure_valid_label)
self.events.mode.connect(self._check_mode)
self.viewer.dims.events.current_step.connect(self._ensure_valid_label)

# Connect click events to node selection
def click(self, _, event):
if (
event.type == "mouse_press"
and self.mode == "pan_zoom"
and not (
self.tracks_viewer.mode == "lineage" and self.viewer.dims.ndisplay == 3
)
): # disable selecting in lineage mode in 3D
# differentiate between click and drag
mouse_press_time = time.time()
dragged = False
yield
# on move
while event.type == "mouse_move":
dragged = True
yield
if dragged and time.time() - mouse_press_time < 0.5:
dragged = False # suppress micro drag events and treat them as click
# on release
if not dragged:
label = self.get_value(
event.position,
view_direction=event.view_direction,
dims_displayed=event.dims_displayed,
world=True,
)
# check opacity (=visibility) in the colormap
if label is not None and label != 0 and self.colormap.map(label)[-1] != 0:
append = "Shift" in event.modifiers
self.tracks_viewer.selected_nodes.add(label, append)

# Listen to paint events and changing the selected label
self.events.paint.connect(self._on_paint)
self.tracks_viewer.selected_nodes.list_updated.connect(self.update_selected_label)
self.events.selected_label.connect(self._ensure_valid_label)
self.events.mode.connect(self._check_mode)
self.viewer.dims.events.current_step.connect(self._ensure_valid_label)
self.process_click(event, label)

def process_click(self, event: Event, label: int):
if (
label is not None and label != 0 and self.colormap.map(label)[-1] != 0
): # check opacity (=visibility) in the colormap
append = "Shift" in event.modifiers
self.tracks_viewer.selected_nodes.add(label, append)
else:
self.tracks_viewer.selected_nodes.reset()

def _get_colormap(self) -> DirectLabelColormap:
"""Get a DirectLabelColormap that maps node ids to their track ids, and then
Expand All @@ -152,37 +175,42 @@ def _get_colormap(self) -> DirectLabelColormap:

def _check_mode(self):
"""Check if the mode is valid and call the ensure_valid_label function"""
# here disconnecting the event listener is still necessary because
# self.mode = paint triggers the event internally and it is not blocked with
# event.blocker()
self.events.mode.disconnect(self._check_mode)

self.events.mode.disconnect(
self._check_mode
) # here disconnecting the event listener is still necessary because
# self.mode = paint triggers the event internally
# and it is not blocked with event.blocker()
if self.mode == "polygon":
show_info("Please use the paint tool to update the label")
show_info(
trans._(
"Please use the paint tool to update the label",
)
)
self.mode = "paint"

self._ensure_valid_label()
self.events.mode.connect(self._check_mode)

def redo(self):
"""Overwrite the redo functionality of the labels layer and invoke redo action on
the tracks_viewer.tracks_controller first
"""
"""Overwrite the redo functionality of the labels layer and
invoke redo action on the tracks_viewer.tracks_controller first"""

self.tracks_viewer.redo()

def undo(self):
"""Overwrite undo function and invoke undo action on the
tracks_viewer.tracks_controller
"""
tracks_viewer.tracks_controller"""

self.tracks_viewer.undo()

def _parse_paint_event(self, event_val):
"""_summary_

Args:
event_val (list[tuple]): A list of paint "atoms" generated by the labels
layer. Each atom is a 3-tuple of arrays containing:
event_val (list[tuple]): A list of paint "atoms" generated by the
labels layer.
Each atom is a 3-tuple of arrays containing:
- a numpy multi-index, pointing to the array elements that were
changed (a tuple with len ndims)
- the values corresponding to those elements before the change
Expand All @@ -208,8 +236,8 @@ def _parse_paint_event(self, event_val):
mask = concatenated_values == old_value
indices = tuple(concatenated_indices[dim][mask] for dim in range(ndim))
time_points = np.unique(indices[0])
for time in time_points:
time_mask = indices[0] == time
for time_point in time_points:
time_mask = indices[0] == time_point
actions.append(
(tuple(indices[dim][time_mask] for dim in range(ndim)), old_value)
)
Expand Down Expand Up @@ -262,10 +290,11 @@ def _on_paint(self, event):

if len(to_delete) > 0 and len(to_add) > 0:
show_warning(
"This paint or fill operation completely replaced one label with a "
"new label. This is currently not supported."
" If you want to update the track id of the node, please edit the "
"edges directly instead."
"""This paint or fill operation completely replaced one label
with a new label.
This is currently not supported.
If you want to update the track id of the node,
please edit the edges directly instead."""
)
self._revert_paint(event)
self.refresh()
Expand Down Expand Up @@ -293,8 +322,18 @@ def update_label_colormap(self, visible: list[int] | str) -> None:
with self.events.selected_label.blocker():
highlighted = self.tracks_viewer.selected_nodes

# update the opacity of the cyclic label colormap values according to
# whether nodes are visible/invisible/highlighted
# update the opacity of the cyclic label colormap values according
# to whether nodes are visible/invisible/highlighted
self.colormap.color_dict = {
key: np.array(
[*value[:-1], 0.6 if key is not None and key != 0 else value[-1]],
dtype=np.float32,
)
for key, value in self.colormap.color_dict.items()
}

# update the opacity of the cyclic label colormap values according
# to whether nodes are visible/invisible/highlighted
if visible == "all":
self.colormap.color_dict = {
key: np.array(
Expand All @@ -318,8 +357,12 @@ def update_label_colormap(self, visible: list[int] | str) -> None:

for node in highlighted:
self.colormap.color_dict[node][-1] = 1 # full opacity
# create a new colormap from the updated colors (to ensure refresh)
self.colormap = DirectLabelColormap(color_dict=self.colormap.color_dict)

self.colormap = DirectLabelColormap(
color_dict=self.colormap.color_dict
) # create a new colormap from the updated colors
# (otherwise it does not refresh)
self.refresh()

def new_colormap(self):
"""Override existing function to generate new colormap on tracks_viewer and
Expand All @@ -346,25 +389,25 @@ def _ensure_valid_label(self, event: Event | None = None):
"""Make sure a valid label is selected, because it is not allowed to paint with a
label that already exists at a different timepoint.
Scenarios:
1. If a node with the selected label value (node id) exists at a different time
point, check if there is any node with the same track_id at the current time
point
1.a if there is a node with the same track id, select that one, so that it
can be used to update an existing node
1.b if there is no node with the same track id, create a new node id and
paint with the track_id of the selected label.
1. If a node with the selected label value (node id) exists at a different
time point, check if there is any node with the same track_id at the current
time point
1.a if there is a node with the same track id, select that one,
so that it can be used to update an existing node
1.b if there is no node with the same track id,
create a new node id and paint with the track_id of the selected label.
This can be used to add a new node with the same track id at a time point
where it does not (yet) exist (anymore).
2. if there is no existing node with this value in the graph, it is assume that
you want to add a node with the current track id
Retrieve the track_id from self.current_track_id and use it to find if there are
any nodes of this track id at current time point
3. If no node with this label exists yet, it is valid and can be used to start a
new track id. Therefore, create a new node id and map a new color.
Add it to the dictionary.
4. If a node with the label exists at the current time point, it is valid and
can be used to update the existing node in a paint event. No action is needed
"""
where it does not (yet) exist (anymore).
2. if there is no existing node with this value in the graph,
it is assume that you want to add a node with the current track id
Retrieve the track_id from self.current_track_id and use it to find
if there are any nodes of this track id at current time point.
3. If no node with this label exists yet, it is valid
and can be used to start a new track id.
Therefore, create a new node id and map a new color. Add it to the dictionary.
4. If a node with the label exists at the current time point,
it is valid and can be used to update the existing node in a paint event.
No action is needed"""

if self.tracks_viewer.tracks is not None and self.mode in (
"fill",
Expand All @@ -386,8 +429,8 @@ def _ensure_valid_label(self, event: Event | None = None):
# we are changing the existing node. This is fine
pass
else:
# if there is already a node in that track in this frame, edit that
# instead
# if there is already a node in that track in this frame,
# edit that instead
edit = False
if self.selected_track in self.tracks_viewer.tracks.track_id_to_node:
for node in self.tracks_viewer.tracks.track_id_to_node[
Expand All @@ -409,12 +452,12 @@ def _ensure_valid_label(self, event: Event | None = None):
)

# the current node does not exist in the graph.
# Use the current selected_track as the track id (will be a new track if a
# new label was found with "m")
# Use the current selected_track as the track id (will be a new track
# if a new label was found with "m")
# Check that the track id is not already in this frame.
else:
# if there is already a node in that track in this frame, edit that
# instead
# if there is already a node in that track in this frame,
# edit that instead
edit = False
if self.selected_track in self.tracks_viewer.tracks.track_id_to_node:
for node in self.tracks_viewer.tracks.track_id_to_node[
Expand Down
Loading
Loading