Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d681075
Added CamerasTable, SessionDocks and EditCommand
AdvaithRavishankar Jan 22, 2024
33c4bcb
added updates to app, commands and docks
AdvaithRavishankar Jan 23, 2024
d482bb4
Fixed bugs, drafted tests
AdvaithRavishankar Jan 23, 2024
7f74a8b
Added minor debug fixes to CamerasTableModel
AdvaithRavishankar Jan 23, 2024
3bb071f
Added Initial Tests
AdvaithRavishankar Jan 29, 2024
e24edc9
changed button to be disabled and made table in sessions docks keys
AdvaithRavishankar Jan 29, 2024
88cd019
fixed bugs with table display
AdvaithRavishankar Jan 29, 2024
a728dd4
fixed button functionality
AdvaithRavishankar Feb 6, 2024
f1b6b1b
fixed table updating
AdvaithRavishankar Feb 8, 2024
548caf5
pushed tests draft
AdvaithRavishankar Feb 8, 2024
33d2754
Use GuiState to determine current session
roomrys Mar 16, 2024
f414abb
Nit: Use camera_table instead of table["camera_table"]
roomrys Mar 16, 2024
dd3daae
Use GuiState instead of camcorder sessions list
roomrys Mar 16, 2024
45974e3
Nit: docstring format and lint
roomrys Mar 16, 2024
4f40b6e
Change row name to "camera"
roomrys Mar 16, 2024
47dfe95
Display all cameras (not just unlinked)
roomrys Mar 16, 2024
20d3e60
Reset selected camera to None after unlink video
roomrys Mar 16, 2024
ba0f556
Fix-up cameras table test
roomrys Mar 16, 2024
c493fd6
Fix-up docks test
Mar 18, 2024
907f873
Remove unused imports
Mar 18, 2024
c287df2
Merge branch 'liezl/add-gui-elements-for-sessions' of https://github.…
Mar 18, 2024
45efdab
Fix segmentation fault
Mar 18, 2024
cd8662e
Reorder buttons and tab sessions dock with others
Mar 18, 2024
25e848c
Lint
Mar 18, 2024
236d044
Update cameras model when change in selected session
Mar 18, 2024
4ecbb95
Add missing import
Mar 18, 2024
77283d0
Use "selected_session" in UnlinkVideo command
Mar 18, 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
12 changes: 11 additions & 1 deletion sleap/gui/app.py
Original file line number Diff line number Diff line change
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 @@ -1029,7 +1030,7 @@ def _create_dock_windows(self):
"""Create dock windows and connect them to GUI."""

self.videos_dock = VideosDock(self)
self.sessions_dock = SessionsDock(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)
Expand Down Expand Up @@ -1094,6 +1095,7 @@ def _update_gui_state(self):
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_frame_range = bool(self.state["has_frame_range"])
has_unsaved_changes = bool(self.state["has_changes"])
Expand Down Expand Up @@ -1148,6 +1150,7 @@ 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)
Expand Down Expand Up @@ -1240,6 +1243,13 @@ def _has_topic(topic_list):
if _has_topic([UpdateTopic.frame, UpdateTopic.project_instances]):
self.state["last_interacted_frame"] = self.state["labeled_frame"]

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

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."""
if self.state["video"] is None:
Expand Down
23 changes: 23 additions & 0 deletions sleap/gui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,10 @@ def openPrereleaseVersion(self):
"""Open the current prerelease version."""
self.execute(OpenPrereleaseVersion)

def unlink_video_from_camera(self):
"""Unlinks video from a camera"""
self.execute(UnlinkVideo)


# File Commands

Expand Down Expand Up @@ -702,6 +706,8 @@ def do_action(context: "CommandContext", params: dict):
if len(labels.videos):
context.state["video"] = labels.videos[0]

context.state["session"] = labels.sessions[0] if len(labels.sessions) else None

context.state["project_loaded"] = True
context.state["has_changes"] = params.get("changed_on_load", False) or (
filename is None
Expand Down Expand Up @@ -3923,3 +3929,20 @@ def copy_to_clipboard(text: str):
clipboard = QtWidgets.QApplication.clipboard()
clipboard.clear(mode=clipboard.Clipboard)
clipboard.setText(text, mode=clipboard.Clipboard)


class UnlinkVideo(EditCommand):
topics = [UpdateTopic.sessions]

@staticmethod
def do_action(context: CommandContext, params: dict):
camcorder = context.state["selected_camera"]
recording_session = context.state["selected_session"]

video = camcorder.get_video(recording_session)

if video is not None and recording_session is not None:
recording_session.remove_video(video)

# Reset the selected camera
context.state["selected_camera"] = None
20 changes: 20 additions & 0 deletions sleap/gui/dataviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from sleap.io.dataset import Labels
from sleap.instance import LabeledFrame, Instance
from sleap.skeleton import Skeleton
from sleap.io.cameras import Camcorder, RecordingSession


class GenericTableModel(QtCore.QAbstractTableModel):
Expand Down Expand Up @@ -662,3 +663,22 @@ def columnCount(self, parent):
def flags(self, index: QtCore.QModelIndex):
"""Overrides Qt method, returns flags (editable etc)."""
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable


class CamerasTableModel(GenericTableModel):
"""Table model for unlinking `Camcorder`s and `Video`s within a `RecordingSession`.

Args:
obj: 'RecordingSession' which has information of cameras
and paired video
"""

properties = ("camera", "video")

def object_to_items(self, obj: RecordingSession):
return obj.camera_cluster.cameras

def item_to_data(self, obj: RecordingSession, item: Camcorder):

video = obj.get_video(item)
return {"camera": item.name, "video": video.filename if video else ""}
60 changes: 55 additions & 5 deletions sleap/gui/widgets/docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
SkeletonNodesTableModel,
SuggestionsTableModel,
VideosTableModel,
CamerasTableModel,
SessionsTableModel,
)
from sleap.gui.dialogs.formbuilder import YamlFormWidget
Expand Down Expand Up @@ -573,9 +574,18 @@ def create_table_edit_buttons(self) -> QWidget:


class SessionsDock(DockWidget):
def __init__(self, main_window: Optional[QMainWindow]):
def __init__(
self,
main_window: Optional[QMainWindow],
tab_with: Optional[QLayout] = None,
):
self.sessions_model_type = SessionsTableModel
self.camera_model_type = CamerasTableModel
super().__init__(
name="Sessions", main_window=main_window, model_type=SessionsTableModel
name="Sessions",
main_window=main_window,
model_type=[self.sessions_model_type, self.camera_model_type],
tab_with=tab_with,
)

def create_triangulation_options(self) -> QWidget:
Expand All @@ -600,13 +610,31 @@ def create_triangulation_options(self) -> QWidget:
hbw.setLayout(hb)
return hbw

def create_video_unlink_button(self) -> QWidget:
main_window = self.main_window

hb = QHBoxLayout()
self.add_button(
hb, "Unlink Video", main_window.commands.unlink_video_from_camera
)

hbw = QWidget()
hbw.setLayout(hb)
return hbw

def create_models(self) -> Union[GenericTableModel, Dict[str, GenericTableModel]]:
main_window = self.main_window
self.sessions_model = self.model_type(
self.sessions_model = self.sessions_model_type(
items=main_window.state["labels"].sessions, context=main_window.commands
)
self.camera_model = self.camera_model_type(
items=main_window.state["selected_session"], context=main_window.commands
)

self.model = {"sessions_model": self.sessions_model}
self.model = {
"sessions_model": self.sessions_model,
"camera_model": self.camera_model,
}
return self.model

def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:
Expand All @@ -617,8 +645,20 @@ def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:
self.sessions_table = GenericTableView(
state=main_window.state, row_name="session", model=self.sessions_model
)
self.camera_table = GenericTableView(
state=main_window.state,
row_name="camera",
model=self.camera_model,
)

self.main_window.state.connect(
"selected_session", self.main_window.update_cameras_model
)

self.table = {"sessions_table": self.sessions_table}
self.table = {
"sessions_table": self.sessions_table,
"camera_table": self.camera_table,
}
return self.table

def create_table_edit_buttons(self) -> QWidget:
Expand All @@ -638,10 +678,20 @@ def lay_everything_out(self) -> None:
if self.table is None:
self.create_tables()

# TODO(LM): Add this to a create method
# Add the sessions table to the dock
self.wgt_layout.addWidget(self.sessions_table)

table_edit_buttons = self.create_table_edit_buttons()
self.wgt_layout.addWidget(table_edit_buttons)

# TODO(LM): Add this to a create method
# Add the cameras table to the dock
self.wgt_layout.addWidget(self.camera_table)

video_unlink_button = self.create_video_unlink_button()
self.wgt_layout.addWidget(video_unlink_button)

# Add the triangulation options to the dock
triangulation_options = self.create_triangulation_options()
self.wgt_layout.addWidget(triangulation_options)
4 changes: 2 additions & 2 deletions sleap/io/cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def sessions(self) -> List["RecordingSession"]:

def get_video(self, session: "RecordingSession") -> Optional[Video]:
if session not in self._video_by_session:
logger.warning(f"{session} not found in {self}.")
logger.debug(f"{session} not found in {self}.")
return None
return self._video_by_session[session]

Expand Down Expand Up @@ -463,7 +463,7 @@ def get_video(self, camcorder: Camcorder) -> Optional[Video]:
)

if camcorder not in self._video_by_camcorder:
logger.warning(
logger.debug(
f"Camcorder {camcorder.name} is not linked to a video in this "
f"RecordingSession."
)
Expand Down
54 changes: 54 additions & 0 deletions tests/gui/test_dataviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,57 @@ def test_table_sort_string(qtbot):
# Make sure we can sort with both numbers and strings (i.e., "")
table.model().sort(0)
table.model().sort(1)


def test_camera_table(qtbot, multiview_min_session_labels):

session = multiview_min_session_labels.sessions[0]
camcorders = session.camera_cluster.cameras

table_model = CamerasTableModel(items=session)
num_rows = table_model.rowCount()

assert table_model.columnCount() == 2
assert num_rows == len(camcorders)

table = GenericTableView(
row_name="camera",
model=table_model,
)

# Test if all comcorders are presented in the correct row
for i in range(num_rows):
table.selectRow(i)

# Check first column
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name

# Check second column
index = table.model().index(i, 1)
linked_video_filename = camcorders[i].get_video(session).filename
assert table.model().data(index) == linked_video_filename

# Test if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
multiview_min_session_labels.sessions[0].remove_video(
camcorders[idx].get_video(multiview_min_session_labels.sessions[0])
)
table.model().items = session

for i in range(num_rows):
table.selectRow(i)

# Check first column
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name

# Check second column
index = table.model().index(i, 1)
linked_video = camcorders[i].get_video(session)
if i in idxs_to_remove:
assert table.model().data(index) == ""
else:
linked_video_filename = linked_video.filename
assert table.model().data(index) == linked_video_filename
57 changes: 52 additions & 5 deletions tests/gui/widgets/test_docks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Module for testing dock widgets for the `MainWindow`."""

from pathlib import Path

import pytest

from sleap import Labels, Video
from sleap.gui.app import MainWindow
from sleap.gui.commands import OpenSkeleton
Expand Down Expand Up @@ -120,15 +122,60 @@ def test_sessions_dock(qtbot):
assert dock.wgt_layout is dock.widget().layout()


def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels):
labels = multiview_min_session_labels
session = labels.sessions[0]
camcorders = session.camera_cluster.cameras
main_window = MainWindow(labels=labels)
assert main_window.state["session"] == session

dock = main_window.sessions_dock
table = dock.camera_table

# Testing if cameras_table is loaded correctly

# Test if all comcorders are presented in the correct row
Copy link

Choose a reason for hiding this comment

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

There's a typographical error in the comment: "comcorders" should be corrected to "camcorders" for clarity and correctness.

-    # Test if all comcorders are presented in the correct row
+    # Test if all camcorders are presented in the correct row

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
# Test if all comcorders are presented in the correct row
# Test if all camcorders are presented in the correct row

for i, cam in enumerate(camcorders):
table.selectRow(i)

# Check first column
assert table.getSelectedRowItem() == cam
assert table.model().data(table.currentIndex()) == cam.name

# Check second column
index = table.model().index(i, 1)
linked_video_filename = cam.get_video(session).filename
assert table.model().data(index) == linked_video_filename

# Test if a comcorder change is reflected
Copy link

Choose a reason for hiding this comment

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

Another typographical error: "comcorder" should be corrected to "camcorder".

-    # Test if a comcorder change is reflected
+    # Test if a camcorder change is reflected

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
# Test if a comcorder change is reflected
# Test if a camcorder change is reflected

idxs_to_remove = [1, 2, 7]
Copy link

Choose a reason for hiding this comment

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

The use of magic numbers in idxs_to_remove = [1, 2, 7] should be documented or derived programmatically to improve readability and maintainability.

Consider documenting the rationale behind the choice of indices in idxs_to_remove or deriving them programmatically to avoid magic numbers.

for idx in idxs_to_remove:
main_window.state["selected_camera"] = camcorders[idx]
main_window._buttons["unlink video"].click()

for i, cam in enumerate(camcorders):
table.selectRow(i)

# Check first column
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name

# Check second column
index = table.model().index(i, 1)
linked_video = camcorders[i].get_video(session)
if i in idxs_to_remove:
assert table.model().data(index) == ""
else:
linked_video_filename = linked_video.filename
assert table.model().data(index) == linked_video_filename
Comment on lines +125 to +170
Copy link

Choose a reason for hiding this comment

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

The test function test_sessions_dock_cameras_table is well-structured and covers important aspects of the SessionsDock cameras table functionality. However, there are a few areas where improvements can be made:

  1. Typographical Error: There's a minor typographical error in the comments. The word "comcorders" should be corrected to "camcorders" for clarity and correctness. This occurs in lines 134 and 147.

  2. Test Coverage for Unlink Video: The test covers the scenario where videos are unlinked from camcorders, but it might be beneficial to also verify that the UnlinkVideo command is correctly updating the underlying data model. This could involve checking that the session object reflects the changes made by the UnlinkVideo command.

  3. Magic Numbers: The test uses magic numbers (e.g., idxs_to_remove = [1, 2, 7]) without explanation. It would improve readability and maintainability to either document why these specific indices are chosen or to derive these indices programmatically based on the test setup.

  4. Error Handling: Consider adding tests for error scenarios, such as attempting to unlink a video from a camcorder that doesn't have a linked video. This would ensure robustness in handling edge cases.

-    # Test if all comcorders are presented in the correct row
+    # Test if all camcorders are presented in the correct row

Consider enhancing the test to verify that the UnlinkVideo command updates the underlying data model as expected.

Document the rationale behind the choice of indices in idxs_to_remove or derive them programmatically to avoid magic numbers.

Would you like assistance in adding tests for error scenarios, such as handling unlink operations for camcorders without linked videos?


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels):
labels = multiview_min_session_labels
session = labels.sessions[0]
camcorders = session.camera_cluster.cameras
main_window = MainWindow(labels=labels)
assert main_window.state["session"] == session
dock = main_window.sessions_dock
table = dock.camera_table
# Testing if cameras_table is loaded correctly
# Test if all comcorders are presented in the correct row
for i, cam in enumerate(camcorders):
table.selectRow(i)
# Check first column
assert table.getSelectedRowItem() == cam
assert table.model().data(table.currentIndex()) == cam.name
# Check second column
index = table.model().index(i, 1)
linked_video_filename = cam.get_video(session).filename
assert table.model().data(index) == linked_video_filename
# Test if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
main_window.state["selected_camera"] = camcorders[idx]
main_window._buttons["unlink video"].click()
for i, cam in enumerate(camcorders):
table.selectRow(i)
# Check first column
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name
# Check second column
index = table.model().index(i, 1)
linked_video = camcorders[i].get_video(session)
if i in idxs_to_remove:
assert table.model().data(index) == ""
else:
linked_video_filename = linked_video.filename
assert table.model().data(index) == linked_video_filename
def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels):
labels = multiview_min_session_labels
session = labels.sessions[0]
camcorders = session.camera_cluster.cameras
main_window = MainWindow(labels=labels)
assert main_window.state["session"] == session
dock = main_window.sessions_dock
table = dock.camera_table
# Testing if cameras_table is loaded correctly
# Test if all camcorders are presented in the correct row
for i, cam in enumerate(camcorders):
table.selectRow(i)
# Check first column
assert table.getSelectedRowItem() == cam
assert table.model().data(table.currentIndex()) == cam.name
# Check second column
index = table.model().index(i, 1)
linked_video_filename = cam.get_video(session).filename
assert table.model().data(index) == linked_video_filename
# Test if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
main_window.state["selected_camera"] = camcorders[idx]
main_window._buttons["unlink video"].click()
for i, cam in enumerate(camcorders):
table.selectRow(i)
# Check first column
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name
# Check second column
index = table.model().index(i, 1)
linked_video = camcorders[i].get_video(session)
if i in idxs_to_remove:
assert table.model().data(index) == ""
else:
linked_video_filename = linked_video.filename
assert table.model().data(index) == linked_video_filename



def test_sessions_dock_session_table(qtbot, multiview_min_session_labels):
"""Test the SessionsDock.sessions_table."""

# Create dock
main_window = MainWindow()
SessionsDock(main_window)

# Loading label file
main_window.commands.loadLabelsObject(multiview_min_session_labels)
labels = multiview_min_session_labels
main_window = MainWindow(labels=labels)

# Testing if sessions table is loaded correctly
sessions = multiview_min_session_labels.sessions
Expand Down