Skip to content

Commit 38005ab

Browse files
committed
Add color/colormap UI and refactor layouts
Introduce a new color_dropdowns module to provide colormap and bbox-color QComboBox helpers (gradient swatches, matplotlib registry integration, and enum-based BGR swatches). Refactor layouts.make_two_field_row (renamed and enhanced) to support flexible key/value pairs, styling, and optional spacing, and add enable_combo_shrink_to_current to size combos to their current item. Integrate these into the main window: wire a Visualization group with colormap and bbox color controls, use the new helpers to populate and manage combo state, add _on_colormap_changed, and update bbox color handling. Also update ui_blocks to use make_two_field_row, tweak several UI labels/rows, comment out the PYLON emulation env var, and change several CameraConfigDialog log statements from INFO to DEBUG for less noisy logging.
1 parent 262d558 commit 38005ab

5 files changed

Lines changed: 553 additions & 92 deletions

File tree

dlclivegui/gui/camera_config/camera_config_dialog.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ def _on_active_camera_selected(self, row: int) -> None:
586586
LOGGER.debug("[Selection] Suppressed currentRowChanged event at index %d.", row)
587587
return
588588
prev_row = self._current_edit_index
589-
LOGGER.info(
589+
LOGGER.debug(
590590
"[Select] row=%s prev=%s preview_state=%s",
591591
row,
592592
prev_row,
@@ -878,7 +878,7 @@ def _apply_camera_settings(self) -> bool:
878878
self._working_settings.cameras[row] = new_model
879879
self._update_active_list_item(row, new_model)
880880

881-
LOGGER.info(
881+
LOGGER.debug(
882882
"[Apply] backend=%s idx=%s changes=%s",
883883
getattr(new_model, "backend", None),
884884
getattr(new_model, "index", None),
@@ -903,7 +903,7 @@ def _apply_camera_settings(self) -> bool:
903903
if should_consider_restart:
904904
restart = self._should_restart_preview(old_settings, new_model)
905905

906-
LOGGER.info(
906+
LOGGER.debug(
907907
"[Apply] preview_state=%s restart=%s backend=%s idx=%s",
908908
self._preview.state,
909909
restart,
@@ -1237,7 +1237,7 @@ def _begin_preview_load(self, cam: CameraSettings, *, reason: str) -> None:
12371237
- Creates and wires loader
12381238
- Sets requested_cam
12391239
"""
1240-
LOGGER.info("[Preview] begin load reason=%s backend=%s idx=%s", reason, cam.backend, cam.index)
1240+
LOGGER.debug("[Preview] begin load reason=%s backend=%s idx=%s", reason, cam.backend, cam.index)
12411241

12421242
# If already loading, just coalesce restart/intention
12431243
if self._preview.state == PreviewState.LOADING:
@@ -1292,7 +1292,7 @@ def _start_preview(self) -> None:
12921292
return
12931293

12941294
self._current_edit_index = row
1295-
LOGGER.info(
1295+
LOGGER.debug(
12961296
"[Preview] resolved start row=%s active_row=%s",
12971297
self._current_edit_index,
12981298
self.active_cameras_list.currentRow(),
@@ -1313,7 +1313,7 @@ def _stop_preview(self) -> None:
13131313

13141314
def _stop_preview_internal(self, *, reason: str) -> None:
13151315
"""Tear down loader/backend/timer. Safe to call from anywhere."""
1316-
LOGGER.info("[Preview] stop reason=%s state=%s", reason, self._preview.state)
1316+
LOGGER.debug("[Preview] stop reason=%s state=%s", reason, self._preview.state)
13171317

13181318
self._preview.state = PreviewState.STOPPING
13191319

dlclivegui/gui/camera_config/ui_blocks.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ...cameras import CameraFactory
3232
from ..misc.drag_spinbox import ScrubSpinBox
3333
from ..misc.eliding_label import ElidingPathLabel
34-
from ..misc.layouts import _make_two_field_row
34+
from ..misc.layouts import make_two_field_row
3535

3636
if TYPE_CHECKING:
3737
from camera_config_dialog import CameraConfigDialog
@@ -250,7 +250,7 @@ def build_settings_group(dlg: CameraConfigDialog) -> QGroupBox:
250250
dlg.cam_index_label = QLabel("0")
251251

252252
dlg.cam_backend_label = QLabel("opencv")
253-
id_backend_row = _make_two_field_row(
253+
id_backend_row = make_two_field_row(
254254
"Index:",
255255
dlg.cam_index_label,
256256
"Backend:",
@@ -267,7 +267,7 @@ def build_settings_group(dlg: CameraConfigDialog) -> QGroupBox:
267267
dlg.detected_fps_label = QLabel("—")
268268
dlg.detected_fps_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
269269

270-
detected_row = _make_two_field_row(
270+
detected_row = make_two_field_row(
271271
"Detected resolution:",
272272
dlg.detected_resolution_label,
273273
"Detected FPS:",
@@ -288,7 +288,7 @@ def build_settings_group(dlg: CameraConfigDialog) -> QGroupBox:
288288
dlg.cam_height.setValue(0)
289289
dlg.cam_height.setSpecialValueText("Auto")
290290

291-
res_row = _make_two_field_row("W", dlg.cam_width, "H", dlg.cam_height, key_width=30)
291+
res_row = make_two_field_row("W", dlg.cam_width, "H", dlg.cam_height, key_width=30)
292292
dlg.settings_form.addRow("Resolution:", res_row)
293293

294294
# --- FPS + Rotation grouped ---
@@ -305,7 +305,7 @@ def build_settings_group(dlg: CameraConfigDialog) -> QGroupBox:
305305
dlg.cam_rotation.addItem("180°", 180)
306306
dlg.cam_rotation.addItem("270°", 270)
307307

308-
fps_rot_row = _make_two_field_row("FPS", dlg.cam_fps, "Rot", dlg.cam_rotation, key_width=30)
308+
fps_rot_row = make_two_field_row("FPS", dlg.cam_fps, "Rot", dlg.cam_rotation, key_width=30)
309309
dlg.settings_form.addRow("Capture:", fps_rot_row)
310310

311311
# --- Exposure + Gain grouped ---
@@ -321,7 +321,7 @@ def build_settings_group(dlg: CameraConfigDialog) -> QGroupBox:
321321
dlg.cam_gain.setSpecialValueText("Auto")
322322
dlg.cam_gain.setDecimals(2)
323323

324-
exp_gain_row = _make_two_field_row("Exp", dlg.cam_exposure, "Gain", dlg.cam_gain, key_width=30)
324+
exp_gain_row = make_two_field_row("Exp", dlg.cam_exposure, "Gain", dlg.cam_gain, key_width=30)
325325
dlg.settings_form.addRow("Analog:", exp_gain_row)
326326

327327
# --- Crop row ---

dlclivegui/gui/main_window.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import time
1010
from pathlib import Path
1111

12-
os.environ["PYLON_CAMEMU"] = "2"
13-
12+
# NOTE @C-Achard: his could be added in settings eventually
13+
# Forces pypylon to create 2 emulation virtual cameras,
14+
# mostly for testing. This shold not be enabled for release.
15+
# os.environ["PYLON_CAMEMU"] = "2"
1416
import cv2
1517
import numpy as np
1618
from PySide6.QtCore import QRect, QSettings, Qt, QTimer, QUrl
@@ -72,6 +74,8 @@
7274
from ..utils.stats import format_dlc_stats
7375
from ..utils.utils import FPSTracker
7476
from .camera_config.camera_config_dialog import CameraConfigDialog
77+
from .misc import color_dropdowns as color_ui
78+
from .misc import layouts as lyts
7579
from .misc.drag_spinbox import ScrubSpinBox
7680
from .misc.eliding_label import ElidingPathLabel
7781
from .recording_manager import RecordingManager
@@ -306,7 +310,7 @@ def _setup_ui(self) -> None:
306310
controls_layout.addWidget(self._build_camera_group())
307311
controls_layout.addWidget(self._build_dlc_group())
308312
controls_layout.addWidget(self._build_recording_group())
309-
controls_layout.addWidget(self._build_bbox_group())
313+
controls_layout.addWidget(self._build_viz_group())
310314

311315
# Preview/Stop buttons at bottom of controls - wrap in widget
312316
button_bar_widget = QWidget()
@@ -378,7 +382,7 @@ def _build_menus(self) -> None:
378382
self._init_theme_actions()
379383

380384
def _build_camera_group(self) -> QGroupBox:
381-
group = QGroupBox("Camera settings")
385+
group = QGroupBox("Camera")
382386
form = QFormLayout(group)
383387

384388
# Camera config button - opens dialog for all camera configuration
@@ -397,7 +401,7 @@ def _build_camera_group(self) -> QGroupBox:
397401
return group
398402

399403
def _build_dlc_group(self) -> QGroupBox:
400-
group = QGroupBox("DLCLive settings")
404+
group = QGroupBox("DLCLive")
401405
form = QFormLayout(group)
402406

403407
path_layout = QHBoxLayout()
@@ -443,7 +447,7 @@ def _build_dlc_group(self) -> QGroupBox:
443447
# form.addRow("Additional options", self.additional_options_edit)
444448
self.dlc_camera_combo = QComboBox()
445449
self.dlc_camera_combo.setToolTip("Select which camera to use for pose inference")
446-
form.addRow("Inference Camera", self.dlc_camera_combo)
450+
form.addRow("Inference camera", self.dlc_camera_combo)
447451

448452
# Wrap inference buttons in a widget to prevent shifting
449453
inference_button_widget = QWidget()
@@ -461,9 +465,9 @@ def _build_dlc_group(self) -> QGroupBox:
461465
inference_buttons.addWidget(self.stop_inference_button)
462466
form.addRow(inference_button_widget)
463467

464-
self.show_predictions_checkbox = QCheckBox("Display pose predictions")
465-
self.show_predictions_checkbox.setChecked(True)
466-
form.addRow(self.show_predictions_checkbox)
468+
# self.show_predictions_checkbox = QCheckBox("Display pose predictions")
469+
# self.show_predictions_checkbox.setChecked(True)
470+
# form.addRow(self.show_predictions_checkbox)
467471

468472
self.allow_processor_ctrl_checkbox = QCheckBox("Allow processor-based control")
469473
self.allow_processor_ctrl_checkbox.setChecked(False)
@@ -495,15 +499,19 @@ def _build_recording_group(self) -> QGroupBox:
495499
# Session + run name
496500
self.session_name_edit = QLineEdit()
497501
self.session_name_edit.setPlaceholderText("e.g. mouseA_day1")
498-
form.addRow("Session name", self.session_name_edit)
502+
# form.addRow("Session name", self.session_name_edit)
499503

500504
self.use_timestamp_checkbox = QCheckBox("Use timestamp for run folder name")
501505
self.use_timestamp_checkbox.setChecked(True)
502506
self.use_timestamp_checkbox.setToolTip(
503507
"If checked, run folder will be run_YYYYMMDD_HHMMSS_mmm.\n"
504508
"If unchecked, run folder will be run_0001, run_0002, ..."
505509
)
506-
form.addRow("", self.use_timestamp_checkbox)
510+
# form.addRow("", self.use_timestamp_checkbox)
511+
session_opts = lyts.make_two_field_row(
512+
"Session name", self.session_name_edit, None, self.use_timestamp_checkbox, key_width=100
513+
)
514+
form.addRow(session_opts)
507515

508516
# Show recording path preview
509517

@@ -575,7 +583,7 @@ def _build_recording_group(self) -> QGroupBox:
575583

576584
## CRF
577585
crf_label = QLabel("CRF")
578-
crf_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
586+
crf_label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
579587
grid.addWidget(crf_label, 0, 4)
580588

581589
self.crf_spin = QSpinBox()
@@ -618,24 +626,55 @@ def _build_recording_group(self) -> QGroupBox:
618626

619627
return group
620628

621-
def _build_bbox_group(self) -> QGroupBox:
622-
group = QGroupBox("Bounding Box Visualization")
629+
def _build_viz_group(self) -> QGroupBox:
630+
group = QGroupBox("Visualization")
623631
form = QFormLayout(group)
632+
self.show_predictions_checkbox = QCheckBox("Display pose predictions")
633+
self.show_predictions_checkbox.setChecked(True)
634+
635+
self.cmap_combo = QComboBox()
636+
self.cmap_combo.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
637+
self.cmap_combo.setToolTip("Select colormap to use when displaying keypoints (bodypart-based coloring)")
638+
color_ui.populate_colormap_combo(
639+
self.cmap_combo,
640+
current=self._colormap,
641+
favorites_first=["turbo", "jet", "hsv"],
642+
exclude_reversed=True,
643+
filters={"cet_": 5}, # include only first 5 colormaps from the 'cet_' family to avoid redundant options
644+
)
645+
lyts.enable_combo_shrink_to_current(self.cmap_combo, min_width=80, max_width=200)
646+
647+
keypoints_settings = lyts.make_two_field_row(
648+
"Keypoint colormap: ",
649+
self.cmap_combo,
650+
None,
651+
self.show_predictions_checkbox,
652+
key_width=None,
653+
left_stretch=0,
654+
right_stretch=0,
655+
)
656+
form.addRow(keypoints_settings)
624657

625-
row_widget = QWidget()
626-
checkbox_layout = QHBoxLayout(row_widget)
627-
checkbox_layout.setContentsMargins(0, 0, 0, 0)
628658
self.bbox_enabled_checkbox = QCheckBox("Show bounding box")
629659
self.bbox_enabled_checkbox.setChecked(False)
630-
checkbox_layout.addWidget(self.bbox_enabled_checkbox)
631-
checkbox_layout.addWidget(QLabel("Color:"))
632660

633661
self.bbox_color_combo = QComboBox()
634-
self._populate_bbox_color_combo_with_swatches()
662+
self.bbox_color_combo.setToolTip("Select color for bounding box")
663+
self.bbox_color_combo.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
664+
color_ui.populate_bbox_color_combo(self.bbox_color_combo, BBoxColors, current_bgr=self._bbox_color)
635665
self.bbox_color_combo.setCurrentIndex(0)
636-
checkbox_layout.addWidget(self.bbox_color_combo)
637-
checkbox_layout.addStretch(1)
638-
form.addRow(row_widget)
666+
lyts.enable_combo_shrink_to_current(self.bbox_color_combo, min_width=80, max_width=200)
667+
668+
bbox_settings = lyts.make_two_field_row(
669+
"Bounding box color: ",
670+
self.bbox_color_combo,
671+
None,
672+
self.bbox_enabled_checkbox,
673+
key_width=None,
674+
left_stretch=0,
675+
right_stretch=0,
676+
)
677+
form.addRow(bbox_settings)
639678

640679
bbox_layout = QHBoxLayout()
641680
self.bbox_x0_spin = ScrubSpinBox()
@@ -679,7 +718,10 @@ def _connect_signals(self) -> None:
679718
# Camera config dialog
680719
self.config_cameras_button.clicked.connect(self._open_camera_config_dialog)
681720

682-
# Connect bounding box controls
721+
# Visualization settings
722+
## Colormap change
723+
self.cmap_combo.currentIndexChanged.connect(self._on_colormap_changed)
724+
## Connect bounding box controls
683725
self.bbox_enabled_checkbox.stateChanged.connect(self._on_bbox_changed)
684726
self.bbox_x0_spin.valueChanged.connect(self._on_bbox_changed)
685727
self.bbox_y0_spin.valueChanged.connect(self._on_bbox_changed)
@@ -760,9 +802,11 @@ def _apply_config(self, config: ApplicationSettings) -> None:
760802
viz = config.visualization
761803
self._p_cutoff = viz.p_cutoff
762804
self._colormap = viz.colormap
805+
if hasattr(self, "cmap_combo"):
806+
color_ui.set_cmap_combo_from_name(self.cmap_combo, self._colormap, fallback="viridis")
763807
self._bbox_color = viz.get_bbox_color_bgr()
764808
if hasattr(self, "bbox_color_combo"):
765-
self._set_combo_from_color(self._bbox_color)
809+
color_ui.set_bbox_combo_from_bgr(self.bbox_color_combo, self._bbox_color)
766810

767811
# Update DLC camera list
768812
self._refresh_dlc_camera_list()
@@ -1050,11 +1094,16 @@ def _on_use_timestamp_changed(self, _state: int) -> None:
10501094
self._settings_store.set_use_timestamp(self.use_timestamp_checkbox.isChecked())
10511095
self._update_recording_path_preview()
10521096

1097+
def _on_colormap_changed(self, _index: int) -> None:
1098+
self._colormap = color_ui.get_cmap_name_from_combo(self.cmap_combo, fallback=self._colormap)
1099+
if self._current_frame is not None:
1100+
self._display_frame(self._current_frame, force=True)
1101+
10531102
def _on_bbox_color_changed(self, _index: int) -> None:
1054-
enum_item = self.bbox_color_combo.currentData()
1055-
if enum_item is None:
1103+
bgr = color_ui.get_bbox_bgr_from_combo(self.bbox_color_combo, fallback=self._bbox_color)
1104+
if bgr is None:
10561105
return
1057-
self._bbox_color = enum_item.value
1106+
self._bbox_color = bgr
10581107
if self._current_frame is not None:
10591108
self._display_frame(self._current_frame, force=True)
10601109

0 commit comments

Comments
 (0)