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

(4->3) Add GUI Elements for Interacting With RecordingSession #1283

Open
wants to merge 64 commits into
base: liezl/asc-initial-update-instances-across-views
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
ff1c45f
Fix (de)serialization of `RecordingSession`
roomrys Apr 18, 2023
4ec66b5
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Apr 19, 2023
b694b7b
Remove tracks from multiview dataset
roomrys Apr 19, 2023
5807276
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
roomrys Apr 19, 2023
9dd22f7
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
roomrys Apr 19, 2023
c6c97d0
[wip] Add sessions dock for triangulation on demmand
roomrys Apr 19, 2023
9f8474a
Move some logic from app to `TriangulateSession` command
roomrys Apr 19, 2023
dd76841
Move `RecordingSession.update_views` to `TriangulateSession`
roomrys Apr 19, 2023
89ec6a2
Make `triangulateSession` more testable
roomrys Apr 19, 2023
9d9e5e3
Add shortcut for navigating views
roomrys Apr 20, 2023
66440c5
Replace `RecordingSession` test data
roomrys Apr 24, 2023
f66438d
Add `RecordingSession` to test set and adapt fixture
roomrys Apr 24, 2023
526b9c2
Merge branch 'liezl/iuiav-fix-(de)-serialization' into liezl/add-gui-…
roomrys Apr 24, 2023
0037d88
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Apr 27, 2023
90609af
Merge branch 'liezl/iuiav-fix-(de)-serialization' into liezl/add-gui-…
roomrys Apr 27, 2023
1d2fb50
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Jul 20, 2023
264c2b6
Merge branch 'liezl/iuiav-fix-(de)-serialization' into liezl/add-gui-…
roomrys Jul 20, 2023
9896a19
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
roomrys Oct 13, 2023
b53c103
Refactor `TriangulateSession`
roomrys Oct 13, 2023
2c9ea0c
Uncomment test lines
roomrys Oct 13, 2023
b9e7158
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
roomrys Oct 16, 2023
5d67989
Add "goto prev view" to yaml
roomrys Oct 16, 2023
b61d90b
Add `Ctrl` key to mark instances as incomplete
roomrys Oct 16, 2023
cc78c91
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
roomrys Oct 19, 2023
90a2c0e
Add `RecordingSession`s table to `SessionsDock` (#1654)
vaibhavtrip29 Mar 15, 2024
145a1d1
Add `CamerasTable` to `SessionsDock` (#1671)
roomrys Mar 18, 2024
936663e
(4a -> 4) Add `UnlinkedVideosTable` to `SessionsDock` (#1720)
roomrys Apr 5, 2024
b6f6563
Merge branch 'liezl/asc-initial-update-instances-across-views' of htt…
Apr 18, 2024
72788a2
Lint
Apr 18, 2024
106d023
(4b -> 4) Display the left-ellipsis for Video filename in UnlinkedVid…
7174Andy Apr 19, 2024
481a07f
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Apr 23, 2024
370573f
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Apr 25, 2024
ce9f2a1
Always use `Instance.from_predicted` to determine `Labels.instances_t…
Apr 25, 2024
72e03d2
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys Apr 25, 2024
3cb6fc7
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys May 1, 2024
84dc0eb
(4a -> 4) Add menu to assign an `Instance` to an `InstanceGroup` (#1747)
ramizhajj1 May 1, 2024
45eeea7
Merge branch 'liezl/asc-initial-update-instances-across-views' into l…
roomrys May 2, 2024
6de801c
(4c -> 4) Add color by `InstanceGroup` option (#1760)
7174Andy May 29, 2024
e5b8c67
(4a -> 4) Add instance group dock (#1775)
vaibhavtrip29 May 29, 2024
ddeb083
(4a -> 4) Re-encode multiview videos (#1787)
roomrys May 29, 2024
00f9a3b
(4a -> 4) Add double click functionality to UnlinkVideoTable (#1763)
ramizhajj1 May 29, 2024
3cba6d0
Update session as callback to updating video
roomrys May 30, 2024
83713d3
Fix coloring issue where ungrouped instances where colored black
roomrys May 30, 2024
52962a0
(4a -> 4) Add visual indicator for current camera (#1783)
justinvshen May 30, 2024
d4136f5
Change switch view hotkey (#1793)
justinvshen Jun 7, 2024
f9858ad
(4b -> 4) Automatically add videos when “Add Recording Session” (#1788)
7174Andy Jun 8, 2024
c76a030
Add feature to maintain current frame index (#1789)
ramizhajj1 Jun 20, 2024
a5b4769
Fix skeleton table coloring bug (#1826)
roomrys Jun 25, 2024
a32d68a
Add id property to RecordingSession (#1829)
roomrys Jun 26, 2024
5d120c6
Switch to linked video when double click on camera in `CamerasTable` …
7174Andy Jul 1, 2024
42ffe4e
Link videos to camera when add session (#1827)
roomrys Jul 1, 2024
475283d
Overwrite the `CameraGroup.optim_points` function (#1845)
roomrys Jul 3, 2024
f82a0d6
Always update non visible points (#1844)
roomrys Jul 9, 2024
16e775d
Use oks score for reprojections (#1836)
roomrys Jul 9, 2024
1bc63ed
Add editing feature for InstanceGroup.name (#1819)
justinvshen Jul 10, 2024
0f9fe25
Enforce projection bounds (#1863)
roomrys Jul 12, 2024
69e00b1
Add highlight for instance group dock (#1860)
justinvshen Jul 26, 2024
f97e19d
Ensure cameras follow same order when (de)serializing
roomrys Oct 4, 2024
b55d4ef
Revert changes to sort cameras before (de)serializing
roomrys Oct 4, 2024
c7185c7
Add color by instance group preference saving (#1999)
justinvshen Nov 1, 2024
62035aa
[Temp MV fix to] add frame to Instance (when using Labels.add_instanc…
roomrys Nov 1, 2024
ef8a496
[MV] Update instance state as frame idx callback (#2012)
roomrys Nov 1, 2024
ef9751a
Check if predicted instance is removed that it's not referenced (#2026)
justinvshen Nov 22, 2024
9775fc2
[MV] Do not sort keys when loading calibration toml (#2038)
roomrys Dec 13, 2024
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
2 changes: 2 additions & 0 deletions sleap/config/shortcuts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ goto next suggestion: Space
goto next track spawn: Ctrl+E
goto next user: Ctrl+U
goto next labeled: Alt+Right
goto next view: V
goto prev suggestion: Shift+Space
goto prev labeled: Alt+Left
goto prev view: Shift+V
learning: Ctrl+L
mark frame: Ctrl+M
new: Ctrl+N
Expand Down
160 changes: 154 additions & 6 deletions sleap/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
frame and instances listed in data view table.
"""


import os
import platform
import random
Expand Down Expand Up @@ -72,9 +71,11 @@
from sleap.gui.web import ReleaseChecker, ping_analytics
from sleap.gui.widgets.docks import (
InstancesDock,
SessionsDock,
SkeletonDock,
SuggestionsDock,
VideosDock,
InstanceGroupDock,
)
from sleap.gui.widgets.slider import set_slider_marks_from_labels
from sleap.gui.widgets.video import QtVideoPlayer
Expand All @@ -86,7 +87,6 @@
from sleap.skeleton import Skeleton
from sleap.util import parse_uri_path


logger = getLogger(__name__)


Expand Down Expand Up @@ -144,6 +144,7 @@ def __init__(
self.state["labeled_frame"] = None
self.state["last_interacted_frame"] = None
self.state["filename"] = None
self.state["session"] = None
self.state["show non-visible nodes"] = prefs["show non-visible nodes"]
self.state["show instances"] = True
self.state["show labels"] = True
Expand Down Expand Up @@ -223,6 +224,7 @@ def closeEvent(self, event):
prefs["color predicted"] = self.state["color predicted"]
prefs["trail shade"] = self.state["trail_shade"]
prefs["share usage data"] = self.state["share usage data"]
prefs["distinctly_color"] = self.state["distinctly_color"]

# Save preferences.
prefs.save()
Expand Down Expand Up @@ -297,6 +299,8 @@ def labels(self, value):
def _initialize_gui(self):
"""Creates menus, dock windows, starts timers to update gui state."""

self.state["distinctly_color"] = prefs["distinctly_color"]

self._create_color_manager()
self._create_video_player()
self.statusBar()
Expand Down Expand Up @@ -326,7 +330,18 @@ def _create_video_player(self):
self.setCentralWidget(self.player)

def switch_frame(video):
"""Jump to last labeled frame"""
"""Maintain the same frame index if available.

If the video is shorter than the current frame index, find the last labeled
frame. If no labeled frame is found, set the frame index to 0.
"""

# If the new video is long enough, stay on the current frame index
current_frame_idx = self.state["frame_idx"]
if video.num_frames > current_frame_idx:
return

# If the new video is not long enough, find last labeled frame or set to 0
last_label = self.labels.find_last(video)
if last_label is not None:
self.state["frame_idx"] = last_label.frame_idx
Expand All @@ -347,9 +362,16 @@ def update_frame_chunk_suggestions(video):
frame_to_spinbox.setMaximum(video.num_frames)
frame_from_spinbox.setMaximum(video.num_frames)

def update_session(video):
"""Update session state for current video."""
if video is not None and len(self.labels.sessions) > 0:
session = self.labels.get_session(video=video)
self.state["session"] = session

self.state.connect(
"video",
callbacks=[
update_session, # Important to update session before other callbacks
switch_frame,
lambda x: self._update_seekbar_marks(),
update_frame_chunk_suggestions,
Expand Down Expand Up @@ -570,6 +592,18 @@ def add_submenu_choices(menu, title, options, key):
"Next Track Spawn Frame",
self.commands.nextTrackFrame,
)
add_menu_item(
goMenu,
"goto next view",
"Next View",
self.commands.nextView,
)
add_menu_item(
goMenu,
"goto prev view",
"Prev View",
self.commands.prevView,
)

goMenu.addSeparator()

Expand Down Expand Up @@ -624,7 +658,7 @@ def prev_vid():
key="palette",
)

distinctly_color_options = ("instances", "nodes", "edges")
distinctly_color_options = ("instance groups", "instances", "nodes", "edges")

add_submenu_choices(
menu=viewMenu,
Expand All @@ -634,7 +668,7 @@ def prev_vid():
)

self.state["palette"] = prefs["palette"]
self.state["distinctly_color"] = "instances"
self.state["distinctly_color"] = prefs["distinctly_color"]

viewMenu.addSeparator()

Expand Down Expand Up @@ -797,9 +831,18 @@ def new_instance_menu_action():
self.commands.deleteFrameLimitPredictions,
)

### Sessions Menu ###

sessionsMenu = self.menuBar().addMenu("Sessions")

self.inst_groups_menu = sessionsMenu.addMenu("Set Instance Group")
self.inst_groups_delete_menu = sessionsMenu.addMenu("Delete Instance Group")
self.state.connect("frame_idx", self._update_sessions_menu)

### Tracks Menu ###

tracksMenu = self.menuBar().addMenu("Tracks")

self.track_menu = tracksMenu.addMenu("Set Instance Track")
add_menu_check_item(
tracksMenu, "propagate track labels", "Propagate Track Labels"
Expand Down Expand Up @@ -1017,9 +1060,11 @@ def _create_dock_windows(self):
"""Create dock windows and connect them to GUI."""

self.videos_dock = VideosDock(self)
self.sessions_dock = SessionsDock(self, tab_with=self.videos_dock)
self.skeleton_dock = SkeletonDock(self, tab_with=self.videos_dock)
self.suggestions_dock = SuggestionsDock(self, tab_with=self.videos_dock)
self.instances_dock = InstancesDock(self, tab_with=self.videos_dock)
self.instance_groups_dock = InstanceGroupDock(self, tab_with=self.videos_dock)

# Bring videos tab forward.
self.videos_dock.wgt_layout.parent().parent().raise_()
Expand Down Expand Up @@ -1079,7 +1124,10 @@ def _update_gui_state(self):
has_selected_node = self.state["selected_node"] is not None
has_selected_edge = self.state["selected_edge"] is not None
has_selected_video = self.state["selected_video"] is not None
has_selected_session = self.state["selected_session"] is not None
has_video = self.state["video"] is not None
has_selected_camcorder = self.state["selected_camera"] is not None
has_selected_unlinked_video = self.state["selected_unlinked_video"] is not None

has_frame_range = bool(self.state["has_frame_range"])
has_unsaved_changes = bool(self.state["has_changes"])
Expand All @@ -1103,6 +1151,7 @@ def _update_gui_state(self):

# Update menus

self.inst_groups_menu.setEnabled(has_selected_instance)
self.track_menu.setEnabled(has_selected_instance)
self.delete_tracks_menu.setEnabled(has_tracks)
self._menu_actions["clear selection"].setEnabled(has_selected_instance)
Expand Down Expand Up @@ -1134,9 +1183,16 @@ def _update_gui_state(self):
self._buttons["show video"].setEnabled(has_selected_video)
self._buttons["remove video"].setEnabled(has_video)
self._buttons["delete instance"].setEnabled(has_selected_instance)
self._buttons["unlink video"].setEnabled(has_selected_camcorder)
self.suggestions_dock.suggestions_form_widget.buttons[
"generate_button"
].setEnabled(has_videos)
self._buttons["remove session"].setEnabled(has_selected_session)
self._buttons["link video"].setEnabled(
has_selected_unlinked_video
and has_selected_camcorder
and has_selected_session
)

# Update overlays
self.overlays["track_labels"].visible = (
Expand All @@ -1162,6 +1218,10 @@ def _has_topic(topic_list):
):
self.plotFrame()

if _has_topic([UpdateTopic.sessions]):
self.sessions_dock.sessions_table.model().items = self.labels.sessions
self.labels._cache.update()

if _has_topic(
[
UpdateTopic.frame,
Expand All @@ -1179,6 +1239,10 @@ def _has_topic(topic_list):

if _has_topic([UpdateTopic.video]):
self.videos_dock.table.model().items = self.labels.videos
self.labels._cache.update()
self.sessions_dock.unlinked_videos_table.model().items = (
self.labels._cache._linkage_of_videos["unlinked"]
)

if _has_topic([UpdateTopic.skeleton]):
self.skeleton_dock.nodes_table.model().items = self.state["skeleton"]
Expand All @@ -1197,6 +1261,7 @@ def _has_topic(topic_list):

if _has_topic([UpdateTopic.project, UpdateTopic.on_frame]):
self.instances_dock.table.model().items = self.state["labeled_frame"]
self._update_instance_group_model()

if _has_topic([UpdateTopic.suggestions]):
self.suggestions_dock.table.model().items = self.labels.suggestions
Expand All @@ -1221,6 +1286,38 @@ def _has_topic(topic_list):

if _has_topic([UpdateTopic.frame, UpdateTopic.project_instances]):
self.state["last_interacted_frame"] = self.state["labeled_frame"]
self._update_sessions_menu()

if _has_topic([UpdateTopic.sessions]):
self.update_cameras_model()
self.update_unlinked_videos_model()
self._update_sessions_menu()
self._update_instance_group_model()

def update_unlinked_videos_model(self):
"""Update the unlinked videos model with the selected session."""
self.sessions_dock.unlinked_videos_table.model().items = (
self.labels._cache._linkage_of_videos["unlinked"]
)

def _update_instance_group_model(self):
"""Update the instance group model with the `InstanceGroup`s in current frame."""

session = self.state["session"]
if session is not None:
frame_idx: int = self.state["frame_idx"]
frame_group = session.frame_groups.get(frame_idx, None)
if frame_group is not None:
self.instance_groups_dock.table.model().items = (
frame_group.instance_groups
)
return

self.instance_groups_dock.table.model().items = []

def update_cameras_model(self):
"""Update the cameras model with the selected session."""
self.sessions_dock.camera_table.model().items = self.state["selected_session"]

def plotFrame(self, *args, **kwargs):
"""Plots (or replots) current frame."""
Expand All @@ -1241,7 +1338,8 @@ def _after_plot_update(self, frame_idx):
# Replot connected views for multi-camera projects
# TODO(LM): Use context.state["session"] in command instead (when implemented)
session = self.labels.get_session(video)
self.commands.triangulateSession(session=session)
if self.state.get("auto_triangulate", False):
self.commands.triangulateSession(session=session)

def _after_plot_change(self, player, frame_idx, selected_inst):
"""Called each time a new frame is drawn."""
Expand Down Expand Up @@ -1333,6 +1431,11 @@ def updateStatusMessage(self, message: Optional[str] = None):
else:
self.statusBar().setStyleSheet("color: black")

if self.state["session"] is not None and current_video is not None:
camera = self.state["session"].get_camera(video=self.state["video"])
if camera is not None:
message += f"{spacer}Camera: {camera.name}"

self.statusBar().showMessage(message)

def resetPrefs(self):
Expand Down Expand Up @@ -1364,6 +1467,51 @@ def _update_track_menu(self):
"New Track", self.commands.addTrack, Qt.CTRL + Qt.Key_0
)

def _update_sessions_menu(self):
"""Update the instance groups menu based on the frame index."""

# Clear menus before adding more items
self.inst_groups_menu.clear()
self.inst_groups_delete_menu.clear()

# Get the session
session = self.state.get("session")
if session is None:
return

# Get the frame group for the current frame
frame_idx = self.state["frame_idx"]
frame_group = session.frame_groups.get(frame_idx, None)
if frame_group is not None:
for inst_group_ind, instance_group in enumerate(
frame_group.instance_groups
):
# Create shortcut key for first 9 groups
key_command = ""
if inst_group_ind < 9:
key_command = Qt.SHIFT + Qt.Key_0 + inst_group_ind + 1

# Update the Set Instance Group menu
self.inst_groups_menu.addAction(
instance_group.name,
lambda x=instance_group: self.commands.setInstanceGroup(x),
key_command,
)

# Update the Delete Instance Group menu
self.inst_groups_delete_menu.addAction(
instance_group.name,
lambda x=instance_group: self.commands.deleteInstanceGroup(
instance_group=x
),
)

self.inst_groups_menu.addAction(
"New Instance Group",
self.commands.addInstanceGroup,
Qt.SHIFT + Qt.Key_0,
)

def _update_seekbar_marks(self):
"""Updates marks on seekbar."""
set_slider_marks_from_labels(
Expand Down
Loading
Loading