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

Add option to group cameras from sessions #2125

Draft
wants to merge 31 commits into
base: liezl/add-gui-elements-for-sessions
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 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
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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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