simba-uw-tf-dev 4.5.8__py3-none-any.whl → 4.7.1__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.
Files changed (98) hide show
  1. simba/SimBA.py +2 -2
  2. simba/assets/.recent_projects.txt +1 -0
  3. simba/assets/icons/frames_2.png +0 -0
  4. simba/assets/lookups/tooptips.json +15 -1
  5. simba/data_processors/agg_clf_counter_mp.py +52 -53
  6. simba/data_processors/blob_location_computer.py +1 -1
  7. simba/data_processors/circling_detector.py +30 -13
  8. simba/data_processors/cuda/geometry.py +45 -27
  9. simba/data_processors/cuda/image.py +1648 -1598
  10. simba/data_processors/cuda/statistics.py +72 -26
  11. simba/data_processors/cuda/timeseries.py +1 -1
  12. simba/data_processors/cue_light_analyzer.py +5 -9
  13. simba/data_processors/egocentric_aligner.py +25 -7
  14. simba/data_processors/freezing_detector.py +55 -47
  15. simba/data_processors/kleinberg_calculator.py +61 -29
  16. simba/feature_extractors/feature_subsets.py +14 -7
  17. simba/feature_extractors/mitra_feature_extractor.py +2 -2
  18. simba/feature_extractors/straub_tail_analyzer.py +4 -6
  19. simba/labelling/standard_labeller.py +1 -1
  20. simba/mixins/config_reader.py +5 -2
  21. simba/mixins/geometry_mixin.py +22 -36
  22. simba/mixins/image_mixin.py +24 -28
  23. simba/mixins/plotting_mixin.py +28 -10
  24. simba/mixins/statistics_mixin.py +48 -11
  25. simba/mixins/timeseries_features_mixin.py +1 -1
  26. simba/mixins/train_model_mixin.py +67 -29
  27. simba/model/inference_batch.py +1 -1
  28. simba/model/yolo_seg_inference.py +3 -3
  29. simba/outlier_tools/skip_outlier_correction.py +1 -1
  30. simba/plotting/ROI_feature_visualizer_mp.py +3 -5
  31. simba/plotting/clf_validator_mp.py +4 -5
  32. simba/plotting/cue_light_visualizer.py +6 -7
  33. simba/plotting/directing_animals_visualizer_mp.py +2 -3
  34. simba/plotting/distance_plotter_mp.py +378 -378
  35. simba/plotting/frame_mergerer_ffmpeg.py +137 -196
  36. simba/plotting/gantt_creator.py +29 -10
  37. simba/plotting/gantt_creator_mp.py +96 -33
  38. simba/plotting/geometry_plotter.py +270 -272
  39. simba/plotting/heat_mapper_clf_mp.py +4 -6
  40. simba/plotting/heat_mapper_location_mp.py +2 -2
  41. simba/plotting/light_dark_box_plotter.py +2 -2
  42. simba/plotting/path_plotter_mp.py +26 -29
  43. simba/plotting/plot_clf_results_mp.py +455 -454
  44. simba/plotting/pose_plotter_mp.py +28 -29
  45. simba/plotting/probability_plot_creator_mp.py +288 -288
  46. simba/plotting/roi_plotter_mp.py +31 -31
  47. simba/plotting/single_run_model_validation_video_mp.py +427 -427
  48. simba/plotting/spontaneous_alternation_plotter.py +2 -3
  49. simba/plotting/yolo_pose_track_visualizer.py +32 -27
  50. simba/plotting/yolo_pose_visualizer.py +35 -36
  51. simba/plotting/yolo_seg_visualizer.py +2 -3
  52. simba/pose_importers/simba_blob_importer.py +3 -3
  53. simba/roi_tools/roi_aggregate_stats_mp.py +5 -4
  54. simba/roi_tools/roi_clf_calculator_mp.py +4 -4
  55. simba/sandbox/analyze_runtimes.py +30 -0
  56. simba/sandbox/cuda/egocentric_rotator.py +374 -0
  57. simba/sandbox/get_cpu_pool.py +5 -0
  58. simba/sandbox/proboscis_to_tip.py +28 -0
  59. simba/sandbox/test_directionality.py +47 -0
  60. simba/sandbox/test_nonstatic_directionality.py +27 -0
  61. simba/sandbox/test_pycharm_cuda.py +51 -0
  62. simba/sandbox/test_simba_install.py +41 -0
  63. simba/sandbox/test_static_directionality.py +26 -0
  64. simba/sandbox/test_static_directionality_2d.py +26 -0
  65. simba/sandbox/verify_env.py +42 -0
  66. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo.py +3 -3
  67. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo_bbox.py +2 -2
  68. simba/ui/pop_ups/clf_add_remove_print_pop_up.py +37 -30
  69. simba/ui/pop_ups/clf_plot_pop_up.py +2 -2
  70. simba/ui/pop_ups/egocentric_alignment_pop_up.py +20 -21
  71. simba/ui/pop_ups/fsttc_pop_up.py +27 -25
  72. simba/ui/pop_ups/gantt_pop_up.py +31 -6
  73. simba/ui/pop_ups/interpolate_pop_up.py +2 -4
  74. simba/ui/pop_ups/kleinberg_pop_up.py +39 -40
  75. simba/ui/pop_ups/multiple_videos_to_frames_popup.py +10 -11
  76. simba/ui/pop_ups/single_video_to_frames_popup.py +10 -10
  77. simba/ui/pop_ups/video_processing_pop_up.py +186 -174
  78. simba/ui/tkinter_functions.py +10 -1
  79. simba/utils/custom_feature_extractor.py +1 -1
  80. simba/utils/data.py +90 -14
  81. simba/utils/enums.py +1 -0
  82. simba/utils/errors.py +441 -440
  83. simba/utils/lookups.py +1203 -1203
  84. simba/utils/printing.py +124 -124
  85. simba/utils/read_write.py +3769 -3721
  86. simba/utils/yolo.py +10 -1
  87. simba/video_processors/blob_tracking_executor.py +2 -2
  88. simba/video_processors/clahe_ui.py +66 -23
  89. simba/video_processors/egocentric_video_rotator.py +46 -44
  90. simba/video_processors/multi_cropper.py +1 -1
  91. simba/video_processors/video_processing.py +5264 -5300
  92. simba/video_processors/videos_to_frames.py +43 -32
  93. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/METADATA +4 -3
  94. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/RECORD +98 -86
  95. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/LICENSE +0 -0
  96. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/WHEEL +0 -0
  97. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/entry_points.txt +0 -0
  98. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,14 @@ from simba.data_processors.pybursts_calculator import kleinberg_burst_detection
13
13
  from simba.mixins.config_reader import ConfigReader
14
14
  from simba.utils.checks import (check_float, check_if_dir_exists,
15
15
  check_if_filepath_list_is_empty, check_int,
16
- check_that_column_exist, check_valid_lst)
16
+ check_that_column_exist, check_valid_boolean,
17
+ check_valid_lst)
17
18
  from simba.utils.enums import Paths, TagNames
18
19
  from simba.utils.printing import SimbaTimer, log_event, stdout_success
19
- from simba.utils.read_write import get_fn_ext, read_df, write_df
20
+ from simba.utils.read_write import (copy_files_to_directory,
21
+ find_files_of_filetypes_in_directory,
22
+ get_current_time, get_fn_ext, read_df,
23
+ remove_a_folder, write_df)
20
24
  from simba.utils.warnings import KleinbergWarning
21
25
 
22
26
 
@@ -38,12 +42,13 @@ class KleinbergCalculator(ConfigReader):
38
42
 
39
43
  :param str config_path: path to SimBA project config file in Configparser format
40
44
  :param List[str] classifier_names: Classifier names to apply Kleinberg smoothing to.
41
- :param float sigma: Burst detection sigma value. Higher sigma values and fewer, longer, behavioural bursts will be recognised. Default: 2.
42
- :param float gamma: Burst detection gamma value. Higher gamma values and fewer behavioural bursts will be recognised. Default: 0.3.
43
- :param int hierarchy: Burst detection hierarchy level. Higher hierarchy values and fewer behavioural bursts will to be recognised. Default: 1.
44
- :param bool hierarchical_search: See `Tutorial <https://github.com/sgoldenlab/simba/blob/master/docs/kleinberg_filter.md#hierarchical-search-example>`_ Default: False.
45
+ :param float sigma: State transition cost for moving to higher burst levels. Higher values (e.g., 2-3) produce fewer but longer bursts; lower values (e.g., 1.1-1.5) detect more frequent, shorter bursts. Must be > 1.01. Default: 2.
46
+ :param float gamma: State transition cost for moving to lower burst levels. Higher values (e.g., 0.5-1.0) reduce total burst count by making downward transitions costly; lower values (e.g., 0.1-0.3) allow more flexible state changes. Must be >= 0. Default: 0.3.
47
+ :param int hierarchy: Hierarchy level to extract bursts from (0=lowest, higher=more selective). Level 0 captures all bursts; level 1-2 typically filters noise; level 3+ selects only the most prominent, sustained bursts. Higher levels yield fewer but more confident detections. Must be >= 0. Default: 1.
48
+ :param bool hierarchical_search: If True, searches for target hierarchy level within detected burst periods, falling back to lower levels if target not found. If False, extracts only bursts at the exact specified hierarchy level. Recommended when target hierarchy may be sparse. Default: False.
45
49
  :param Optional[Union[str, os.PathLike]] input_dir: The directory with files to perform kleinberg smoothing on. If None, defaults to `project_folder/csv/machine_results`
46
50
  :param Optional[Union[str, os.PathLike]] output_dir: Location to save smoothened data in. If None, defaults to `project_folder/csv/machine_results`
51
+ :param Optional[bool] save_originals: If True, saves the original data in sub-directory of the ouput directory.`
47
52
 
48
53
  :example I:
49
54
  >>> kleinberg_calculator = KleinbergCalculator(config_path='MySimBAConfigPath', classifier_names=['Attack'], sigma=2, gamma=0.3, hierarchy=2, hierarchical_search=False)
@@ -68,10 +73,12 @@ class KleinbergCalculator(ConfigReader):
68
73
 
69
74
  def __init__(self,
70
75
  config_path: Union[str, os.PathLike],
71
- classifier_names: List[str],
72
- sigma: Optional[int] = 2,
73
- gamma: Optional[float] = 0.3,
76
+ classifier_names: Optional[List[str]] = None,
77
+ sigma: float = 2,
78
+ gamma: float = 0.3,
74
79
  hierarchy: Optional[int] = 1,
80
+ verbose: bool = True,
81
+ save_originals: bool = True,
75
82
  hierarchical_search: Optional[bool] = False,
76
83
  input_dir: Optional[Union[str, os.PathLike]] = None,
77
84
  output_dir: Optional[Union[str, os.PathLike]] = None):
@@ -81,25 +88,31 @@ class KleinbergCalculator(ConfigReader):
81
88
  check_float(value=sigma, name=f'{self.__class__.__name__} sigma', min_value=1.01)
82
89
  check_float(value=gamma, name=f'{self.__class__.__name__} gamma', min_value=0)
83
90
  check_int(value=hierarchy, name=f'{self.__class__.__name__} hierarchy', min_value=0)
84
- check_valid_lst(data=classifier_names, source=f'{self.__class__.__name__} classifier_names', valid_dtypes=(str,), min_len=1)
91
+ if isinstance(classifier_names, list):
92
+ check_valid_lst(data=classifier_names, source=f'{self.__class__.__name__} classifier_names', valid_dtypes=(str,), min_len=1)
93
+ else:
94
+ classifier_names = deepcopy(self.clf_names)
95
+ check_valid_boolean(value=verbose, source=f'{self.__class__.__name__} verbose', raise_error=True)
96
+ check_valid_boolean(value=save_originals, source=f'{self.__class__.__name__} save_originals', raise_error=True)
85
97
  self.hierarchical_search, sigma, gamma, hierarchy, self.output_dir = (hierarchical_search, float(sigma), float(gamma), int(hierarchy), output_dir)
86
- self.sigma, self.gamma, self.hierarchy, self.clfs = ( float(sigma), float(gamma), float(hierarchy), classifier_names)
98
+ self.sigma, self.gamma, self.hierarchy, self.clfs = ( float(sigma), float(gamma), int(hierarchy), classifier_names)
99
+ self.verbose, self.save_originals = verbose, save_originals
87
100
  if input_dir is None:
88
- self.data_paths, self.output_dir = self.machine_results_paths, self.machine_results_dir
89
- check_if_filepath_list_is_empty(filepaths=self.machine_results_paths, error_msg=f"SIMBA ERROR: No data files found in {self.machine_results_dir}. Cannot perform Kleinberg smoothing")
90
- original_data_files_folder = os.path.join(self.project_path, Paths.MACHINE_RESULTS_DIR.value, f"Pre_Kleinberg_{self.datetime}")
91
- if not os.path.exists(original_data_files_folder):
92
- os.makedirs(original_data_files_folder)
93
- for file_path in self.machine_results_paths:
94
- _, file_name, ext = get_fn_ext(file_path)
95
- shutil.copyfile(file_path, os.path.join(original_data_files_folder, file_name + ext))
101
+ self.input_dir = os.path.join(self.project_path, Paths.MACHINE_RESULTS_DIR.value)
96
102
  else:
97
103
  check_if_dir_exists(in_dir=input_dir)
98
- self.data_paths = glob.glob(input_dir + f"/*.{self.file_type}")
99
- check_if_filepath_list_is_empty(filepaths=self.data_paths, error_msg=f"SIMBA ERROR: No data files found in {input_dir}. Cannot perform Kleinberg smoothing")
100
- if not os.path.isdir(output_dir):
101
- os.makedirs(output_dir)
102
- print(f"Processing Kleinberg burst detection for {len(self.data_paths)} file(s) and {len(classifier_names)} classifier(s)...")
104
+ self.input_dir = deepcopy(input_dir)
105
+ self.data_paths = find_files_of_filetypes_in_directory(directory=self.input_dir, extensions=[f'.{self.file_type}'], sort_alphabetically=True, raise_error=True)
106
+ if output_dir is None:
107
+ self.output_dir = deepcopy(self.input_dir)
108
+ else:
109
+ check_if_dir_exists(in_dir=output_dir)
110
+ self.output_dir = deepcopy(output_dir)
111
+ self.original_data_files_folder = os.path.join(self.output_dir, f"Pre_Kleinberg_{self.datetime}")
112
+ remove_a_folder(folder_dir=self.original_data_files_folder, ignore_errors=True)
113
+ os.makedirs(self.original_data_files_folder)
114
+ copy_files_to_directory(file_paths=self.data_paths, dir=self.original_data_files_folder, verbose=False, integer_save_names=False)
115
+ if self.verbose: print(f"Processing Kleinberg burst detection for {len(self.data_paths)} file(s) and {len(classifier_names)} classifier(s)...")
103
116
 
104
117
  def hierarchical_searcher(self):
105
118
  if (len(self.kleinberg_bouts["Hierarchy"]) == 1) and (int(self.kleinberg_bouts.at[0, "Hierarchy"]) == 0):
@@ -135,7 +148,7 @@ class KleinbergCalculator(ConfigReader):
135
148
  for file_cnt, file_path in enumerate(self.data_paths):
136
149
  _, video_name, _ = get_fn_ext(file_path)
137
150
  video_timer = SimbaTimer(start=True)
138
- print(f"Performing Kleinberg burst detection for video {video_name} (Video {file_cnt+1}/{len(self.data_paths)})...")
151
+ if self.verbose: print(f"[{get_current_time()}] Performing Kleinberg burst detection for video {video_name} (Video {file_cnt+1}/{len(self.data_paths)})...")
139
152
  data_df = read_df(file_path, self.file_type).reset_index(drop=True)
140
153
  video_out_df = deepcopy(data_df)
141
154
  check_that_column_exist(df=data_df, column_name=self.clfs, file_name=video_name)
@@ -150,7 +163,7 @@ class KleinbergCalculator(ConfigReader):
150
163
  self.kleinberg_bouts.insert(loc=0, column="Video", value=video_name)
151
164
  detailed_df_lst.append(self.kleinberg_bouts)
152
165
  if self.hierarchical_search:
153
- print(f"Applying hierarchical search for video {video_name}...")
166
+ if self.verbose: print(f"[{get_current_time()}] Applying hierarchical search for video {video_name}...")
154
167
  self.hierarchical_searcher()
155
168
  else:
156
169
  self.clf_bouts_in_hierarchy = self.kleinberg_bouts[self.kleinberg_bouts["Hierarchy"] == self.hierarchy]
@@ -160,19 +173,38 @@ class KleinbergCalculator(ConfigReader):
160
173
  video_out_df.loc[hierarchy_idx, clf] = 1
161
174
  write_df(video_out_df, self.file_type, save_path)
162
175
  video_timer.stop_timer()
163
- print(f'Kleinberg analysis complete for video {video_name} (saved at {save_path}), elapsed time: {video_timer.elapsed_time_str}s.')
176
+ if self.verbose: print(f'[{get_current_time()}] Kleinberg analysis complete for video {video_name} (saved at {save_path}), elapsed time: {video_timer.elapsed_time_str}s.')
164
177
 
165
178
  self.timer.stop_timer()
179
+ if not self.save_originals:
180
+ remove_a_folder(folder_dir=self.original_data_files_folder, ignore_errors=False)
181
+ else:
182
+ if self.verbose: stdout_success(msg=f"Original, un-smoothened data, saved in {self.original_data_files_folder} directory", elapsed_time=self.timer.elapsed_time_str, source=self.__class__.__name__)
166
183
  if len(detailed_df_lst) > 0:
167
184
  self.detailed_df = pd.concat(detailed_df_lst, axis=0)
168
185
  detailed_save_path = os.path.join(self.logs_path, f"Kleinberg_detailed_log_{self.datetime}.csv")
169
186
  self.detailed_df.to_csv(detailed_save_path)
170
- stdout_success(msg=f"Kleinberg analysis complete. See {detailed_save_path} for details of detected bouts of all classifiers in all hierarchies", elapsed_time=self.timer.elapsed_time_str, source=self.__class__.__name__)
187
+ if self.verbose: stdout_success(msg=f"Kleinberg analysis complete for {len(self.data_paths)} files. Results stored in {self.output_dir} directory. See {detailed_save_path} for details of detected bouts of all classifiers in all hierarchies", elapsed_time=self.timer.elapsed_time_str, source=self.__class__.__name__)
171
188
  else:
172
- print("Kleinberg analysis complete.")
189
+ if self.verbose: print(f"[{get_current_time()}] Kleinberg analysis complete for {len(self.data_paths)} files. Results stored in {self.output_dir} directory.")
173
190
  KleinbergWarning(msg="All behavior bouts removed following kleinberg smoothing", source=self.__class__.__name__)
174
191
 
175
192
 
193
+
194
+
195
+ # test = KleinbergCalculator(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini",
196
+ # classifier_names=['straub_tail'],
197
+ # sigma=1.1,
198
+ # gamma=0.1,
199
+ # hierarchy=1,
200
+ # save_originals=False,
201
+ # hierarchical_search=False)
202
+ #
203
+ # test.run()
204
+ #
205
+
206
+
207
+
176
208
  # test = KleinbergCalculator(config_path='/Users/simon/Desktop/envs/simba/troubleshooting/levi/project_folder/project_config.ini',
177
209
  # classifier_names=['No_Seizure_(0)'],
178
210
  # sigma=1.1,
@@ -83,12 +83,12 @@ class FeatureSubsetsCalculator(ConfigReader, TrainModelMixin):
83
83
  :align: center
84
84
 
85
85
  :example:
86
- >>> test = FeatureSubsetsCalculator(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini",
86
+ >>> test = FeatureSubsetsCalculator(config_path=r"C:/troubleshooting/mitra/project_folder/project_config.ini",
87
87
  >>> feature_families=[FRAME_BP_MOVEMENT, WITHIN_ANIMAL_THREE_POINT_ANGLES],
88
88
  >>> append_to_features_extracted=False,
89
89
  >>> file_checks=False,
90
90
  >>> append_to_targets_inserted=False,
91
- >>> save_dir=r"C:\troubleshooting\mitra\project_folder\csv\new_features")
91
+ >>> save_dir=r"C:/troubleshooting/mitra/project_folder/csv/new_features")
92
92
  >>> test.run()
93
93
  """
94
94
 
@@ -154,11 +154,11 @@ class FeatureSubsetsCalculator(ConfigReader, TrainModelMixin):
154
154
  self.within_animal_four_point_combs[animal] = np.array(list(combinations(animal_bps, 4)))
155
155
 
156
156
  def _get_two_point_bp_distances(self):
157
- for c in self.two_point_combs:
157
+ for cnt, c in enumerate(self.two_point_combs):
158
158
  x1, y1, x2, y2 = list(sum([(f"{x}_x", f"{y}_y") for (x, y) in zip(c, c)], ()))
159
159
  bp1 = self.data_df[[x1, y1]].values
160
160
  bp2 = self.data_df[[x2, y2]].values
161
- self.results[f"Distance (mm) {c[0]}-{c[1]}"] = FeatureExtractionMixin.framewise_euclidean_distance(location_1=bp1.astype(np.float64), location_2=bp2.astype(np.float64), px_per_mm=np.float64(self.px_per_mm), centimeter=False)
161
+ self.results[f"Distance (mm) {c[0]}-{c[1]}"] = FeatureExtractionMixin.bodypart_distance(bp1_coords=bp1.astype(np.int32), bp2_coords=bp2.astype(np.int32), px_per_mm=np.float64(self.px_per_mm), in_centimeters=False)
162
162
 
163
163
  def __get_three_point_angles(self):
164
164
  for animal, points in self.within_animal_three_point_combs.items():
@@ -342,13 +342,20 @@ class FeatureSubsetsCalculator(ConfigReader, TrainModelMixin):
342
342
 
343
343
 
344
344
 
345
+ # test = FeatureSubsetsCalculator(config_path=r"C:\troubleshooting\srami0619\project_folder\project_config.ini",
346
+ # feature_families=[TWO_POINT_BP_DISTANCES],
347
+ # append_to_features_extracted=False,
348
+ # file_checks=True,
349
+ # append_to_targets_inserted=False)
350
+ # test.run()
351
+
352
+
345
353
 
346
354
  # test = FeatureSubsetsCalculator(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini",
347
- # feature_families=[ARENA_EDGE],
355
+ # feature_families=[TWO_POINT_BP_DISTANCES],
348
356
  # append_to_features_extracted=False,
349
357
  # file_checks=True,
350
- # append_to_targets_inserted=False,
351
- # save_dir=r"C:\troubleshooting\mitra\project_folder\csv\feature_subset")
358
+ # append_to_targets_inserted=False)
352
359
  # test.run()
353
360
 
354
361
  #
@@ -28,7 +28,7 @@ RIGHT_EAR = 'right_ear'
28
28
  CENTER = 'center'
29
29
  TAIL_BASE = 'tail_base'
30
30
  TAIL_CENTER = 'tail_center'
31
- TAIL_TIP = 'tail_tip'
31
+ TAIL_TIP = 'tail_end'
32
32
 
33
33
  TIME_WINDOWS = np.array([0.25, 0.5, 1.0, 2.0])
34
34
 
@@ -207,7 +207,7 @@ class MitraFeatureExtractor(ConfigReader,
207
207
 
208
208
 
209
209
 
210
- # feature_extractor = MitraFeatureExtractor(config_path=r"D:\troubleshooting\mitra\project_folder\project_config.ini")
210
+ # feature_extractor = MitraFeatureExtractor(config_path=r"E:\troubleshooting\mitra_emergence\project_folder\project_config.ini")
211
211
  # feature_extractor.run()
212
212
 
213
213
 
@@ -9,8 +9,6 @@ try:
9
9
  except:
10
10
  from typing_extensions import Literal
11
11
 
12
- import functools
13
- import multiprocessing
14
12
  from copy import deepcopy
15
13
 
16
14
  import numpy as np
@@ -46,10 +44,10 @@ class StraubTailAnalyzer(ConfigReader):
46
44
  .. [1] Lazaro et al., Brainwide Genetic Capture for Conscious State Transitions, `biorxiv`, doi: https://doi.org/10.1101/2025.03.28.646066
47
45
 
48
46
  :example:
49
- >>> runner = StraubTailAnalyzer(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini",
50
- >>> data_dir=r'C:\troubleshooting\mitra\project_folder\videos\additional\bg_removed\rotated',
51
- >>> video_dir=r'C:\troubleshooting\mitra\project_folder\videos\additional\bg_removed\rotated',
52
- >>> save_dir=r'C:\troubleshooting\mitra\project_folder\videos\additional\bg_removed\rotated\tail_features_additional',
47
+ >>> runner = StraubTailAnalyzer(config_path=r"C:/troubleshooting/mitra/project_folder/project_config.ini",
48
+ >>> data_dir=r'C:/troubleshooting/mitra/project_folder/videos/additional/bg_removed/rotated',
49
+ >>> video_dir=r'C:/troubleshooting/mitra/project_folder/videos/additional/bg_removed/rotated',
50
+ >>> save_dir=r'C:/troubleshooting/mitra/project_folder/videos/additional/bg_removed/rotated/tail_features_additional',
53
51
  >>> anchor_points=('tail_base', 'tail_center', 'tail_tip'),
54
52
  >>> body_parts=('nose', 'left_ear', 'right_ear', 'right_side', 'left_side', 'tail_base'))
55
53
  >>> runner.run()
@@ -64,7 +64,7 @@ class LabellingInterface(ConfigReader):
64
64
  :param bool continuing: Set True to resume annotations from an existing targets file. Defaults to False.
65
65
 
66
66
  :example:
67
- >>> _ = LabellingInterface(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini", file_path=r"C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0521.mp4", thresholds=None, continuing=False)
67
+ >>> _ = LabellingInterface(config_path=r"C:/troubleshooting/mitra/project_folder/project_config.ini", file_path=r"C:/troubleshooting/mitra/project_folder/videos/501_MA142_Gi_CNO_0521.mp4", thresholds=None, continuing=False)
68
68
  """
69
69
 
70
70
  def __init__(self,
@@ -41,8 +41,8 @@ from simba.utils.read_write import (find_core_cnt, get_all_clf_names,
41
41
  get_fn_ext, read_config_file, read_df,
42
42
  read_project_path_and_file_type, write_df)
43
43
  from simba.utils.warnings import (BodypartColumnNotFoundWarning,
44
- InvalidValueWarning, NoDataFoundWarning,
45
- NoFileFoundWarning)
44
+ DuplicateNamesWarning, InvalidValueWarning,
45
+ NoDataFoundWarning, NoFileFoundWarning)
46
46
 
47
47
 
48
48
  class ConfigReader(object):
@@ -610,11 +610,14 @@ class ConfigReader(object):
610
610
  >>> config_reader.get_bp_headers()
611
611
  """
612
612
 
613
+ duplicates = list({x for x in self.body_parts_lst if self.body_parts_lst.count(x) > 1})
614
+ if len(duplicates) > 0: DuplicateNamesWarning(msg=f'The pose configuration file at {self.body_parts_path} contains duplicate entries: {duplicates}', source=self.__class__.__name__)
613
615
  self.bp_headers = []
614
616
  for bp in self.body_parts_lst:
615
617
  c1, c2, c3 = (f"{bp}_x", f"{bp}_y", f"{bp}_p")
616
618
  self.bp_headers.extend((c1, c2, c3))
617
619
 
620
+
618
621
  def read_config_entry(
619
622
  self,
620
623
  config: ConfigParser,
@@ -1339,8 +1339,7 @@ class GeometryMixin(object):
1339
1339
  )
1340
1340
  for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1341
1341
  results.append(result)
1342
- pool.join()
1343
- pool.terminate()
1342
+ terminate_cpu_pool(pool=pool, force=False)
1344
1343
  if data_ndim == 2:
1345
1344
  return [i for s in results for i in s]
1346
1345
  else:
@@ -1370,8 +1369,7 @@ class GeometryMixin(object):
1370
1369
  cap_style=cap_style)
1371
1370
  for cnt, mp_return in enumerate(pool.imap(constants, geomety_lst, chunksize=1)):
1372
1371
  results.append(mp_return)
1373
- pool.join()
1374
- pool.terminate()
1372
+ terminate_cpu_pool(pool=pool, force=False)
1375
1373
  return [l for ll in results for l in ll]
1376
1374
 
1377
1375
  def multiframe_bodyparts_to_circle(self,
@@ -1524,8 +1522,7 @@ class GeometryMixin(object):
1524
1522
  )
1525
1523
  for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1526
1524
  results.append(result)
1527
- pool.join()
1528
- pool.terminate()
1525
+ terminate_cpu_pool(pool=pool, force=False)
1529
1526
  return results
1530
1527
 
1531
1528
  def multiframe_compute_pct_shape_overlap(self,
@@ -1559,7 +1556,7 @@ class GeometryMixin(object):
1559
1556
  :rtype: List[float]
1560
1557
 
1561
1558
  :example:
1562
- >>> df = read_df(file_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\csv\outlier_corrected_movement_location\Together_2.csv", file_type='csv').astype(int)
1559
+ >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
1563
1560
  >>> animal_1_cols = [x for x in df.columns if '_1_' in x and not '_p' in x]
1564
1561
  >>> animal_2_cols = [x for x in df.columns if '_2_' in x and not '_p' in x]
1565
1562
  >>> animal_1_arr = df[animal_1_cols].values.reshape(len(df), int(len(animal_1_cols)/ 2), 2)
@@ -1625,7 +1622,7 @@ class GeometryMixin(object):
1625
1622
  :return List[float]: List of overlap between corresponding Polygons. If overlap 1, else 0.
1626
1623
 
1627
1624
  :example:
1628
- >>> df = read_df(file_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\csv\outlier_corrected_movement_location\Together_2.csv", file_type='csv').astype(int)
1625
+ >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
1629
1626
  >>> animal_1_cols = [x for x in df.columns if '_1_' in x and not '_p' in x]
1630
1627
  >>> animal_2_cols = [x for x in df.columns if '_2_' in x and not '_p' in x]
1631
1628
  >>> animal_1_arr = df[animal_1_cols].values.reshape(len(df), int(len(animal_1_cols)/ 2), 2)
@@ -1696,7 +1693,7 @@ class GeometryMixin(object):
1696
1693
  :rtype: List[float]
1697
1694
 
1698
1695
  :example:
1699
- >>> df = read_df(file_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\csv\outlier_corrected_movement_location\Together_2.csv", file_type='csv').astype(int)
1696
+ >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
1700
1697
  >>> animal_1_cols = [x for x in df.columns if '_1_' in x and not '_p' in x]
1701
1698
  >>> animal_2_cols = [x for x in df.columns if '_2_' in x and not '_p' in x]
1702
1699
  >>> animal_1_arr = df[animal_1_cols].values.reshape(len(df), int(len(animal_1_cols)/ 2), 2)
@@ -1766,7 +1763,7 @@ class GeometryMixin(object):
1766
1763
  :rtype: List[Polygon]
1767
1764
 
1768
1765
  :example:
1769
- >>> df = read_df(file_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\csv\outlier_corrected_movement_location\Together_2.csv", file_type='csv').astype(int)
1766
+ >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
1770
1767
  >>> animal_1_cols = [x for x in df.columns if '_1_' in x and not '_p' in x]
1771
1768
  >>> animal_1_arr = df[animal_1_cols].values.reshape(len(df), int(len(animal_1_cols)/ 2), 2)
1772
1769
  >>> animal_1_geo = GeometryMixin.bodyparts_to_polygon(data=animal_1_arr)
@@ -1798,8 +1795,7 @@ class GeometryMixin(object):
1798
1795
  timer.stop_timer()
1799
1796
  if verbose:
1800
1797
  stdout_success(msg="Rotated rectangles complete.", elapsed_time=timer.elapsed_time_str)
1801
- pool.join()
1802
- pool.terminate()
1798
+ terminate_cpu_pool(pool=pool, force=False)
1803
1799
  return results
1804
1800
 
1805
1801
  @staticmethod
@@ -2003,8 +1999,7 @@ class GeometryMixin(object):
2003
1999
  )
2004
2000
  for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
2005
2001
  results.append(result)
2006
- pool.join()
2007
- pool.terminate()
2002
+ terminate_cpu_pool(pool=pool, force=False)
2008
2003
  return results
2009
2004
 
2010
2005
  def multiframe_union(self, shapes: Iterable[Union[LineString, MultiLineString, Polygon]], core_cnt: int = -1) -> \
@@ -2043,8 +2038,7 @@ class GeometryMixin(object):
2043
2038
  with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
2044
2039
  for cnt, result in enumerate(pool.imap(GeometryMixin().union, shapes, chunksize=1)):
2045
2040
  results.append(result)
2046
- pool.join()
2047
- pool.terminate()
2041
+ terminate_cpu_pool(pool=pool, force=False)
2048
2042
  return results
2049
2043
 
2050
2044
  def multiframe_symmetric_difference(self, shapes: Iterable[Union[LineString, MultiLineString, Polygon]],
@@ -2084,8 +2078,7 @@ class GeometryMixin(object):
2084
2078
  pool.imap(GeometryMixin().symmetric_difference, shapes, chunksize=1)
2085
2079
  ):
2086
2080
  results.append(result)
2087
- pool.join()
2088
- pool.terminate()
2081
+ terminate_cpu_pool(pool=pool, force=False)
2089
2082
  return results
2090
2083
 
2091
2084
  def multiframe_delaunay_triangulate_keypoints(self, data: np.ndarray, core_cnt: int = -1) -> List[List[Polygon]]:
@@ -2132,8 +2125,7 @@ class GeometryMixin(object):
2132
2125
  ):
2133
2126
  results.append(result)
2134
2127
 
2135
- pool.join()
2136
- pool.terminate()
2128
+ terminate_cpu_pool(pool=pool, force=False)
2137
2129
  return results
2138
2130
 
2139
2131
  def multiframe_difference(
@@ -2221,8 +2213,7 @@ class GeometryMixin(object):
2221
2213
  msg="Multi-frame difference compute complete",
2222
2214
  elapsed_time=timer.elapsed_time_str,
2223
2215
  )
2224
- pool.join()
2225
- pool.terminate()
2216
+ terminate_cpu_pool(pool=pool, force=False)
2226
2217
  return results
2227
2218
 
2228
2219
  def multiframe_area(self,
@@ -2276,8 +2267,7 @@ class GeometryMixin(object):
2276
2267
 
2277
2268
  timer.stop_timer()
2278
2269
  stdout_success(msg="Multi-frame area compute complete", elapsed_time=timer.elapsed_time_str)
2279
- pool.join()
2280
- pool.terminate()
2270
+ terminate_cpu_pool(pool=pool, force=False)
2281
2271
  return results
2282
2272
 
2283
2273
  def multiframe_bodyparts_to_multistring_skeleton(
@@ -2619,8 +2609,7 @@ class GeometryMixin(object):
2619
2609
  pool.imap(GeometryMixin.is_shape_covered, shapes, chunksize=1)
2620
2610
  ):
2621
2611
  results.append(mp_return)
2622
- pool.join()
2623
- pool.terminate()
2612
+ terminate_cpu_pool(pool=pool, force=False)
2624
2613
  return results
2625
2614
 
2626
2615
  @staticmethod
@@ -3321,8 +3310,7 @@ class GeometryMixin(object):
3321
3310
  for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3322
3311
  if result[1] != -1:
3323
3312
  img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3324
- pool.join()
3325
- pool.terminate()
3313
+ terminate_cpu_pool(pool=pool, force=False)
3326
3314
  timer.stop_timer()
3327
3315
  stdout_success(
3328
3316
  msg="Cumulative coordinates in geometries complete",
@@ -3415,8 +3403,7 @@ class GeometryMixin(object):
3415
3403
  for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3416
3404
  if result[1] != -1:
3417
3405
  img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3418
- pool.join()
3419
- pool.terminate()
3406
+ terminate_cpu_pool(pool=pool, force=False)
3420
3407
  if fps is None:
3421
3408
  return np.cumsum(img_arr, axis=0)
3422
3409
  else:
@@ -3538,10 +3525,10 @@ class GeometryMixin(object):
3538
3525
  :rtype: Tuple[Dict[Tuple[int, int], Dict[Tuple[int, int], float]], Dict[Tuple[int, int], Dict[Tuple[int, int], int]]]
3539
3526
 
3540
3527
  :example:
3541
- >>> video_meta_data = get_video_meta_data(video_path=r"C:\troubleshooting\mitra\project_folder\videos\708_MA149_Gq_CNO_0515.mp4")
3528
+ >>> video_meta_data = get_video_meta_data(video_path=r"C:/troubleshooting/mitra/project_folder/videos/708_MA149_Gq_CNO_0515.mp4")
3542
3529
  >>> w, h = video_meta_data['width'], video_meta_data['height']
3543
3530
  >>> grid = GeometryMixin().bucket_img_into_grid_square(bucket_grid_size=(5, 5), bucket_grid_size_mm=None, img_size=(h, w), verbose=False)[0]
3544
- >>> data = read_df(file_path=r'C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location\708_MA149_Gq_CNO_0515.csv', file_type='csv')[['Nose_x', 'Nose_y']].values
3531
+ >>> data = read_df(file_path=r'C:/troubleshooting/mitra/project_folder/csv/outlier_corrected_movement_location/708_MA149_Gq_CNO_0515.csv', file_type='csv')[['Nose_x', 'Nose_y']].values
3545
3532
  >>> transition_probabilities, _ = geometry_transition_probabilities(data=data, grid=grid)
3546
3533
  """
3547
3534
 
@@ -3559,8 +3546,7 @@ class GeometryMixin(object):
3559
3546
  constants = functools.partial(GeometryMixin._compute_framewise_geometry_idx, grid=grid, verbose=verbose)
3560
3547
  for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3561
3548
  results.append(result)
3562
- pool.join();
3563
- pool.terminate();
3549
+ terminate_cpu_pool(pool=pool, force=False)
3564
3550
  del data
3565
3551
 
3566
3552
  results = np.vstack(results)[:, 1:].astype(np.int32)
@@ -4004,7 +3990,7 @@ class GeometryMixin(object):
4004
3990
  :rtype: np.ndarray
4005
3991
 
4006
3992
  :example:
4007
- >>> data_path = r"C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location\FRR_gq_Saline_0624.csv"
3993
+ >>> data_path = r"C:/troubleshooting/mitra/project_folder/csv/outlier_corrected_movement_location/FRR_gq_Saline_0624.csv"
4008
3994
  >>> animal_data = read_df(file_path=data_path, file_type='csv', usecols=['Nose_x', 'Nose_y', 'Tail_base_x', 'Tail_base_y', 'Left_side_x', 'Left_side_y', 'Right_side_x', 'Right_side_y']).values.reshape(-1, 4, 2)[0:20].astype(np.int32)
4009
3995
  >>> animal_polygons = GeometryMixin().bodyparts_to_polygon(data=animal_data)
4010
3996
  >>> GeometryMixin.geometries_to_exterior_keypoints(geometries=animal_polygons)
@@ -4174,7 +4160,7 @@ class GeometryMixin(object):
4174
4160
  :rtype: Union[None, Dict[Any, dict]]
4175
4161
 
4176
4162
  :example I:
4177
- >>> results = GeometryMixin.sleap_csv_to_geometries(data=r"C:\troubleshooting\ants\pose_data\ant.csv")
4163
+ >>> results = GeometryMixin.sleap_csv_to_geometries(data=r"C:/troubleshooting/ants/pose_data/ant.csv")
4178
4164
  >>> # Results structure: {track_id: {frame_idx: Polygon, ...}, ...}
4179
4165
 
4180
4166
  :example II
@@ -18,7 +18,7 @@ from collections import ChainMap
18
18
  import cv2
19
19
  import pandas as pd
20
20
  from numba import float64, int64, jit, njit, prange, uint8
21
- from shapely.geometry import MultiPolygon, Polygon
21
+ from shapely.geometry import Polygon
22
22
  from skimage.metrics import structural_similarity
23
23
 
24
24
  from simba.utils.checks import (check_file_exist_and_readable, check_float,
@@ -27,16 +27,14 @@ from simba.utils.checks import (check_file_exist_and_readable, check_float,
27
27
  check_int, check_str, check_valid_array,
28
28
  check_valid_boolean, check_valid_lst,
29
29
  check_valid_tuple, is_img_bw, is_img_greyscale)
30
+ from simba.utils.data import terminate_cpu_pool
30
31
  from simba.utils.enums import Defaults, Formats, GeometryEnum, Options
31
- from simba.utils.errors import (ArrayError, FFMPEGCodecGPUError,
32
- FrameRangeError, InvalidInputError,
33
- NotDirectoryError)
32
+ from simba.utils.errors import ArrayError, FrameRangeError, InvalidInputError
34
33
  from simba.utils.printing import SimbaTimer, stdout_success
35
34
  from simba.utils.read_write import (find_core_cnt,
36
35
  find_files_of_filetypes_in_directory,
37
36
  get_fn_ext, get_video_meta_data,
38
- read_frm_of_video,
39
- read_img_batch_from_video_gpu, write_df)
37
+ read_frm_of_video)
40
38
 
41
39
 
42
40
  class ImageMixin(object):
@@ -59,17 +57,16 @@ class ImageMixin(object):
59
57
  pass
60
58
 
61
59
  @staticmethod
62
- def brightness_intensity(imgs: List[np.ndarray], ignore_black: Optional[bool] = True) -> List[float]:
60
+ def brightness_intensity(imgs: Union[List[np.ndarray], np.ndarray], ignore_black: bool = True, verbose: bool = False) -> np.ndarray:
63
61
  """
64
62
  Compute the average brightness intensity within each image within a list.
65
63
 
66
64
  For example, (i) create a list of images containing a light cue ROI, (ii) compute brightness in each image, (iii) perform kmeans on brightness, and get the frames when the light cue is on vs off.
67
65
 
68
66
  .. seealso::
69
- For GPU acceleration, see :func:`simba.data_processors.cuda.image.img_stack_brightness`.
70
- For geometry based brightness, see :func:`simba.mixins.geometry_mixin.GeometryMixin.get_geometry_brightness_intensity`
67
+ For GPU acceleration, see :func:`simba.data_processors.cuda.image.img_stack_brightness`. For geometry based brightness, see :func:`simba.mixins.geometry_mixin.GeometryMixin.get_geometry_brightness_intensity`
71
68
 
72
- :param List[np.ndarray] imgs: List of images as arrays to calculate average brightness intensity within.
69
+ :param Union[List[np.ndarray], np.ndarray] imgs: List of images as arrays or 3/4d array of images to calculate average brightness intensity within.
73
70
  :param Optional[bool] ignore_black: If True, ignores black pixels. If the images are sliced non-rectangular geometric shapes created by ``slice_shapes_in_img``, then pixels that don't belong to the shape has been masked in black.
74
71
  :returns: List of floats of size len(imgs) with brightness intensities.
75
72
  :rtype: List[float]
@@ -79,14 +76,12 @@ class ImageMixin(object):
79
76
  >>> ImageMixin.brightness_intensity(imgs=[img], ignore_black=False)
80
77
  >>> [159.0]
81
78
  """
82
- results = []
83
- check_instance(source=f"{ImageMixin().brightness_intensity.__name__} imgs", instance=imgs, accepted_types=list)
84
- for cnt, img in enumerate(imgs):
85
- check_instance(
86
- source=f"{ImageMixin().brightness_intensity.__name__} img {cnt}",
87
- instance=img,
88
- accepted_types=np.ndarray,
89
- )
79
+ results, timer = [], SimbaTimer(start=True)
80
+ check_instance(source=f"{ImageMixin().brightness_intensity.__name__} imgs", instance=imgs, accepted_types=(list, np.ndarray,))
81
+ if isinstance(imgs, np.ndarray): imgs = np.array(imgs)
82
+ for img_cnt in range(imgs.shape[0]):
83
+ img = imgs[img_cnt]
84
+ check_instance(source=f"{ImageMixin().brightness_intensity.__name__} img {img_cnt}", instance=img, accepted_types=np.ndarray)
90
85
  if len(img) == 0:
91
86
  results.append(0)
92
87
  else:
@@ -94,7 +89,10 @@ class ImageMixin(object):
94
89
  results.append(np.ceil(np.average(img[img != 0])))
95
90
  else:
96
91
  results.append(np.ceil(np.average(img)))
97
- return results
92
+ b = np.array(results).astype(np.float32)
93
+ timer.stop_timer()
94
+ if verbose: print(f'Brightness computed in {b.shape[0]} images (elapsed time {timer.elapsed_time_str}s)')
95
+
98
96
 
99
97
  @staticmethod
100
98
  def gaussian_blur(img: np.ndarray, kernel_size: Optional[Tuple] = (9, 9)) -> np.ndarray:
@@ -546,8 +544,8 @@ class ImageMixin(object):
546
544
  pool.imap(constants, split_frm_idx, chunksize=1)
547
545
  ):
548
546
  results.append(result)
549
- pool.terminate()
550
- pool.join()
547
+
548
+ terminate_cpu_pool(pool=pool, force=False)
551
549
  results = dict(ChainMap(*results))
552
550
 
553
551
  max_value, max_frm = -np.inf, None
@@ -876,8 +874,7 @@ class ImageMixin(object):
876
874
  pool.imap(ImageMixin()._image_reader_helper, file_paths, chunksize=1)
877
875
  ):
878
876
  imgs.update(result)
879
- pool.join()
880
- pool.terminate()
877
+ terminate_cpu_pool(pool=pool, force=False)
881
878
  return imgs
882
879
 
883
880
  @staticmethod
@@ -1027,8 +1024,7 @@ class ImageMixin(object):
1027
1024
  for cnt, result in enumerate(pool.imap(constants, frm_lst, chunksize=1)):
1028
1025
  results.update(result)
1029
1026
 
1030
- pool.join()
1031
- pool.terminate()
1027
+ terminate_cpu_pool(pool=pool, force=False)
1032
1028
  return results
1033
1029
 
1034
1030
  @staticmethod
@@ -1509,8 +1505,8 @@ class ImageMixin(object):
1509
1505
  for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
1510
1506
  results.append(result)
1511
1507
  results = dict(ChainMap(*results))
1512
- pool.join()
1513
- pool.terminate()
1508
+
1509
+ terminate_cpu_pool(pool=pool, force=False)
1514
1510
  results = dict(sorted(results.items(), key=lambda item: int(item[0])))
1515
1511
  timer.stop_timer()
1516
1512
  stdout_success(msg="Geometry image slicing complete.", elapsed_time=timer.elapsed_time_str, source=self.__class__.__name__)
@@ -1902,7 +1898,7 @@ class ImageMixin(object):
1902
1898
  :rtype: np.ndarray
1903
1899
 
1904
1900
  :example:
1905
- >>> VIDEO_PATH = r"D:\EPM_2\EPM_1.mp4"
1901
+ >>> VIDEO_PATH = r"D:/EPM_2/EPM_1.mp4"
1906
1902
  >>> img = read_img_batch_from_video(video_path=VIDEO_PATH, greyscale=True, start_frm=0, end_frm=15, core_cnt=1)
1907
1903
  >>> imgs = np.stack(list(img.values()))
1908
1904
  >>> resized_img = resize_img_stack(imgs=imgs)