Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
ffe5582
Imports
Jvshen Feb 27, 2025
3023b10
Add a button to add camera to group
Jvshen Feb 27, 2025
23c9dd6
Add camera to group function
Jvshen Feb 27, 2025
16c7d88
Create Camera Groups table model
Jvshen Feb 27, 2025
0b963be
Create camera groups table
Jvshen Feb 27, 2025
7182b07
Create camera groups table
Jvshen Feb 27, 2025
e646a92
Add camera groups table model to class attributes
Jvshen Feb 27, 2025
fca9e8f
Add camera groups table function and adding and deleteing a camera gr…
Jvshen Feb 27, 2025
baa283b
Add table model for camera groups
Jvshen Feb 27, 2025
5ce4bc4
Merge branch 'liezl/add-gui-elements-for-sessions' of https://github.…
Jvshen Mar 4, 2025
3844141
Update sessions dock to be able to add and delete camera groups and a…
Jvshen Mar 4, 2025
d61ea3c
Initialize camera_groups state variables in App class
Jvshen Mar 4, 2025
4233131
Update on_data_update to sync camera_groups between labels metadata a…
Jvshen Mar 4, 2025
8bb6b99
Add setCameraGroupName method to enable renaming camera groups in table
Jvshen Mar 4, 2025
b508503
Add CameraGroup class, AddCameraGroup, DeleteCameraGroup,
Jvshen Mar 4, 2025
1292cdc
Add camera group methods to CommandContext for executing camera group…
Jvshen Mar 4, 2025
c0c28a4
Change naming and remove unnecessary code
Jvshen Mar 11, 2025
ca06614
Add import
Jvshen Mar 11, 2025
d6bf553
Refactor: Rename function CameraGroup to CameraCategory in CommandCon…
Jvshen Mar 11, 2025
695500d
Refactor: Rename function CameraGroup to CameraCategory in command cl…
Jvshen Mar 11, 2025
0cc94e6
Refactor: Rename function CameraGroupsTableModel to CameraCategoriesT…
Jvshen Mar 11, 2025
83e2dff
Refactor: Rename function CameraGroup to CameraCategory in docks
Jvshen Mar 11, 2025
b16c1af
Add CameraCategory class to cameras.py
Jvshen Mar 11, 2025
1032c83
Add camera_categories attribute
Jvshen Mar 11, 2025
6836608
Add YAML config for Export Labels Package dialog
Jvshen Mar 13, 2025
07de332
Add menu item for Export Labels Package dialog
Jvshen Mar 13, 2025
271ddf1
Add ExportLabelsPackage command for handling dialog-based exports
Jvshen Mar 13, 2025
74c7160
Add execute function for ExportLabelsPackage in CommandContext
Jvshen Mar 13, 2025
b9ce559
Add dialog function for collecting export options
Jvshen Mar 13, 2025
8de15b0
Move export labels dialog into command class and remove export_labels.py
Jvshen Mar 13, 2025
9e6a698
Format change
Jvshen Mar 13, 2025
fd1a93f
Add Camera_category option to exporting
Jvshen Apr 3, 2025
6cf5f0e
formatting
Jvshen Apr 3, 2025
63aae15
Make ExportLabelsPackage be a subclass of ExportDatasetWithImages
Jvshen Apr 3, 2025
8c1839b
Spacing and formatting
Jvshen Apr 3, 2025
f980680
Dialog for exporting labels
Jvshen Apr 3, 2025
918bc9d
make_cattr for cameraCategory
Jvshen Apr 3, 2025
4bdec1b
Filter labels to include frames from cameras in camera categories
Jvshen Apr 3, 2025
4a8afa3
Testing to see if labels store and retrieve camera categories correctly
Jvshen Apr 3, 2025
64c0ec1
add camera category filter to export labels dialog
Jvshen Apr 15, 2025
54b943b
remove old export labels package button
Jvshen Apr 15, 2025
8caa652
remove unused import
Jvshen Apr 15, 2025
0544830
remove extra do_action method
Jvshen Apr 15, 2025
956d3c3
Add back navigation commands comment
Jvshen Apr 15, 2025
c67e634
remove since Labels.camera_categories now always exists
Jvshen Apr 15, 2025
240da9b
Set, add, and populate combobox with available camera categories
Jvshen Apr 15, 2025
986d6a4
Formatting
Jvshen Apr 15, 2025
a2f83f8
Remove unused method
Jvshen Apr 15, 2025
8f550f7
Remove "camera_categories"
Jvshen Apr 17, 2025
5be482b
Change to category
Jvshen Apr 17, 2025
e634f51
Formatting
Jvshen Apr 17, 2025
957cbdf
WIP: Change add_camera_to_category workflow
Jvshen Apr 17, 2025
e7e8936
Formatting
Jvshen Apr 17, 2025
6fd57c8
Make changes to add camera to categories
roomrys Apr 17, 2025
6bb48f9
Make changes to add camera to categories
roomrys Apr 17, 2025
48fcf7f
Reset selection after command
roomrys Apr 21, 2025
5ed8c4e
Fix selected_camera_category not being selected
roomrys Apr 21, 2025
64805b1
Link Delete Category button straight to command
roomrys Apr 21, 2025
34eec48
Remove unused update model method
roomrys Apr 21, 2025
2939eb0
Move create category code to commands
roomrys Apr 21, 2025
1eda317
Rename methods to more appropriate names
roomrys Apr 21, 2025
acdf642
Disable delete category button when there is no selected category
roomrys Apr 21, 2025
e947165
Delegate code to commands instead of dock
roomrys Apr 24, 2025
3b42451
Reorganize method in SessionsDock
roomrys Apr 24, 2025
637ca42
Add remove from category button
Jvshen Apr 24, 2025
721eb32
Add defaults for removeCameraFromCateogory
Jvshen Apr 24, 2025
9ccf2e0
Raise error if camera category or camera is not selected
Jvshen Apr 24, 2025
f4a9045
Add remove from categoyr to update_gui_state
Jvshen Apr 24, 2025
47e0168
Delegate table update to command topic when rename
roomrys Apr 28, 2025
3d56c4b
Rename variable
roomrys Apr 28, 2025
7ae86ea
Add failing camera category load (or save) test
roomrys Apr 28, 2025
f98714e
Redo to_dict and from_dict methods (standalone)
roomrys Apr 28, 2025
115dadf
Test standalone to_dict and from_dict methods
roomrys Apr 28, 2025
dc90117
Redo to and from dict for Labels integration
roomrys Apr 28, 2025
fad7446
Typehint and docstring CameraCategory.make_cattr
roomrys Apr 29, 2025
c208d50
Fix CameraCategory (de)serialization with Labels
roomrys Apr 29, 2025
5a04419
Test CameraCategory (de)serialization with Labels
roomrys Apr 29, 2025
c8e6daa
Lint
roomrys Apr 29, 2025
612ae83
[wip] Filter exported labels by camera category
roomrys Apr 29, 2025
2fa7ebe
[wip] Add test for export labels with category
roomrys Apr 29, 2025
f795e62
Add tests for executing exportlabelspackage and verifying cameras and…
Jvshen May 1, 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
34 changes: 34 additions & 0 deletions sleap/gui/dataviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,40 @@ def item_to_data(self, obj: RecordingSession, item: Camcorder):
video = obj.get_video(item)
return {"camera": item.name, "video": video.filename if video else ""}

class CameraGroupsTableModel(GenericTableModel):
"""Table model for camera groups."""

properties = ("name", "cameras")

def __init__(self, items=None, context=None):
super().__init__(items=items, context=context)
Copy link
Contributor

Choose a reason for hiding this comment

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

Expose the property kwarg:

Suggested change
def __init__(self, items=None, context=None):
super().__init__(items=items, context=context)
def __init__(self, items=None, properties=None, context=None):
super().__init__(items=items, properties=properties, context=context)

# Register for updates
if context and hasattr(context, 'state'):
context.state.connect("camera_groups", self.update_items)

def update_items(self, camera_groups):
"""Update the model when camera groups change."""
self.items = camera_groups
self.beginResetModel()
self.endResetModel()
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh man, yeah I do kinda like this better than what we currently do:

sleap/sleap/gui/app.py

Lines 1216 to 1220 in 6531447

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

. Let's leave it for now (and possibly open a new PR to refactor all the other table updates).


def object_to_items(self, obj):
return obj

def item_to_data(self, obj, item):
return {
"name": item.name,
"cameras": len(item.cameras)
}

def can_set(self, item, key):
return key == "name"

def set_item(self, item, key, value):
if key == "name" and value:
item.name = value
# Mark project as changed
self.context.changestack_push("rename camera group")
Copy link
Contributor

Choose a reason for hiding this comment

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

While we want to call a self.context method here, this is not the one (we haven't created the method we want to call yet).

Suggested change
# Mark project as changed
self.context.changestack_push("rename camera group")
# TODO(JS): Add `CommandContext` method that updates the camera group name
raise NotImplementedError


class InstanceGroupTableModel(GenericTableModel):
"""Table model for displaying all instance groups in a given frame.
Expand Down
182 changes: 182 additions & 0 deletions sleap/gui/widgets/docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
QTabWidget,
QVBoxLayout,
QWidget,
QInputDialog,
QMessageBox,
)

from sleap.gui.dataviews import (
Expand All @@ -34,6 +36,7 @@
CamerasTableModel,
SessionsTableModel,
InstanceGroupTableModel,
CameraGroupsTableModel,
)
from sleap.io.cameras import RecordingSession, FrameGroup, InstanceGroup
from sleap.gui.dialogs.formbuilder import YamlFormWidget
Expand Down Expand Up @@ -584,13 +587,15 @@ def __init__(
self.sessions_model_type = SessionsTableModel
self.camera_model_type = CamerasTableModel
self.unlinked_videos_model_type = VideosTableModel
self.camera_groups_model_type = CameraGroupsTableModel
super().__init__(
name="Sessions",
main_window=main_window,
model_type=[
self.sessions_model_type,
self.camera_model_type,
self.unlinked_videos_model_type,
self.camera_groups_model_type,
],
tab_with=tab_with,
)
Expand Down Expand Up @@ -625,10 +630,52 @@ def create_video_unlink_button(self) -> QWidget:
hb, "Unlink Video", main_window.commands.unlink_video_from_camera
)

self.add_button(hb, "Add to Group", self._add_camera_to_group)

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

def _add_camera_to_group(self):
"""Add the selected camera to a group."""
camera = self.main_window.state.get("camera")
if not camera:
QMessageBox.information(
self.main_window,
"No Camera Selected",
"Please select a camera to add to a group."
)
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of raising an error message, maybe we can just disable the button so no one can click on it if there is no camera selected. See this example for the "Link Video" button:

sleap/sleap/gui/app.py

Lines 1191 to 1195 in d898a84

self._buttons["link video"].setEnabled(
has_selected_unlinked_video
and has_selected_camcorder
and has_selected_session
)

.

Suggested change
if not camera:
QMessageBox.information(
self.main_window,
"No Camera Selected",
"Please select a camera to add to a group."
)
return


# Get available groups
if not self.main_window.state.get("camera_groups") or len(self.main_window.state["camera_groups"]) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

GuiState has a __getitem__ method, so we at least won't run into an error if the key doesn't exist.

sleap/sleap/gui/state.py

Lines 52 to 54 in 6531447

def __getitem__(self, key: GSVarType) -> Any:
"""Gets value for key, or None if no value."""
return self.get(key, default=None)

How about we use get, but with a default of an empty list:

Suggested change
if not self.main_window.state.get("camera_groups") or len(self.main_window.state["camera_groups"]) == 0:
if len(self.main_window.state.get("camera_groups", []) == 0:

Copy link
Contributor

Choose a reason for hiding this comment

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

ex:

In [20]: from sleap.gui.state import GuiState

In [21]: state = GuiState()

In [22]: state[3]

In [23]: state.get(3, [])
Out[23]: []

reply = QMessageBox.question(
self.main_window,
"No Groups",
"No camera groups exist. Would you like to create one?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self._create_camera_group()
return

# Show group selection dialog
group_names = [group.name for group in self.main_window.state["camera_groups"]]
selected_name, ok = QInputDialog.getItem(
self.main_window,
"Select Group",
"Choose a group to add the camera to:",
group_names,
0,
False
)

if ok and selected_name:
selected_idx = group_names.index(selected_name)
selected_group = self.main_window.state["camera_groups"][selected_idx]
self.main_window.commands.addCameraToGroup(camera, selected_group)

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

Expand All @@ -650,11 +697,15 @@ def create_models(self) -> Union[GenericTableModel, Dict[str, GenericTableModel]
self.unlinked_videos_model = self.unlinked_videos_model_type(
items=main_window.state["selected_session"], context=main_window.commands
)
self.camera_groups_model = self.camera_groups_model_type(
items=main_window.state.get("camera_groups", []), context=main_window.commands
)

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

Expand All @@ -681,6 +732,14 @@ def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:
ellipsis_left=True,
)

# Create camera groups table
self.camera_groups_table = GenericTableView(
is_activatable=True,
state=main_window.state,
row_name="selected_camera_group",
model=self.camera_groups_model,
)

self.main_window.state.connect(
"selected_session", self.main_window.update_cameras_model
)
Expand All @@ -689,10 +748,15 @@ def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:
"selected_session", self.main_window.update_unlinked_videos_model
)

self.main_window.state.connect(
"camera_groups", self._update_camera_groups_model
)

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

Expand Down Expand Up @@ -736,6 +800,124 @@ def lay_everything_out(self) -> None:
video_link_button = self.create_video_link_button()
self.wgt_layout.addWidget(video_link_button)

camera_groups_container = self.create_camera_groups_table()
self.wgt_layout.addWidget(camera_groups_container)

def create_camera_groups_table(self) -> QWidget:
"""Create the camera groups table and buttons."""
main_window = self.main_window

# Create a container widget with a title
container = QGroupBox("Camera Groups")
container_layout = QVBoxLayout()
container.setLayout(container_layout)

# Create the camera groups table
if not hasattr(self, 'camera_groups_model') or self.camera_groups_model is None:
# Create the model if it doesn't exist
self.camera_groups_model = self.camera_groups_model_type(
items=main_window.state.get("camera_groups", []),
context=main_window.commands
)

# Create the camera groups table
self.camera_groups_table = GenericTableView(
is_activatable=True,
state=main_window.state,
row_name="selected_camera_group",
model=self.camera_groups_model,
)
container_layout.addWidget(self.camera_groups_table)

# Create buttons for camera groups
hb = QHBoxLayout()
self.add_button(hb, "Create Group", self._create_camera_group)
self.add_button(hb, "Delete Group", self._delete_camera_group)

hbw = QWidget()
hbw.setLayout(hb)
container_layout.addWidget(hbw)

# Connect state changes to model updates
main_window.state.connect("camera_groups", self._update_camera_groups_model)

return container

def _update_camera_groups_model(self, camera_groups):
"""Update the camera groups model when the state changes."""
if hasattr(self, 'camera_groups_model') and self.camera_groups_model:
self.camera_groups_model.items = camera_groups
self.camera_groups_model.beginResetModel()
self.camera_groups_model.endResetModel()
print(f"Updated camera groups model with {len(camera_groups)} groups")

def create_add_to_group_button(self) -> QWidget:
"""Create the 'Add to Group' button."""
hb = QHBoxLayout()
self.add_button(hb, "Add to Group", self._add_camera_to_group)

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

def _create_camera_group(self):
"""Create a new camera group."""
name, ok = QInputDialog.getText(
self.main_window, "New Camera Group", "Enter name for camera group:"
)
if ok and name:
self.main_window.commands.addCameraGroup(name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.main_window.commands.addCameraGroup(name)
# TODO(JS): self.main_window.commands.addCameraGroup(name)
raise NotImplementedError

# Force update the table
if hasattr(self, 'camera_groups_table') and self.camera_groups_table:
self.camera_groups_table.model().beginResetModel()
self.camera_groups_table.model().endResetModel()

def _delete_camera_group(self):
"""Delete the selected camera group."""
camera_group = self.main_window.state["selected_camera_group"]
if camera_group:
self.main_window.commands.deleteCameraGroup(camera_group)

def _add_camera_to_group(self):
"""Add the selected camera to a group."""
camera = self.main_window.state.get("camera")
if not camera:
QMessageBox.information(
self.main_window,
"No Camera Selected",
"Please select a camera to add to a group."
)
return

# Get available groups
if not self.main_window.state.get("camera_groups") or len(self.main_window.state["camera_groups"]) == 0:
reply = QMessageBox.question(
self.main_window,
"No Groups",
"No camera groups exist. Would you like to create one?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self._create_camera_group()
return

# Show group selection dialog
group_names = [group.name for group in self.main_window.state["camera_groups"]]
selected_name, ok = QInputDialog.getItem(
self.main_window,
"Select Group",
"Choose a group to add the camera to:",
group_names,
0,
False
)

if ok and selected_name:
selected_idx = group_names.index(selected_name)
selected_group = self.main_window.state["camera_groups"][selected_idx]
self.main_window.commands.addCameraToGroup(camera, selected_group)
Copy link
Contributor

Choose a reason for hiding this comment

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

This entire method is a duplicate

Suggested change
def _add_camera_to_group(self):
"""Add the selected camera to a group."""
camera = self.main_window.state.get("camera")
if not camera:
QMessageBox.information(
self.main_window,
"No Camera Selected",
"Please select a camera to add to a group."
)
return
# Get available groups
if not self.main_window.state.get("camera_groups") or len(self.main_window.state["camera_groups"]) == 0:
reply = QMessageBox.question(
self.main_window,
"No Groups",
"No camera groups exist. Would you like to create one?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self._create_camera_group()
return
# Show group selection dialog
group_names = [group.name for group in self.main_window.state["camera_groups"]]
selected_name, ok = QInputDialog.getItem(
self.main_window,
"Select Group",
"Choose a group to add the camera to:",
group_names,
0,
False
)
if ok and selected_name:
selected_idx = group_names.index(selected_name)
selected_group = self.main_window.state["camera_groups"][selected_idx]
self.main_window.commands.addCameraToGroup(camera, selected_group)



class InstanceGroupDock(DockWidget):
"""Dock widget for displaying instance groups."""
Expand Down
Loading