simba-uw-tf-dev 4.7.4__py3-none-any.whl → 4.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of simba-uw-tf-dev might be problematic. Click here for more details.

@@ -82,7 +82,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
82
82
  Label(self.import_tracking_frm, text="Please CREATE PROJECT CONFIG before importing tracking data \n", font=Formats.FONT_REGULAR.value).grid(row=0, column=0, sticky=NW)
83
83
  else:
84
84
  ConfigReader.__init__(self, config_path=config_path, read_video_info=False)
85
- self.data_type_dropdown = SimBADropDown(parent=self.import_tracking_frm, dropdown_options=Options.IMPORT_TYPE_OPTIONS.value, label="DATA TYPE: ", label_width=25, command=self.create_import_menu, dropdown_width=25, value=Options.IMPORT_TYPE_OPTIONS.value[0])
85
+ self.data_type_dropdown = SimBADropDown(parent=self.import_tracking_frm, dropdown_options=Options.IMPORT_TYPE_OPTIONS.value, label="DATA TYPE: ", label_width=25, command=self.create_import_menu, dropdown_width=25, value=Options.IMPORT_TYPE_OPTIONS.value[0], img='file_type')
86
86
  self.data_type_dropdown.grid(row=0, column=0, sticky=NW)
87
87
  self.create_import_menu(data_type_choice=Options.IMPORT_TYPE_OPTIONS.value[0])
88
88
  self.import_tracking_frm.grid(row=idx_row, column=idx_column, sticky=NW)
@@ -91,7 +91,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
91
91
 
92
92
  def __show_smoothing_entry_box_from_dropdown(self, choice: str):
93
93
  if (choice == GAUSSIAN) or (choice == SAVITZKY_GOLAY):
94
- self.smoothing_time_eb.grid(row=0, column=1, sticky=E)
94
+ self.smoothing_time_eb.grid(row=1, column=0, sticky=E)
95
95
  else:
96
96
  self.smoothing_time_eb.grid_forget()
97
97
 
@@ -265,21 +265,21 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
265
265
  self.animal_name_entry_boxes = None
266
266
 
267
267
  self.interpolation_frm = CreateLabelFrameWithIcon(parent=self.choice_frm, header="INTERPOLATION METHOD", pady=5, padx=5,font=Formats.FONT_HEADER.value, icon_name='fill', relief='groove')
268
- self.interpolation_dropdown = SimBADropDown(parent=self.interpolation_frm, dropdown_options=Options.INTERPOLATION_OPTIONS_W_NONE.value, label='INTERPOLATION METHOD: ', label_width=25, dropdown_width=35, value=Options.INTERPOLATION_OPTIONS_W_NONE.value[0])
268
+ self.interpolation_dropdown = SimBADropDown(parent=self.interpolation_frm, dropdown_options=Options.INTERPOLATION_OPTIONS_W_NONE.value, label='INTERPOLATION METHOD: ', label_width=25, dropdown_width=35, value=Options.INTERPOLATION_OPTIONS_W_NONE.value[0], img='fill')
269
269
  self.interpolation_frm.grid(row=0, column=0, sticky=NW)
270
270
  self.interpolation_dropdown.grid(row=0, column=0, sticky=NW)
271
271
 
272
272
  self.smoothing_frm = CreateLabelFrameWithIcon(parent=self.choice_frm, header="SMOOTHING METHOD", pady=5, padx=5, font=Formats.FONT_HEADER.value, icon_name='smooth', relief='groove')
273
- self.smoothing_dropdown = SimBADropDown(parent=self.smoothing_frm, dropdown_options=Options.SMOOTHING_OPTIONS_W_NONE.value, label='SMOOTHING: ', label_width=25, dropdown_width=35, value=Options.SMOOTHING_OPTIONS_W_NONE.value[0], command=self.__show_smoothing_entry_box_from_dropdown)
274
- self.smoothing_time_eb = Entry_Box(self.smoothing_frm, "SMOOTHING PERIOD (MS):", labelwidth=30, validation="numeric", entry_box_width=10)
273
+ self.smoothing_dropdown = SimBADropDown(parent=self.smoothing_frm, dropdown_options=Options.SMOOTHING_OPTIONS_W_NONE.value, label='SMOOTHING: ', label_width=25, dropdown_width=35, value=Options.SMOOTHING_OPTIONS_W_NONE.value[0], command=self.__show_smoothing_entry_box_from_dropdown, img='smooth')
274
+ self.smoothing_time_eb = Entry_Box(self.smoothing_frm, "SMOOTHING PERIOD (MS):", labelwidth=25, validation="numeric", entry_box_width=35, img='timer_2')
275
275
  self.smoothing_frm.grid(row=1, column=0, sticky=NW)
276
276
  self.smoothing_dropdown.grid(row=0, column=0, sticky=NW)
277
277
 
278
278
  if data_type_choice in ["CSV (DLC/DeepPoseKit)", "MAT (DANNCE 3D)", "JSON (BENTO)", "CSV (SimBA BLOB)", 'H5 (FaceMap)', 'CSV (SimBA YOLO)']: # DATA TYPES WHERE NO TRACKS HAVE TO BE SPECIFIED
279
279
  self.import_directory_frm = LabelFrame(self.choice_frm, text=FRAME_DIR_IMPORT_TITLES[data_type_choice], pady=5, padx=5, font=Formats.FONT_HEADER.value,)
280
- self.import_directory_select = FolderSelect(self.import_directory_frm, "Input data DIRECTORY:", lblwidth=25, initialdir=self.project_path)
280
+ self.import_directory_select = FolderSelect(self.import_directory_frm, "Input data DIRECTORY:", lblwidth=25, initialdir=self.project_path, lbl_icon='folder')
281
281
  self.import_single_frm = LabelFrame(self.choice_frm, text=FRAME_FILE_IMPORT_TITLES[data_type_choice], pady=5, padx=5, font=Formats.FONT_HEADER.value,)
282
- self.import_file_select = FileSelect(self.import_single_frm, "Input data FILE:", lblwidth=25, file_types=[("Pose data file", FILE_TYPES[data_type_choice])])
282
+ self.import_file_select = FileSelect(self.import_single_frm, "Input data FILE:", lblwidth=25, file_types=[("Pose data file", FILE_TYPES[data_type_choice])], lbl_icon='file_type')
283
283
 
284
284
  if data_type_choice == "CSV (DLC/DeepPoseKit)":
285
285
  self.import_dir_btn = Button(self.import_directory_frm, fg="blue", font=Formats.FONT_REGULAR.value, text="Import DLC CSV DIRECTORY to SimBA project", command=lambda: self.__import_dlc_csv_data(interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -342,7 +342,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
342
342
  self.dlc_data_type_option_dropdown.setChoices(Options.MULTI_DLC_TYPE_IMPORT_OPTION.value[1])
343
343
  self.tracking_type_frm.grid(row=5, column=0, sticky=NW)
344
344
  self.dlc_data_type_option_dropdown.grid(row=0, column=0, sticky=NW)
345
- self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 DLC DIRECTORY: ", lblwidth=25)
345
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 DLC DIRECTORY: ", lblwidth=25, lbl_icon='folder')
346
346
  self.instructions_lbl = Label(self.data_dir_frm, text="Please import videos BEFORE importing the \n multi animal DLC tracking data", font=Formats.FONT_REGULAR.value)
347
347
  self.run_btn = Button(self.import_frm, text="IMPORT DLC .H5", fg="blue", command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
348
348
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -352,7 +352,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
352
352
  data_path=self.data_dir_select.folder_path,
353
353
  tracking_data_type=self.dlc_data_type_option_dropdown.getChoices()))
354
354
  elif data_type_choice == "SLP (SLEAP)":
355
- self.data_dir_select = FolderSelect(self.data_dir_frm, "SLP SLEAP DIRECTORY: ", lblwidth=25)
355
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "SLP SLEAP DIRECTORY: ", lblwidth=25, lbl_icon='folder')
356
356
  self.instructions_lbl = Label(self.data_dir_frm, font=Formats.FONT_REGULAR.value, text="Please import videos before importing the \n multi animal SLEAP tracking data if you are tracking more than ONE animal")
357
357
  self.run_btn = Button(self.import_frm, text="IMPORT SLEAP .SLP", fg="blue", font=Formats.FONT_REGULAR.value, command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
358
358
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -362,7 +362,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
362
362
  data_path=self.data_dir_select.folder_path))
363
363
 
364
364
  elif data_type_choice == "TRK (multi-animal APT)":
365
- self.data_dir_select = FolderSelect(self.data_dir_frm, "TRK APT DIRECTORY: ", lblwidth=25)
365
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "TRK APT DIRECTORY: ", lblwidth=25, lbl_icon='folder')
366
366
  self.instructions_lbl = Label(self.data_dir_frm, text="Please import videos before importing the \n multi animal TRK tracking data", font=Formats.FONT_REGULAR.value,)
367
367
  self.run_btn = Button(self.import_frm, text="IMPORT APT .TRK", font=Formats.FONT_REGULAR.value, fg="blue", command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
368
368
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -372,7 +372,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
372
372
  data_path=self.data_dir_select.folder_path))
373
373
 
374
374
  elif data_type_choice == "CSV (SLEAP)":
375
- self.data_dir_select = FolderSelect(self.data_dir_frm, "CSV SLEAP DIRECTORY:", lblwidth=25)
375
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "CSV SLEAP DIRECTORY:", lblwidth=25, lbl_icon='folder')
376
376
  self.instructions_lbl = Label(self.data_dir_frm, font=Formats.FONT_REGULAR.value, text="Please import videos before importing the SLEAP tracking data \n IF you are tracking more than ONE animal")
377
377
  self.run_btn = Button(self.import_frm, text="IMPORT SLEAP .CSV", fg="blue", font=Formats.FONT_REGULAR.value, command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
378
378
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -382,7 +382,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
382
382
  data_path=self.data_dir_select.folder_path))
383
383
 
384
384
  elif data_type_choice == "H5 (SLEAP)":
385
- self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 SLEAP DIRECTORY", lblwidth=25)
385
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 SLEAP DIRECTORY", lblwidth=25, lbl_icon='folder')
386
386
  self.instructions_lbl = Label(self.data_dir_frm, font=Formats.FONT_REGULAR.value, text="Please import videos before importing the SLEAP H5 tracking data \n IF you are tracking more than ONE animal")
387
387
  self.run_btn = Button(self.import_frm, text="IMPORT SLEAP H5", font=Formats.FONT_REGULAR.value, fg="blue", command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
388
388
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -391,7 +391,7 @@ class ImportPoseFrame(ConfigReader, PopUpMixin):
391
391
  animal_names=self.animal_name_entry_boxes,
392
392
  data_path=self.data_dir_select.folder_path))
393
393
  elif data_type_choice == "H5 (SuperAnimal-TopView)":
394
- self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 SuperAnimal DIRECTORY:", lblwidth=25)
394
+ self.data_dir_select = FolderSelect(self.data_dir_frm, "H5 SuperAnimal DIRECTORY:", lblwidth=25, lbl_icon='folder')
395
395
  self.instructions_lbl = Label(self.data_dir_frm, font=Formats.FONT_REGULAR.value, text="Please import videos before importing the H5 SuperAnimal tracking data \n IF you are tracking more than ONE animal")
396
396
  self.run_btn = Button(self.import_frm, text="IMPORT SuperAnimal-Mouse-TopView H5", fg="blue", font=Formats.FONT_REGULAR.value, command=lambda: self.__multi_animal_run_call(pose_estimation_tool=data_type_choice,
397
397
  interpolation_settings=self.interpolation_dropdown.getChoices(),
@@ -57,7 +57,7 @@ class SklearnVisualizationPopUp(PopUpMixin, ConfigReader):
57
57
  self.bp_threshold_lbl.grid(row=0, column=0, sticky=NW)
58
58
  self.bp_threshold_entry.grid(row=1, column=0, sticky=NW)
59
59
 
60
- self.style_settings_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="SETTINGS", icon_name='style', icon_link=Links.SKLEARN_PLOTS.value, padx=5, pady=5, relief='solid')
60
+ self.style_settings_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="TEXT SETTINGS", icon_name='style', icon_link=Links.SKLEARN_PLOTS.value, padx=5, pady=5, relief='solid')
61
61
  self.text_size_dropdown = SimBADropDown(parent=self.style_settings_frm, dropdown_options=TEXT_SIZE_OPTIONS, label='TEXT SIZE: ', label_width=40, dropdown_width=15, value='AUTO', img='text')
62
62
  self.text_spacing_dropdown = SimBADropDown(parent=self.style_settings_frm, dropdown_options=TEXT_SIZE_OPTIONS, label='TEXT SPACING: ', label_width=40, dropdown_width=15, value='AUTO', img='text_spacing')
63
63
  self.text_thickness_dropdown = SimBADropDown(parent=self.style_settings_frm, dropdown_options=TEXT_SIZE_OPTIONS, label='TEXT THICKNESS: ', label_width=40, dropdown_width=15, value='AUTO', img='bold')
@@ -69,13 +69,14 @@ from simba.video_processors.video_processing import (
69
69
  crop_single_video, crop_single_video_circle, crop_single_video_polygon,
70
70
  crossfade_two_videos, downsample_video, extract_frame_range,
71
71
  extract_frames_single_video, flip_videos, frames_to_movie, gif_creator,
72
- multi_split_video, remove_beginning_of_video, resize_videos_by_height,
73
- resize_videos_by_width, reverse_videos, roi_blurbox, rotate_video,
74
- superimpose_elapsed_time, superimpose_frame_count, superimpose_freetext,
75
- superimpose_overlay_video, superimpose_video_names,
76
- superimpose_video_progressbar, temporal_concatenation, upsample_fps,
77
- video_bg_subtraction, video_bg_subtraction_mp, video_concatenator,
78
- video_to_bw, video_to_greyscale, watermark_video, remove_end_of_video)
72
+ multi_split_video, remove_beginning_of_video, remove_end_of_video,
73
+ resize_videos_by_height, resize_videos_by_width, reverse_videos,
74
+ roi_blurbox, rotate_video, superimpose_elapsed_time,
75
+ superimpose_frame_count, superimpose_freetext, superimpose_overlay_video,
76
+ superimpose_video_names, superimpose_video_progressbar,
77
+ temporal_concatenation, upsample_fps, video_bg_subtraction,
78
+ video_bg_subtraction_mp, video_concatenator, video_to_bw,
79
+ video_to_greyscale, watermark_video)
79
80
 
80
81
  sys.setrecursionlimit(10**7)
81
82
 
@@ -1548,7 +1549,7 @@ class ClipSingleVideoByFrameNumbers(PopUpMixin):
1548
1549
  if interactive_ui.img_window.winfo_exists(): interactive_ui.close()
1549
1550
  self.click_event.set(True)
1550
1551
 
1551
- interactive_ui = TimelapseSlider(video_path=self.selected_video.file_path)
1552
+ interactive_ui = TimelapseSlider(video_path=self.selected_video.file_path, use_timestamps=False)
1552
1553
  interactive_ui.run()
1553
1554
  self.click_event = BooleanVar(value=False)
1554
1555
  interactive_ui.img_window.protocol("WM_DELETE_WINDOW", window_closed)
@@ -1647,7 +1648,7 @@ class ClipMultipleVideosByFrameNumbersPopUp(PopUpMixin):
1647
1648
  if interactive_ui.img_window.winfo_exists(): interactive_ui.close()
1648
1649
  self.click_event.set(True)
1649
1650
 
1650
- interactive_ui = TimelapseSlider(video_path=kwargs['video_path'], frame_cnt=kwargs['frame_cnt'], crop_ratio=kwargs['crop_ratio'])
1651
+ interactive_ui = TimelapseSlider(video_path=kwargs['video_path'], frame_cnt=kwargs['frame_cnt'], crop_ratio=kwargs['crop_ratio'], use_timestamps=False)
1651
1652
  interactive_ui.run()
1652
1653
  self.click_event = BooleanVar(value=False)
1653
1654
  interactive_ui.img_window.protocol("WM_DELETE_WINDOW", window_closed)
@@ -2494,7 +2495,7 @@ class Convert2WEBMPopUp(PopUpMixin):
2494
2495
  def __init__(self):
2495
2496
  super().__init__(title="CONVERT VIDEOS TO WEBM", icon='webm')
2496
2497
  settings_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="SETTINGS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_TOOLS.value)
2497
- self.WEBM_CODEC_LK = {'VP8': 'vp8', 'VP9': 'vp9'}
2498
+ self.WEBM_CODEC_LK = {'VP8': 'vp8', 'VP9': 'vp9', 'AV1': 'av1'}
2498
2499
 
2499
2500
  self.quality_dropdown = SimBADropDown(parent=settings_frm, label="OUTPUT VIDEO QUALITY:", dropdown_options=list(range(10, 110, 10)), label_width=25, value=60, img='pct', dropdown_width=30)
2500
2501
  self.codec_dropdown = SimBADropDown(parent=settings_frm, label="COMPRESSION CODEC:", dropdown_options=list(self.WEBM_CODEC_LK.keys()), label_width=25, value='VP9', img='file_type', dropdown_width=30)
@@ -37,6 +37,7 @@ class TimelapseSlider():
37
37
  :param int ruler_divisions: Number of major divisions on time ruler. Default 6.
38
38
  :param bool show_ruler: If True, display time ruler below timelapse. Default True.
39
39
  :param int ruler_height: Height of ruler in pixels. Default 60.
40
+ :param bool use_timestamps: If True, display timestamps (HH:MM:SS) in labels and ruler. If False, display frame numbers. Default True.
40
41
 
41
42
  :example:
42
43
  >>> slider = TimelapseSlider(video_path='path/to/video.mp4', frame_cnt=25, crop_ratio=75)
@@ -61,7 +62,8 @@ class TimelapseSlider():
61
62
  ruler_height: Optional[int] = None,
62
63
  ruler_width: Optional[int] = None,
63
64
  img_width: Optional[int] = None,
64
- img_height: Optional[int] = None):
65
+ img_height: Optional[int] = None,
66
+ use_timestamps: bool = True):
65
67
 
66
68
  check_file_exist_and_readable(file_path=video_path)
67
69
  check_int(name='frame_cnt', value=frame_cnt, min_value=1, raise_error=True)
@@ -80,9 +82,11 @@ class TimelapseSlider():
80
82
  check_valid_boolean(value=show_ruler, source=f'{self.__class__.__name__} show_ruler', raise_error=True)
81
83
  self.video_meta = get_video_meta_data(video_path=video_path, raise_error=True)
82
84
  if show_ruler: check_int(name='ruler_divisions', value=ruler_divisions, min_value=1, raise_error=True)
85
+ check_valid_boolean(value=use_timestamps, source=f'{self.__class__.__name__} use_timestamps', raise_error=True)
83
86
  self.size, self.padding, self.crop_ratio, self.frame_cnt = ruler_width, padding, crop_ratio, frame_cnt
84
87
  self.ruler_height, self.video_path, self.show_ruler, self.ruler_divisions = ruler_height, video_path, show_ruler, ruler_divisions
85
88
  self.img_width, self.img_height = img_width, img_height
89
+ self.use_timestamps = use_timestamps
86
90
  self.frm_name = f'{self.video_meta["video_name"]} - TIMELAPSE VIEWER - hit "X" or ESC to close'
87
91
  self.video_capture, self._pending_frame_update, self._frame_debounce_ms = None, None, 50
88
92
 
@@ -94,16 +98,26 @@ class TimelapseSlider():
94
98
  lbl.image = self.tk_image
95
99
 
96
100
  def _update_selection(self, slider_type: str):
97
- start_sec = int(self.start_scale.get_value())
98
- end_sec = int(self.end_scale.get_value())
101
+ start_sec = self.start_scale.get_value()
102
+ end_sec = self.end_scale.get_value()
99
103
  max_sec = int(self.video_meta['video_length_s'])
104
+
105
+ # Convert to int for timestamp mode, keep float for frame mode to preserve precision
106
+ if self.use_timestamps:
107
+ start_sec = int(start_sec)
108
+ end_sec = int(end_sec)
109
+ else:
110
+ # In frame mode, use float precision but ensure we're within bounds
111
+ start_sec = max(0.0, min(float(start_sec), float(max_sec)))
112
+ end_sec = max(0.0, min(float(end_sec), float(max_sec)))
113
+
100
114
  if slider_type == 'start':
101
115
  if start_sec >= end_sec:
102
- end_sec = min(start_sec + 1, max_sec)
116
+ end_sec = min(start_sec + (1.0 if self.use_timestamps else 1.0/self.video_meta['fps']), max_sec)
103
117
  self.end_scale.set_value(end_sec)
104
118
  else:
105
119
  if end_sec <= start_sec:
106
- start_sec = max(end_sec - 1, 0)
120
+ start_sec = max(end_sec - (1.0 if self.use_timestamps else 1.0/self.video_meta['fps']), 0)
107
121
  self.start_scale.set_value(start_sec)
108
122
 
109
123
  self.selected_start[0] = start_sec
@@ -118,34 +132,118 @@ class TimelapseSlider():
118
132
  self.selected_start_frame[0] = start_frame
119
133
  self.selected_end_frame[0] = end_frame
120
134
 
121
- self.start_time_label.config(text=seconds_to_timestamp(start_sec), fg='green')
122
- self.end_time_label.config(text=seconds_to_timestamp(end_sec), fg='red')
135
+ if self.use_timestamps:
136
+ self.start_time_label.config(text=seconds_to_timestamp(start_sec), fg='green')
137
+ self.end_time_label.config(text=seconds_to_timestamp(end_sec), fg='red')
138
+ else:
139
+ start_frame = int(start_sec * self.video_meta['fps'])
140
+ end_frame = int(end_sec * self.video_meta['fps'])
141
+ self.start_time_label.config(text=str(start_frame), fg='green')
142
+ self.end_time_label.config(text=str(end_frame), fg='red')
123
143
 
124
144
  if self.video_meta['video_length_s'] > 0:
125
145
  self._highlight_segment(start_sec, end_sec)
126
146
  self._schedule_frame_update(slider_type=slider_type)
127
147
 
128
148
  def _move_start_frame(self, direction: int):
129
- current_seconds = self.selected_start[0]
130
- new_seconds = current_seconds + direction
131
- new_seconds = max(0, min(new_seconds, int(self.video_meta['video_length_s'])))
132
- self.start_scale.set_value(int(new_seconds))
133
- self._update_selection(slider_type='start')
134
- if self._pending_frame_update is not None:
135
- if hasattr(self, 'img_window') and self.img_window.winfo_exists():
136
- self.img_window.after_cancel(self._pending_frame_update)
137
- self._update_frame_display(slider_type='start')
149
+ # Temporarily unbind the callback to avoid conflicts
150
+ original_cmd = self.start_scale.scale.cget('command')
151
+ self.start_scale.scale.config(command='')
152
+
153
+ try:
154
+ if self.use_timestamps:
155
+ current_seconds = self.selected_start[0]
156
+ new_seconds = current_seconds + direction
157
+ # Ensure start doesn't exceed end
158
+ end_seconds = self.selected_end[0]
159
+ new_seconds = max(0, min(new_seconds, int(self.video_meta['video_length_s']), end_seconds - 1))
160
+ self.start_scale.set_value(int(new_seconds))
161
+ self.selected_start[0] = int(new_seconds)
162
+ # Recalculate frame from seconds
163
+ new_frame = int(new_seconds * self.video_meta['fps'])
164
+ new_frame = max(0, min(new_frame, self.video_meta['frame_count'] - 1))
165
+ self.selected_start_frame[0] = new_frame
166
+ else:
167
+ # Get current frame from the selected_start_frame
168
+ current_frame = self.selected_start_frame[0]
169
+ new_frame = current_frame + direction
170
+ # Ensure start doesn't exceed end
171
+ end_frame = self.selected_end_frame[0]
172
+ new_frame = max(0, min(new_frame, self.video_meta['frame_count'] - 1, end_frame - 1))
173
+ # Convert frame to seconds for the slider
174
+ new_seconds = new_frame / self.video_meta['fps']
175
+ # Update the slider value
176
+ self.start_scale.set_value(new_seconds)
177
+ # Directly update both frame and seconds
178
+ self.selected_start_frame[0] = new_frame
179
+ self.selected_start[0] = new_seconds
180
+
181
+ # Update the display labels and highlights
182
+ if self.use_timestamps:
183
+ self.start_time_label.config(text=seconds_to_timestamp(self.selected_start[0]), fg='green')
184
+ else:
185
+ self.start_time_label.config(text=str(self.selected_start_frame[0]), fg='green')
186
+
187
+ # Update highlights and frame preview
188
+ if self.video_meta['video_length_s'] > 0:
189
+ self._highlight_segment(self.selected_start[0], self.selected_end[0])
190
+ if self._pending_frame_update is not None:
191
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
192
+ self.img_window.after_cancel(self._pending_frame_update)
193
+ self._update_frame_display(slider_type='start')
194
+ finally:
195
+ # Rebind the callback
196
+ self.start_scale.scale.config(command=original_cmd)
138
197
 
139
198
  def _move_end_frame(self, direction: int):
140
- current_seconds = self.selected_end[0]
141
- new_seconds = current_seconds + direction
142
- new_seconds = max(0, min(new_seconds, int(self.video_meta['video_length_s'])))
143
- self.end_scale.set_value(int(new_seconds))
144
- self._update_selection(slider_type='end')
145
- if self._pending_frame_update is not None:
146
- if hasattr(self, 'img_window') and self.img_window.winfo_exists():
147
- self.img_window.after_cancel(self._pending_frame_update)
148
- self._update_frame_display(slider_type='end')
199
+ # Temporarily unbind the callback to avoid conflicts
200
+ original_cmd = self.end_scale.scale.cget('command')
201
+ self.end_scale.scale.config(command='')
202
+
203
+ try:
204
+ if self.use_timestamps:
205
+ current_seconds = self.selected_end[0]
206
+ new_seconds = current_seconds + direction
207
+ # Ensure end doesn't go below start
208
+ start_seconds = self.selected_start[0]
209
+ new_seconds = max(start_seconds + 1, min(new_seconds, int(self.video_meta['video_length_s'])))
210
+ self.end_scale.set_value(int(new_seconds))
211
+ self.selected_end[0] = int(new_seconds)
212
+ # Recalculate frame from seconds
213
+ new_frame = int(new_seconds * self.video_meta['fps'])
214
+ new_frame = max(0, min(new_frame, self.video_meta['frame_count'] - 1))
215
+ self.selected_end_frame[0] = new_frame
216
+ else:
217
+ # Get current frame from the selected_end_frame
218
+ current_frame = self.selected_end_frame[0]
219
+ new_frame = current_frame + direction
220
+ # Ensure end doesn't go below start
221
+ start_frame = self.selected_start_frame[0]
222
+ new_frame = max(start_frame + 1, min(new_frame, self.video_meta['frame_count'] - 1))
223
+ # Convert frame to seconds for the slider
224
+ new_seconds = new_frame / self.video_meta['fps']
225
+ # Update the slider value
226
+ self.end_scale.set_value(new_seconds)
227
+ # Directly update both frame and seconds
228
+ self.selected_end_frame[0] = new_frame
229
+ self.selected_end[0] = new_seconds
230
+
231
+ # Update the display labels and highlights
232
+ if self.use_timestamps:
233
+ self.end_time_label.config(text=seconds_to_timestamp(self.selected_end[0]), fg='red')
234
+ else:
235
+ self.end_time_label.config(text=str(self.selected_end_frame[0]), fg='red')
236
+
237
+ # Update highlights and frame preview
238
+ if self.video_meta['video_length_s'] > 0:
239
+ self._highlight_segment(self.selected_start[0], self.selected_end[0])
240
+ if self._pending_frame_update is not None:
241
+ if hasattr(self, 'img_window') and self.img_window.winfo_exists():
242
+ self.img_window.after_cancel(self._pending_frame_update)
243
+ self._update_frame_display(slider_type='end')
244
+ finally:
245
+ # Rebind the callback
246
+ self.end_scale.scale.config(command=original_cmd)
149
247
 
150
248
  def _schedule_frame_update(self, slider_type: str):
151
249
  """Schedule frame preview update with debouncing.
@@ -164,10 +262,20 @@ class TimelapseSlider():
164
262
  def _update_frame_display(self, slider_type: str):
165
263
  if slider_type == 'start':
166
264
  seconds = self.selected_start[0]
167
- self.frame_label.config(text=f"Start Frame Preview ({seconds_to_timestamp(seconds)})", font=Formats.FONT_LARGE_BOLD.value, fg='green')
265
+ if self.use_timestamps:
266
+ label_text = f"Start Frame Preview ({seconds_to_timestamp(seconds)})"
267
+ else:
268
+ frame_num = int(seconds * self.video_meta['fps'])
269
+ label_text = f"Start Frame Preview (Frame {frame_num})"
270
+ self.frame_label.config(text=label_text, font=Formats.FONT_LARGE_BOLD.value, fg='green')
168
271
  else:
169
272
  seconds = self.selected_end[0]
170
- self.frame_label.config(text=f"End Frame Preview ({seconds_to_timestamp(seconds)})", font=Formats.FONT_LARGE_BOLD.value, fg='red')
273
+ if self.use_timestamps:
274
+ label_text = f"End Frame Preview ({seconds_to_timestamp(seconds)})"
275
+ else:
276
+ frame_num = int(seconds * self.video_meta['fps'])
277
+ label_text = f"End Frame Preview (Frame {frame_num})"
278
+ self.frame_label.config(text=label_text, font=Formats.FONT_LARGE_BOLD.value, fg='red')
171
279
 
172
280
  frame_index = int(seconds * self.video_meta['fps'])
173
281
  if frame_index >= self.video_meta['frame_count']: frame_index = self.video_meta['frame_count'] - 1
@@ -207,7 +315,7 @@ class TimelapseSlider():
207
315
  timelapse_height, timelapse_width = self.timelapse_img.shape[0], self.timelapse_img.shape[1]
208
316
  padded_timelapse = np.zeros((timelapse_height, timelapse_width + (2 * self.padding), 3), dtype=np.uint8)
209
317
  padded_timelapse[:, self.padding:self.padding + timelapse_width] = self.timelapse_img
210
- ruler = ImageMixin.create_time_ruler(video_path=self.video_path, width=timelapse_width, height=self.ruler_height, num_divisions=self.ruler_divisions)
318
+ ruler = ImageMixin.create_time_ruler(video_path=self.video_path, width=timelapse_width, height=self.ruler_height, num_divisions=self.ruler_divisions, show_time=self.use_timestamps)
211
319
  self.timelapse_img = cv2.vconcat([padded_timelapse, ruler])
212
320
 
213
321
  self.original_timelapse = self.timelapse_img.copy()
@@ -241,24 +349,37 @@ class TimelapseSlider():
241
349
  self.start_scale.grid(row=0, column=1, padx=5)
242
350
  self.start_scale.scale.config(command=lambda x: self._update_selection(slider_type='start'))
243
351
 
244
- self.start_time_label = SimBALabel(parent=self.slider_frame, txt="00:00:00", font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='green')
352
+ initial_start_text = "00:00:00" if self.use_timestamps else "0"
353
+ self.start_time_label = SimBALabel(parent=self.slider_frame, txt=initial_start_text, font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='green')
245
354
  self.start_time_label.grid(row=0, column=2, padx=5)
246
355
 
247
- self.start_frame_left_btn = SimbaButton(parent=self.slider_frame, txt="-1s", tooltip_txt="Previous second", cmd=self._move_start_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_green')
356
+ start_btn_txt = "Previous second" if self.use_timestamps else "Previous frame"
357
+ start_btn_tooltip = "Move start time back by 1 second" if self.use_timestamps else "Move start frame back by 1 frame"
358
+ self.start_frame_left_btn = SimbaButton(parent=self.slider_frame, txt=start_btn_txt, tooltip_txt=start_btn_tooltip, cmd=self._move_start_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_green')
248
359
  self.start_frame_left_btn.grid(row=0, column=3, padx=2)
249
- self.start_frame_right_btn = SimbaButton(parent=self.slider_frame, txt="+1s", tooltip_txt="Next second", cmd=self._move_start_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_green')
360
+ start_btn_txt_right = "Next second" if self.use_timestamps else "Next frame"
361
+ start_btn_tooltip_right = "Move start time forward by 1 second" if self.use_timestamps else "Move start frame forward by 1 frame"
362
+ self.start_frame_right_btn = SimbaButton(parent=self.slider_frame, txt=start_btn_txt_right, tooltip_txt=start_btn_tooltip_right, cmd=self._move_start_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_green')
250
363
  self.start_frame_right_btn.grid(row=0, column=4, padx=2)
251
364
 
252
365
  self.end_scale = SimBAScaleBar(parent=self.slider_frame, label="END TIME:", from_=0, to=int(self.video_meta['video_length_s']), orient=HORIZONTAL, length=400, resolution=1, value=int(self.video_meta['video_length_s']), showvalue=False, label_width=15, sliderrelief='raised', troughcolor='white', activebackground='red', lbl_font=Formats.FONT_LARGE_BOLD.value)
253
366
  self.end_scale.grid(row=1, column=1, padx=5)
254
367
  self.end_scale.scale.config(command=lambda x: self._update_selection(slider_type='end'))
255
368
 
256
- self.end_time_label = SimBALabel(parent=self.slider_frame, txt=seconds_to_timestamp(int(self.video_meta['video_length_s'])), font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='red')
369
+ if self.use_timestamps:
370
+ initial_end_text = seconds_to_timestamp(int(self.video_meta['video_length_s']))
371
+ else:
372
+ initial_end_text = str(self.video_meta['frame_count'] - 1)
373
+ self.end_time_label = SimBALabel(parent=self.slider_frame, txt=initial_end_text, font=Formats.FONT_LARGE_BOLD.value, width=10, txt_clr='red')
257
374
  self.end_time_label.grid(row=1, column=2, padx=5)
258
375
 
259
- self.end_frame_left_btn = SimbaButton(parent=self.slider_frame, txt="-1s", tooltip_txt="Previous second", cmd=self._move_end_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_red')
376
+ end_btn_txt = "Previous second" if self.use_timestamps else "Previous frame"
377
+ end_btn_tooltip = "Move end time back by 1 second" if self.use_timestamps else "Move end frame back by 1 frame"
378
+ self.end_frame_left_btn = SimbaButton(parent=self.slider_frame, txt=end_btn_txt, tooltip_txt=end_btn_tooltip, cmd=self._move_end_frame, cmd_kwargs={'direction': -1}, font=Formats.FONT_REGULAR_BOLD.value, img='left_arrow_red')
260
379
  self.end_frame_left_btn.grid(row=1, column=3, padx=2)
261
- self.end_frame_right_btn = SimbaButton(parent=self.slider_frame, txt="+1s", tooltip_txt="Next second", cmd=self._move_end_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_red')
380
+ end_btn_txt_right = "Next second" if self.use_timestamps else "Next frame"
381
+ end_btn_tooltip_right = "Move end time forward by 1 second" if self.use_timestamps else "Move end frame forward by 1 frame"
382
+ self.end_frame_right_btn = SimbaButton(parent=self.slider_frame, txt=end_btn_txt_right, tooltip_txt=end_btn_tooltip_right, cmd=self._move_end_frame, cmd_kwargs={'direction': 1}, font=Formats.FONT_REGULAR_BOLD.value, img='right_arrow_red')
262
383
  self.end_frame_right_btn.grid(row=1, column=4, padx=2)
263
384
 
264
385
  self.selected_start = [0]
@@ -325,8 +446,9 @@ class TimelapseSlider():
325
446
 
326
447
 
327
448
 
328
- #
449
+
329
450
  # x = TimelapseSlider(video_path=r"E:\troubleshooting\mitra_emergence\project_folder\clip_test\Box1_180mISOcontrol_Females_clipped_progress_bar.mp4",
330
451
  # frame_cnt=25,
331
- # crop_ratio=75)
452
+ # crop_ratio=75,
453
+ # use_timestamps=False)
332
454
  # x.run()
@@ -56,7 +56,7 @@ from simba.utils.lookups import (get_current_time, get_ffmpeg_codec,
56
56
  get_ffmpeg_crossfade_methods, get_fonts,
57
57
  get_named_colors, percent_to_crf_lookup,
58
58
  percent_to_qv_lk, quality_pct_to_crf,
59
- video_quality_to_preset_lookup)
59
+ video_quality_to_preset_lookup, get_ffmpeg_encoders)
60
60
  from simba.utils.printing import SimbaTimer, stdout_information, stdout_success
61
61
  from simba.utils.read_write import (
62
62
  check_if_hhmmss_timestamp_is_valid_part_of_video,
@@ -440,10 +440,11 @@ def clahe_enhance_video(file_path: Union[str, os.PathLike],
440
440
  dir, file_name, file_ext = get_fn_ext(filepath=file_path)
441
441
  if out_path is None:
442
442
  save_path = os.path.join(dir, f"CLAHE_{file_name}.avi")
443
+ fourcc = cv2.VideoWriter_fourcc(*Formats.AVI_CODEC.value)
443
444
  else:
444
445
  check_if_dir_exists(in_dir=os.path.dirname(out_path), source=f'{clahe_enhance_video.__name__} out_path')
446
+ fourcc = cv2.VideoWriter_fourcc(*Formats.MP4_CODEC.value)
445
447
  save_path = out_path
446
- fourcc = cv2.VideoWriter_fourcc(*Formats.AVI_CODEC.value)
447
448
  if verbose: print(f"Applying CLAHE on video {file_name}, this might take awhile...")
448
449
  cap = cv2.VideoCapture(file_path)
449
450
  writer = cv2.VideoWriter( save_path, fourcc, video_meta_data["fps"], (video_meta_data["width"], video_meta_data["height"]), 0)
@@ -3265,6 +3266,9 @@ def convert_to_avi(path: Union[str, os.PathLike],
3265
3266
  timer = SimbaTimer(start=True)
3266
3267
  check_ffmpeg_available(raise_error=True)
3267
3268
  check_str(name=f'{convert_to_avi.__name__} codec', value=codec, options=('xvid', 'divx', 'mjpeg'))
3269
+ CODEC_LK = {'mpeg4': 'mpeg4', 'divx': 'libxvid', 'mjpeg': 'mjpeg'}
3270
+ codec = CODEC_LK[codec]
3271
+ check_valid_codec(codec=codec, raise_error=True, source=f'{convert_to_avi.__name__} codec')
3268
3272
  check_instance(source=f'{convert_to_avi.__name__} path', instance=path, accepted_types=(str,))
3269
3273
  check_int(name=f'{convert_to_avi.__name__} quality', value=quality)
3270
3274
  datetime_ = datetime.now().strftime("%Y%m%d%H%M%S")
@@ -3292,18 +3296,18 @@ def convert_to_avi(path: Union[str, os.PathLike],
3292
3296
  _ = get_video_meta_data(video_path=file_path)
3293
3297
  out_path = os.path.join(save_dir, f'{video_name}.avi')
3294
3298
  if codec == 'divx':
3295
- cmd = f'ffmpeg -i "{file_path}" -c:v mpeg4 -crf {crf} -vtag DIVX "{out_path}" -loglevel error -stats -hide_banner -y'
3299
+ cmd = f'ffmpeg -i "{file_path}" -c:v {codec} -crf {crf} -vtag DIVX "{out_path}" -loglevel error -stats -hide_banner -y'
3296
3300
  elif codec == 'xvid':
3297
- cmd = f'ffmpeg -i "{file_path}" -c:v libxvid -q:v {qv} "{out_path}" -loglevel error -stats -hide_banner -y'
3301
+ cmd = f'ffmpeg -i "{file_path}" -c:v {codec} -q:v {qv} "{out_path}" -loglevel error -stats -hide_banner -y'
3298
3302
  else:
3299
- cmd = f'ffmpeg -i "{file_path}" -c:v mjpeg -q:v {qv} "{out_path}" -loglevel error -stats -hide_banner -y'
3303
+ cmd = f'ffmpeg -i "{file_path}" -c:v {codec} -q:v {qv} "{out_path}" -loglevel error -stats -hide_banner -y'
3300
3304
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
3301
3305
  timer.stop_timer()
3302
3306
  stdout_success(msg=f"{len(file_paths)} video(s) converted to AVI and saved in {save_dir} directory.", elapsed_time=timer.elapsed_time_str, source=convert_to_avi.__name__,)
3303
3307
 
3304
3308
 
3305
3309
  def convert_to_webm(path: Union[str, os.PathLike],
3306
- codec: Literal['vp8', 'vp9'] = 'vp9',
3310
+ codec: Literal['vp8', 'vp9', 'av1'] = 'vp9',
3307
3311
  save_dir: Optional[Union[str, os.PathLike]] = None,
3308
3312
  quality: Optional[int] = 60) -> None:
3309
3313
 
@@ -3326,7 +3330,10 @@ def convert_to_webm(path: Union[str, os.PathLike],
3326
3330
 
3327
3331
  timer = SimbaTimer(start=True)
3328
3332
  check_ffmpeg_available(raise_error=True)
3329
- check_str(name=f'{convert_to_webm.__name__} codec', value=codec, options=('vp8', 'vp9'))
3333
+ check_str(name=f'{convert_to_webm.__name__} codec', value=codec, options=('vp8', 'vp9', 'av1'))
3334
+ CODEC_LK = {'vp8': 'libvpx', 'vp9': 'libvpx-vp9', 'av1': 'libaom-av1'}
3335
+ codec = CODEC_LK[codec]
3336
+ check_valid_codec(codec=codec, raise_error=True, source=f'{convert_to_webm.__name__} codec')
3330
3337
  check_instance(source=f'{convert_to_webm.__name__} path', instance=path, accepted_types=(str,))
3331
3338
  check_int(name=f'{convert_to_webm.__name__} quality', value=quality)
3332
3339
  datetime_ = datetime.now().strftime("%Y%m%d%H%M%S")
@@ -3346,17 +3353,13 @@ def convert_to_webm(path: Union[str, os.PathLike],
3346
3353
  os.makedirs(save_dir)
3347
3354
  else:
3348
3355
  raise InvalidInputError(msg=f'Paths is not a valid file or directory path.', source=convert_to_webm.__name__)
3356
+
3349
3357
  for file_cnt, file_path in enumerate(file_paths):
3350
3358
  _, video_name, _ = get_fn_ext(filepath=file_path)
3351
3359
  print(f'Converting video {video_name} to WEBM (Video {file_cnt+1}/{len(file_paths)})...')
3352
3360
  _ = get_video_meta_data(video_path=file_path)
3353
3361
  out_path = os.path.join(save_dir, f'{video_name}.webm')
3354
- if codec == 'vp8':
3355
- cmd = f'ffmpeg -i "{file_path}" -c:v libvpx -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3356
- elif codec == 'vp9':
3357
- cmd = f'ffmpeg -i "{file_path}" -c:v libvpx-vp9 -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3358
- else:
3359
- cmd = f'ffmpeg -i "{file_path}" -c:v libaom-av1 -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3362
+ cmd = f'ffmpeg -i "{file_path}" -c:v {codec} -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3360
3363
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
3361
3364
  timer.stop_timer()
3362
3365
  stdout_success(msg=f"{len(file_paths)} video(s) converted to WEBM and saved in {save_dir} directory.", elapsed_time=timer.elapsed_time_str, source=convert_to_webm.__name__,)
@@ -3381,6 +3384,10 @@ def convert_to_mov(path: Union[str, os.PathLike],
3381
3384
  timer = SimbaTimer(start=True)
3382
3385
  check_ffmpeg_available(raise_error=True)
3383
3386
  check_str(name=f'{convert_to_mov.__name__} codec', value=codec, options=('prores', 'animation', 'cineform', 'dnxhd'))
3387
+
3388
+
3389
+
3390
+
3384
3391
  check_instance(source=f'{convert_to_mov.__name__} path', instance=path, accepted_types=(str,))
3385
3392
  check_int(name=f'{convert_to_mov.__name__} quality', value=quality)
3386
3393
  datetime_ = datetime.now().strftime("%Y%m%d%H%M%S")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simba-uw-tf-dev
3
- Version: 4.7.4
3
+ Version: 4.7.6
4
4
  Summary: Toolkit for computer classification and analysis of behaviors in experimental animals
5
5
  Home-page: https://github.com/sgoldenlab/simba
6
6
  Author: Simon Nilsson, Jia Jie Choong, Sophia Hwang