6363 VisualizationSettings ,
6464)
6565
66+ from ..config import SkeletonColorMode , SkeletonStyle
6667from ..processors .processor_utils import (
6768 default_processors_dir ,
6869 instantiate_from_scan ,
@@ -891,6 +892,45 @@ def _connect_signals(self) -> None:
891892
892893 # ------------------------------------------------------------------
893894 # Config
895+ # ------------------------------------------------------------------
896+ def _apply_viz_settings_to_ui (self , viz : VisualizationSettings ) -> None :
897+ """Set UI state from VisualizationSettings (does not require skeleton to exist)."""
898+ # Pose toggle
899+ self .show_predictions_checkbox .blockSignals (True )
900+ self .show_predictions_checkbox .setChecked (bool (viz .show_pose ))
901+ self .show_predictions_checkbox .blockSignals (False )
902+
903+ # Skeleton toggle (may remain disabled until skeleton exists)
904+ self .show_skeleton_checkbox .blockSignals (True )
905+ self .show_skeleton_checkbox .setChecked (bool (viz .show_skeleton ))
906+ self .show_skeleton_checkbox .blockSignals (False )
907+
908+ # Skeleton style controls (combo/spin) - set values even if disabled
909+ if hasattr (self , "skeleton_color_combo" ):
910+ mode = viz .skeleton_style .mode .value # "solid" or "gradient_keypoints"
911+ color = tuple (viz .skeleton_style .color )
912+ color_ui .set_skeleton_combo_from_style (self .skeleton_color_combo , mode = mode , color = color )
913+
914+ if hasattr (self , "skeleton_thickness_spin" ):
915+ self .skeleton_thickness_spin .blockSignals (True )
916+ self .skeleton_thickness_spin .setValue (int (viz .skeleton_style .thickness ))
917+ self .skeleton_thickness_spin .blockSignals (False )
918+
919+ def _apply_viz_settings_to_skeleton (self , viz : VisualizationSettings ) -> None :
920+ """Apply VisualizationSettings onto the active runtime Skeleton, if present."""
921+ if self ._skeleton is None :
922+ return
923+
924+ # Copy style fields
925+ self ._skeleton .style .mode = skel .SkeletonColorMode (viz .skeleton_style .mode .value )
926+ self ._skeleton .style .color = tuple (viz .skeleton_style .color )
927+ self ._skeleton .style .thickness = int (viz .skeleton_style .thickness )
928+ self ._skeleton .style .gradient_steps = int (viz .skeleton_style .gradient_steps )
929+ self ._skeleton .style .scale_with_zoom = bool (viz .skeleton_style .scale_with_zoom )
930+
931+ # Enable/disable UI controls now that skeleton exists
932+ self ._sync_skeleton_controls_from_model ()
933+
894934 def _apply_config (self , config : ApplicationSettings ) -> None :
895935 # Update active cameras label
896936 self ._update_active_cameras_label ()
@@ -907,6 +947,7 @@ def _apply_config(self, config: ApplicationSettings) -> None:
907947 self .output_directory_edit .setText (recording .directory )
908948 self .filename_edit .setText (recording .filename )
909949 self .container_combo .setCurrentText (recording .container )
950+ self .record_with_overlays_checkbox .setChecked (recording .with_overlays )
910951 codec_index = self .codec_combo .findText (recording .codec )
911952 if codec_index >= 0 :
912953 self .codec_combo .setCurrentIndex (codec_index )
@@ -937,6 +978,7 @@ def _apply_config(self, config: ApplicationSettings) -> None:
937978 viz = config .visualization
938979 self ._p_cutoff = viz .p_cutoff
939980 self ._colormap = viz .colormap
981+ self ._apply_viz_settings_to_ui (viz )
940982 if hasattr (self , "cmap_combo" ):
941983 color_ui .set_cmap_combo_from_name (self .cmap_combo , self ._colormap , fallback = "viridis" )
942984 self ._bbox_color = viz .get_bbox_color_bgr ()
@@ -945,6 +987,7 @@ def _apply_config(self, config: ApplicationSettings) -> None:
945987 ## Skeleton
946988 if resolved_model_path .strip ():
947989 self ._configure_skeleton_for_model (resolved_model_path )
990+ self ._apply_viz_settings_to_skeleton (viz )
948991
949992 # Update DLC camera list
950993 self ._refresh_dlc_camera_list ()
@@ -1007,6 +1050,7 @@ def _recording_settings_from_ui(self) -> RecordingSettings:
10071050 container = self .container_combo .currentText ().strip () or "mp4" ,
10081051 codec = self .codec_combo .currentText ().strip () or "libx264" ,
10091052 crf = int (self .crf_spin .value ()),
1053+ with_overlays = self .record_with_overlays_checkbox .isChecked (),
10101054 )
10111055
10121056 def _bbox_settings_from_ui (self ) -> BoundingBoxSettings :
@@ -1019,10 +1063,29 @@ def _bbox_settings_from_ui(self) -> BoundingBoxSettings:
10191063 )
10201064
10211065 def _visualization_settings_from_ui (self ) -> VisualizationSettings :
1066+ # Read skeleton mode+color from combo
1067+ mode_str , color = color_ui .get_skeleton_style_from_combo (
1068+ self .skeleton_color_combo ,
1069+ fallback_mode = "solid" ,
1070+ fallback_color = (0 , 255 , 255 ),
1071+ )
1072+
1073+ # Build SkeletonStyle (pydantic)
1074+ style = SkeletonStyle (
1075+ mode = SkeletonColorMode (mode_str ), # or SkeletonColorMode.GRADIENT_KEYPOINTS if mode_str matches
1076+ color = tuple (color ) if color else (0 , 255 , 255 ),
1077+ thickness = int (self .skeleton_thickness_spin .value ()),
1078+ gradient_steps = getattr (self ._skeleton .style , "gradient_steps" , 16 ) if self ._skeleton else 16 ,
1079+ scale_with_zoom = getattr (self ._skeleton .style , "scale_with_zoom" , True ) if self ._skeleton else True ,
1080+ )
1081+
10221082 return VisualizationSettings (
10231083 p_cutoff = self ._p_cutoff ,
10241084 colormap = self ._colormap ,
10251085 bbox_color = self ._bbox_color ,
1086+ show_pose = self .show_predictions_checkbox .isChecked (),
1087+ show_skeleton = self .show_skeleton_checkbox .isChecked (),
1088+ skeleton_style = style ,
10261089 )
10271090
10281091 # ------------------------------------------------------------------
@@ -1286,28 +1349,16 @@ def _on_show_skeleton_changed(self, _state: int) -> None:
12861349 self ._display_frame (self ._current_frame , force = True )
12871350
12881351 def _on_skeleton_style_changed (self , _value : int = 0 ) -> None :
1289- """Apply UI skeleton styling to the current Skeleton instance."""
12901352 if self ._skeleton is None :
12911353 return
12921354
1293- mode , color = color_ui .get_skeleton_style_from_combo (
1294- self .skeleton_color_combo ,
1295- fallback_mode = "solid" ,
1296- fallback_color = self ._skeleton .style .color ,
1297- )
1298-
1299- # Update style mode
1300- if mode == "gradient_keypoints" :
1301- self ._skeleton .style .mode = skel .SkeletonColorMode .GRADIENT_KEYPOINTS
1302- else :
1303- self ._skeleton .style .mode = skel .SkeletonColorMode .SOLID
1304- if color is not None :
1305- self ._skeleton .style .color = tuple (color )
1355+ mode_str , color = color_ui .get_skeleton_style_from_combo (self .skeleton_color_combo )
1356+ self ._skeleton .style .mode = skel .SkeletonColorMode (mode_str )
1357+ if self ._skeleton .style .mode == skel .SkeletonColorMode .SOLID and color is not None :
1358+ self ._skeleton .style .color = tuple (color )
13061359
1307- # Thickness
13081360 self ._skeleton .style .thickness = int (self .skeleton_thickness_spin .value ())
13091361
1310- # Redraw
13111362 if self ._current_frame is not None :
13121363 self ._display_frame (self ._current_frame , force = True )
13131364
@@ -1469,17 +1520,12 @@ def _render_overlays_for_recording(self, cam_id, frame):
14691520 offset = offset ,
14701521 scale = scale ,
14711522 )
1472-
1473- if self ._skeleton and hasattr (self , "show_skeleton_checkbox" ) and self .show_skeleton_checkbox .isChecked ():
1474- pose_arr = np .asarray (self ._last_pose .pose )
1475- if pose_arr .ndim == 3 :
1476- st = self ._skeleton .draw_many (output , pose_arr , self ._p_cutoff , offset , scale )
1477- else :
1478- self ._skeleton .draw (output , self ._last_pose .pose , self ._p_cutoff , offset , scale )
1479- if st .should_disable :
1480- self .show_skeleton_checkbox .blockSignals (True )
1481- self .show_skeleton_checkbox .setChecked (False )
1482- self .show_skeleton_checkbox .blockSignals (False )
1523+ self ._draw_skeleton_on_frame (
1524+ output ,
1525+ self ._last_pose .pose ,
1526+ offset = offset ,
1527+ scale = scale ,
1528+ )
14831529
14841530 if self ._bbox_enabled :
14851531 output = draw_bbox (
@@ -1795,7 +1841,7 @@ def _configure_skeleton_for_model(self, model_path: str) -> None:
17951841
17961842 root = p if p .is_dir () else p .parent
17971843 cfg = root / "config.yaml"
1798- if cfg .exists ():
1844+ if cfg .exists () and self . _skeleton is None :
17991845 try :
18001846 sk = skel .load_dlc_skeleton (cfg )
18011847 except Exception as e :
@@ -1808,7 +1854,15 @@ def _configure_skeleton_for_model(self, model_path: str) -> None:
18081854 if hasattr (self , "show_skeleton_checkbox" ):
18091855 self .show_skeleton_checkbox .setEnabled (True )
18101856 self .statusBar ().showMessage ("Skeleton available: DLC config.yaml" , 3000 )
1811- return
1857+
1858+ if self ._skeleton is not None :
1859+ try :
1860+ viz = self ._config .visualization
1861+ self ._apply_viz_settings_to_skeleton (viz )
1862+ except Exception as e :
1863+ logger .warning (f"Failed to apply visualization settings to skeleton: { e } " )
1864+ pass
1865+ return
18121866
18131867 # None found
18141868 self .statusBar ().showMessage ("No skeleton definition available for this model." , 3000 )
@@ -2126,51 +2180,59 @@ def _on_dlc_error(self, message: str) -> None:
21262180 self ._stop_inference (show_message = False )
21272181 self ._show_error (message )
21282182
2129- def _try_draw_skeleton (self , overlay : np .ndarray , pose : np .ndarray ) -> None :
2183+ def _draw_skeleton_on_frame (
2184+ self ,
2185+ overlay : np .ndarray ,
2186+ pose : np .ndarray ,
2187+ * ,
2188+ offset : tuple [int , int ],
2189+ scale : tuple [float , float ],
2190+ allow_auto_disable : bool = True ,
2191+ ) -> skel .SkeletonRenderStatus | None :
2192+ """Draw skeleton on overlay with correct style. Optionally auto-disables UI on mismatch."""
21302193 if self ._skeleton is None :
2131- return
2194+ return None
21322195 if not self .show_skeleton_checkbox .isChecked ():
2133- return
2196+ return None
21342197 if self ._skeleton_auto_disabled :
2135- return
2198+ return None
21362199
21372200 pose_arr = np .asarray (pose )
21382201
2139- # Compute keypoint colors only if gradient mode is active
2202+ # Provide keypoint_colors iff gradient mode is active
21402203 kp_colors = None
2141- try :
2142- if self ._skeleton .style .mode == skel .SkeletonColorMode .GRADIENT_KEYPOINTS :
2143- n_kpts = pose_arr .shape [1 ] if pose_arr .ndim == 3 else pose_arr .shape [0 ]
2144- kp_colors = keypoint_colors_bgr (self ._colormap , int (n_kpts ))
2204+ if self ._skeleton .style .mode == skel .SkeletonColorMode .GRADIENT_KEYPOINTS :
2205+ n_kpts = pose_arr .shape [1 ] if pose_arr .ndim == 3 else pose_arr .shape [0 ]
2206+ kp_colors = keypoint_colors_bgr (self ._colormap , int (n_kpts ))
21452207
2208+ try :
21462209 if pose_arr .ndim == 3 :
21472210 status = self ._skeleton .draw_many (
21482211 overlay ,
21492212 pose_arr ,
21502213 p_cutoff = self ._p_cutoff ,
2151- offset = self . _dlc_tile_offset ,
2152- scale = self . _dlc_tile_scale ,
2214+ offset = offset ,
2215+ scale = scale ,
21532216 keypoint_colors = kp_colors ,
21542217 )
21552218 else :
21562219 status = self ._skeleton .draw (
21572220 overlay ,
21582221 pose_arr ,
21592222 p_cutoff = self ._p_cutoff ,
2160- offset = self . _dlc_tile_offset ,
2161- scale = self . _dlc_tile_scale ,
2223+ offset = offset ,
2224+ scale = scale ,
21622225 keypoint_colors = kp_colors ,
21632226 )
2164-
21652227 except Exception as e :
21662228 status = skel .SkeletonRenderStatus (
21672229 code = skel .SkeletonRenderCode .POSE_SHAPE_INVALID ,
21682230 message = f"Skeleton rendering error: { e } " ,
21692231 )
21702232
2171- if status .should_disable :
2233+ if allow_auto_disable and status .should_disable :
21722234 self ._skeleton_auto_disabled = True
2173- msg = status .message or "Skeleton disabled due to keypoint mismatch."
2235+ msg = status .message or "Skeleton disabled due to mismatch."
21742236 if msg != self ._last_skeleton_disable_msg :
21752237 self ._last_skeleton_disable_msg = msg
21762238 self .statusBar ().showMessage (f"Skeleton disabled: { msg } " , 6000 )
@@ -2179,6 +2241,8 @@ def _try_draw_skeleton(self, overlay: np.ndarray, pose: np.ndarray) -> None:
21792241 self .show_skeleton_checkbox .setChecked (False )
21802242 self .show_skeleton_checkbox .blockSignals (False )
21812243
2244+ return status
2245+
21822246 def _update_video_display (self , frame : np .ndarray ) -> None :
21832247 display_frame = frame
21842248
@@ -2192,7 +2256,12 @@ def _update_video_display(self, frame: np.ndarray) -> None:
21922256 scale = self ._dlc_tile_scale ,
21932257 )
21942258
2195- self ._try_draw_skeleton (display_frame , self ._last_pose .pose )
2259+ self ._draw_skeleton_on_frame (
2260+ display_frame ,
2261+ self ._last_pose .pose ,
2262+ offset = self ._dlc_tile_offset ,
2263+ scale = self ._dlc_tile_scale ,
2264+ )
21962265
21972266 if self ._bbox_enabled :
21982267 display_frame = draw_bbox (
0 commit comments