simba-uw-tf-dev 4.6.6__py3-none-any.whl → 4.6.8__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 (49) hide show
  1. simba/assets/.recent_projects.txt +1 -0
  2. simba/data_processors/blob_location_computer.py +1 -1
  3. simba/data_processors/circling_detector.py +30 -13
  4. simba/data_processors/cuda/image.py +53 -25
  5. simba/data_processors/cuda/statistics.py +57 -19
  6. simba/data_processors/cuda/timeseries.py +1 -1
  7. simba/data_processors/egocentric_aligner.py +1 -1
  8. simba/data_processors/freezing_detector.py +54 -50
  9. simba/feature_extractors/feature_subsets.py +2 -2
  10. simba/feature_extractors/mitra_feature_extractor.py +2 -2
  11. simba/feature_extractors/straub_tail_analyzer.py +4 -4
  12. simba/labelling/standard_labeller.py +1 -1
  13. simba/mixins/config_reader.py +5 -2
  14. simba/mixins/geometry_mixin.py +8 -8
  15. simba/mixins/image_mixin.py +14 -14
  16. simba/mixins/plotting_mixin.py +28 -10
  17. simba/mixins/statistics_mixin.py +39 -9
  18. simba/mixins/timeseries_features_mixin.py +1 -1
  19. simba/mixins/train_model_mixin.py +65 -27
  20. simba/model/inference_batch.py +1 -1
  21. simba/model/yolo_seg_inference.py +3 -3
  22. simba/outlier_tools/skip_outlier_correction.py +1 -1
  23. simba/plotting/gantt_creator.py +29 -10
  24. simba/plotting/gantt_creator_mp.py +50 -17
  25. simba/plotting/heat_mapper_clf_mp.py +2 -2
  26. simba/pose_importers/simba_blob_importer.py +3 -3
  27. simba/roi_tools/roi_aggregate_stats_mp.py +1 -1
  28. simba/roi_tools/roi_clf_calculator_mp.py +1 -1
  29. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo.py +3 -3
  30. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo_bbox.py +2 -2
  31. simba/ui/pop_ups/clf_plot_pop_up.py +2 -2
  32. simba/ui/pop_ups/gantt_pop_up.py +31 -6
  33. simba/ui/pop_ups/video_processing_pop_up.py +1 -1
  34. simba/utils/custom_feature_extractor.py +1 -1
  35. simba/utils/data.py +2 -2
  36. simba/utils/read_write.py +32 -18
  37. simba/utils/yolo.py +10 -1
  38. simba/video_processors/blob_tracking_executor.py +2 -2
  39. simba/video_processors/clahe_ui.py +1 -1
  40. simba/video_processors/egocentric_video_rotator.py +3 -3
  41. simba/video_processors/multi_cropper.py +1 -1
  42. simba/video_processors/video_processing.py +27 -10
  43. simba/video_processors/videos_to_frames.py +2 -2
  44. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/METADATA +3 -2
  45. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/RECORD +49 -49
  46. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/LICENSE +0 -0
  47. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/WHEEL +0 -0
  48. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/entry_points.txt +0 -0
  49. {simba_uw_tf_dev-4.6.6.dist-info → simba_uw_tf_dev-4.6.8.dist-info}/top_level.txt +0 -0
@@ -1,2 +1,3 @@
1
+ E:/troubleshooting/mitra_emergence/project_folder/project_config.ini
1
2
  C:/troubleshooting/meberled/project_folder/project_config.ini
2
3
  C:/troubleshooting/mitra/project_folder/project_config.ini
@@ -51,7 +51,7 @@ class BlobLocationComputer(object):
51
51
  :param Optional[bool] multiprocessing: If True, video background subtraction will be done using multiprocessing. Default is False.
52
52
 
53
53
  :example:
54
- >>> x = BlobLocationComputer(data_path=r"C:\troubleshooting\RAT_NOR\project_folder\videos\2022-06-20_NOB_DOT_4_downsampled_bg_subtracted.mp4", multiprocessing=True, gpu=True, batch_size=2000, save_dir=r"C:\blob_positions")
54
+ >>> x = BlobLocationComputer(data_path=r"C:/troubleshooting/RAT_NOR/project_folder/videos/2022-06-20_NOB_DOT_4_downsampled_bg_subtracted.mp4", multiprocessing=True, gpu=True, batch_size=2000, save_dir=r"C:/blob_positions")
55
55
  >>> x.run()
56
56
  """
57
57
  def __init__(self,
@@ -11,12 +11,13 @@ from simba.mixins.feature_extraction_mixin import FeatureExtractionMixin
11
11
  from simba.mixins.timeseries_features_mixin import TimeseriesFeatureMixin
12
12
  from simba.utils.checks import (
13
13
  check_all_file_names_are_represented_in_video_log, check_if_dir_exists,
14
- check_int, check_str, check_valid_dataframe)
14
+ check_str, check_valid_dataframe)
15
15
  from simba.utils.data import detect_bouts, plug_holes_shortest_bout
16
16
  from simba.utils.enums import Formats
17
17
  from simba.utils.printing import stdout_success
18
18
  from simba.utils.read_write import (find_files_of_filetypes_in_directory,
19
- get_fn_ext, read_df, read_video_info)
19
+ get_current_time, get_fn_ext, read_df,
20
+ read_video_info)
20
21
 
21
22
  CIRCLING = 'CIRCLING'
22
23
 
@@ -58,30 +59,34 @@ class CirclingDetector(ConfigReader):
58
59
  """
59
60
 
60
61
  def __init__(self,
61
- data_dir: Union[str, os.PathLike],
62
62
  config_path: Union[str, os.PathLike],
63
63
  nose_name: Optional[str] = 'nose',
64
+ data_dir: Optional[Union[str, os.PathLike]] = None,
64
65
  left_ear_name: Optional[str] = 'left_ear',
65
66
  right_ear_name: Optional[str] = 'right_ear',
66
67
  tail_base_name: Optional[str] = 'tail_base',
67
68
  center_name: Optional[str] = 'center',
68
- time_threshold: Optional[int] = 10,
69
- circular_range_threshold: Optional[int] = 320,
69
+ time_threshold: Optional[int] = 7,
70
+ circular_range_threshold: Optional[int] = 350,
71
+ shortest_bout: int = 100,
70
72
  movement_threshold: Optional[int] = 60,
71
73
  save_dir: Optional[Union[str, os.PathLike]] = None):
72
74
 
73
- check_if_dir_exists(in_dir=data_dir)
74
75
  for bp_name in [nose_name, left_ear_name, right_ear_name, tail_base_name]: check_str(name='body part name', value=bp_name, allow_blank=False)
75
- self.data_paths = find_files_of_filetypes_in_directory(directory=data_dir, extensions=['.csv'])
76
76
  ConfigReader.__init__(self, config_path=config_path, read_video_info=True, create_logger=False)
77
+ if data_dir is not None:
78
+ check_if_dir_exists(in_dir=data_dir)
79
+ else:
80
+ data_dir = self.outlier_corrected_dir
81
+ self.data_paths = find_files_of_filetypes_in_directory(directory=data_dir, extensions=['.csv'])
77
82
  self.nose_heads = [f'{nose_name}_x'.lower(), f'{nose_name}_y'.lower()]
78
83
  self.left_ear_heads = [f'{left_ear_name}_x'.lower(), f'{left_ear_name}_y'.lower()]
79
84
  self.right_ear_heads = [f'{right_ear_name}_x'.lower(), f'{right_ear_name}_y'.lower()]
80
85
  self.center_heads = [f'{center_name}_x'.lower(), f'{center_name}_y'.lower()]
81
86
  self.required_field = self.nose_heads + self.left_ear_heads + self.right_ear_heads
82
- self.save_dir = save_dir
87
+ self.save_dir, self.shortest_bout = save_dir, shortest_bout
83
88
  if self.save_dir is None:
84
- self.save_dir = os.path.join(self.logs_path, f'circling_data_{self.datetime}')
89
+ self.save_dir = os.path.join(self.logs_path, f'circling_data_{time_threshold}s_{circular_range_threshold}d_{movement_threshold}mm_{self.datetime}')
85
90
  os.makedirs(self.save_dir)
86
91
  else:
87
92
  check_if_dir_exists(in_dir=self.save_dir)
@@ -93,7 +98,7 @@ class CirclingDetector(ConfigReader):
93
98
  check_all_file_names_are_represented_in_video_log(video_info_df=self.video_info_df, data_paths=self.data_paths)
94
99
  for file_cnt, file_path in enumerate(self.data_paths):
95
100
  video_name = get_fn_ext(filepath=file_path)[1]
96
- print(f'Analyzing {video_name} ({file_cnt+1}/{len(self.data_paths)})...')
101
+ print(f'[{get_current_time()}] Analyzing circling {video_name}... (video {file_cnt+1}/{len(self.data_paths)})')
97
102
  save_file_path = os.path.join(self.save_dir, f'{video_name}.csv')
98
103
  df = read_df(file_path=file_path, file_type='csv').reset_index(drop=True)
99
104
  _, px_per_mm, fps = read_video_info(video_info_df=self.video_info_df, video_name=video_name)
@@ -115,11 +120,24 @@ class CirclingDetector(ConfigReader):
115
120
  circling_idx = np.argwhere(sliding_circular_range >= self.circular_range_threshold).astype(np.int32).flatten()
116
121
  movement_idx = np.argwhere(movement_sum >= self.movement_threshold).astype(np.int32).flatten()
117
122
  circling_idx = [x for x in movement_idx if x in circling_idx]
123
+ df[f'Probability_{CIRCLING}'] = 0
118
124
  df[CIRCLING] = 0
119
125
  df.loc[circling_idx, CIRCLING] = 1
126
+ df.loc[circling_idx, f'Probability_{CIRCLING}'] = 1
127
+ df = plug_holes_shortest_bout(data_df=df, clf_name=CIRCLING, fps=fps, shortest_bout=self.shortest_bout)
120
128
  bouts = detect_bouts(data_df=df, target_lst=[CIRCLING], fps=fps)
121
- df = plug_holes_shortest_bout(data_df=df, clf_name=CIRCLING, fps=fps, shortest_bout=100)
129
+ if len(bouts) > 0:
130
+ df[CIRCLING] = 0
131
+ circling_idx = list(bouts.apply(lambda x: list(range(int(x["Start_frame"]), int(x["End_frame"]) + 1)), 1))
132
+ circling_idx = [x for xs in circling_idx for x in xs]
133
+ df.loc[circling_idx, CIRCLING] = 1
134
+ df.loc[circling_idx, f'Probability_{CIRCLING}'] = 1
135
+ else:
136
+ df[CIRCLING] = 0
137
+ circling_idx = []
138
+
122
139
  df.to_csv(save_file_path)
140
+ #print(video_name, len(circling_idx), round(len(circling_idx) / fps, 4), df[CIRCLING].sum())
123
141
  agg_results.loc[len(agg_results)] = [video_name, len(circling_idx), round(len(circling_idx) / fps, 4), len(bouts), round((len(circling_idx) / len(df)) * 100, 4), len(df), round(len(df)/fps, 2) ]
124
142
 
125
143
  agg_results.to_csv(agg_results_path)
@@ -127,7 +145,6 @@ class CirclingDetector(ConfigReader):
127
145
 
128
146
  #
129
147
  #
130
- # detector = CirclingDetector(data_dir=r'C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location',
131
- # config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini")
148
+ # detector = CirclingDetector(config_path=r"E:\troubleshooting\mitra_emergence\project_folder\project_config.ini")
132
149
  # detector.run()
133
150
 
@@ -331,10 +331,22 @@ def _digital(data, results):
331
331
 
332
332
  def img_stack_brightness(x: np.ndarray,
333
333
  method: Optional[Literal['photometric', 'digital']] = 'digital',
334
- ignore_black: Optional[bool] = True) -> np.ndarray:
334
+ ignore_black: bool = True,
335
+ verbose: bool = False,
336
+ batch_size: int = 2500) -> np.ndarray:
335
337
  """
336
338
  Calculate the average brightness of a stack of images using a specified method.
337
339
 
340
+ Useful for analyzing light cues or brightness changes over time. For example, compute brightness in images containing a light cue ROI, then perform clustering (e.g., k-means) on brightness values to identify frames when the light cue is on vs off.
341
+
342
+ .. csv-table::
343
+ :header: EXPECTED RUNTIMES
344
+ :file: ../../../docs/tables/img_stack_brightness_gpu.csv
345
+ :widths: 10, 45, 45
346
+ :align: center
347
+ :class: simba-table
348
+ :header-rows: 1
349
+
338
350
 
339
351
  - **Photometric Method**: The brightness is calculated using the formula:
340
352
 
@@ -346,7 +358,7 @@ def img_stack_brightness(x: np.ndarray,
346
358
  .. math::
347
359
  \text{brightness} = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B
348
360
 
349
- .. selalso::
361
+ .. seealso::
350
362
  For CPU function see :func:`~simba.mixins.image_mixin.ImageMixin.brightness_intensity`.
351
363
 
352
364
  :param np.ndarray x: A 4D array of images with dimensions (N, H, W, C), where N is the number of images, H and W are the height and width, and C is the number of channels (RGB).
@@ -363,27 +375,42 @@ def img_stack_brightness(x: np.ndarray,
363
375
 
364
376
  check_instance(source=img_stack_brightness.__name__, instance=x, accepted_types=(np.ndarray,))
365
377
  check_if_valid_img(data=x[0], source=img_stack_brightness.__name__)
366
- x = np.ascontiguousarray(x).astype(np.uint8)
378
+ check_int(name=f'{img_stack_brightness.__name__} batch_size', value=batch_size, allow_zero=False, allow_negative=False, raise_error=True)
379
+ x, timer = np.ascontiguousarray(x).astype(np.uint8), SimbaTimer(start=True)
380
+ results = []
367
381
  if x.ndim == 4:
368
- grid_x = (x.shape[1] + 16 - 1) // 16
369
- grid_y = (x.shape[2] + 16 - 1) // 16
370
- grid_z = x.shape[0]
371
- threads_per_block = (16, 16, 1)
372
- blocks_per_grid = (grid_y, grid_x, grid_z)
373
- x_dev = cuda.to_device(x)
374
- results = cuda.device_array((x.shape[0], x.shape[1], x.shape[2]), dtype=np.uint8)
375
- if method == PHOTOMETRIC:
376
- _photometric[blocks_per_grid, threads_per_block](x_dev, results)
382
+ batch_results_dev = cuda.device_array((batch_size, x.shape[1], x.shape[2]), dtype=np.uint8)
383
+ for batch_cnt, l in enumerate(range(0, x.shape[0], batch_size)):
384
+ r = l + batch_size
385
+ batch_x = x[l:r]
386
+ if batch_x.ndim == 4:
387
+ grid_x = (batch_x.shape[1] + 16 - 1) // 16
388
+ grid_y = (batch_x.shape[2] + 16 - 1) // 16
389
+ grid_z = batch_x.shape[0]
390
+ threads_per_block = (16, 16, 1)
391
+ blocks_per_grid = (grid_y, grid_x, grid_z)
392
+ x_dev = cuda.to_device(batch_x)
393
+ if method == PHOTOMETRIC:
394
+ _photometric[blocks_per_grid, threads_per_block](x_dev, batch_results_dev)
395
+ else:
396
+ _digital[blocks_per_grid, threads_per_block](x_dev, batch_results_dev)
397
+ batch_results_host = batch_results_dev.copy_to_host()[:batch_x.shape[0]]
398
+ batch_results_cp = cp.asarray(batch_results_host)
399
+ if ignore_black:
400
+ mask = batch_results_cp != 0
401
+ batch_results_cp = cp.where(mask, batch_results_cp, cp.nan)
402
+ batch_results = cp.nanmean(batch_results_cp, axis=(1, 2))
403
+ batch_results = cp.where(cp.isnan(batch_results), 0, batch_results)
404
+ batch_results = batch_results.get()
405
+ else:
406
+ batch_results = cp.mean(batch_results_cp, axis=(1, 2)).get()
377
407
  else:
378
- _digital[blocks_per_grid, threads_per_block](x_dev, results)
379
- results = results.copy_to_host()
380
- if ignore_black:
381
- masked_array = np.ma.masked_equal(results, 0)
382
- results = np.mean(masked_array, axis=(1, 2)).filled(0)
383
- else:
384
- results = deepcopy(x)
385
- results = np.mean(results, axis=(1, 2))
386
-
408
+ batch_results = deepcopy(x)
409
+ batch_results = np.mean(batch_results, axis=(1, 2))
410
+ results.append(batch_results)
411
+ timer.stop_timer()
412
+ results = np.concatenate(results) if len(results) > 0 else np.array([])
413
+ if verbose: print(f'Brightness computed in {results.shape[0]} images (elapsed time {timer.elapsed_time_str}s)')
387
414
  return results
388
415
 
389
416
 
@@ -1602,10 +1629,11 @@ def pose_plotter(data: Union[str, os.PathLike, np.ndarray],
1602
1629
  # SAVE_PATH = "/mnt/c/troubleshooting/mitra/project_folder/frames/output/pose_ex/test.mp4"
1603
1630
  #
1604
1631
  #
1605
- DATA_PATH = "/mnt/d/troubleshooting/mitra/project_folder/csv/outlier_corrected_movement_location/592_MA147_CNO1_0515.csv"
1606
- VIDEO_PATH = "/mnt/d/troubleshooting/mitra/project_folder/videos/592_MA147_CNO1_0515.mp4"
1607
- SAVE_PATH = "/mnt/d/troubleshooting/mitra/project_folder/videos/test_cuda.mp4"
1608
- pose_plotter(data=DATA_PATH, video_path=VIDEO_PATH, save_path=SAVE_PATH, circle_size=10, batch_size=100)
1632
+ if __name__ == "__main__":
1633
+ DATA_PATH = "/mnt/d/troubleshooting/mitra/project_folder/csv/outlier_corrected_movement_location/592_MA147_CNO1_0515.csv"
1634
+ VIDEO_PATH = "/mnt/d/troubleshooting/mitra/project_folder/videos/592_MA147_CNO1_0515.mp4"
1635
+ SAVE_PATH = "/mnt/d/troubleshooting/mitra/project_folder/videos/test_cuda.mp4"
1636
+ pose_plotter(data=DATA_PATH, video_path=VIDEO_PATH, save_path=SAVE_PATH, circle_size=10, batch_size=100)
1609
1637
 
1610
1638
 
1611
1639
 
@@ -3,7 +3,7 @@ __author__ = "Simon Nilsson; sronilsson@gmail.com"
3
3
 
4
4
  import math
5
5
  from itertools import combinations
6
- from typing import Optional, Tuple
6
+ from typing import Optional, Tuple, Union
7
7
 
8
8
  from simba.utils.printing import SimbaTimer
9
9
 
@@ -21,14 +21,17 @@ from simba.utils.warnings import GPUToolsWarning
21
21
 
22
22
  try:
23
23
  import cupy as cp
24
- from cuml.metrics import kl_divergence as kl_divergence_gpu
25
- #from cuml.metrics.cluster.adjusted_rand_index import adjusted_rand_score
26
- #from cuml.metrics.cluster.silhouette_score import cython_silhouette_score
27
24
  from cupyx.scipy.spatial.distance import cdist
28
25
  except Exception as e:
29
26
  GPUToolsWarning(msg=f'GPU tools not detected, reverting to CPU: {e.args}')
30
27
  import numpy as cp
31
28
  from scipy.spatial.distance import cdist
29
+ try:
30
+ from cuml.metrics import kl_divergence as kl_divergence_gpu
31
+ from cuml.metrics.cluster.adjusted_rand_index import adjusted_rand_score
32
+ from cuml.metrics.cluster.silhouette_score import cython_silhouette_score
33
+ except Exception as e:
34
+ GPUToolsWarning(msg=f'GPU tools not detected, reverting to CPU: {e.args}')
32
35
  from scipy.stats import entropy as kl_divergence_gpu
33
36
  from sklearn.metrics import adjusted_rand_score
34
37
  from sklearn.metrics import silhouette_score as cython_silhouette_score
@@ -40,8 +43,8 @@ except:
40
43
 
41
44
  from simba.data_processors.cuda.utils import _cuda_are_rows_equal
42
45
  from simba.mixins.statistics_mixin import Statistics
43
- from simba.utils.checks import (check_int, check_str, check_valid_array,
44
- check_valid_tuple)
46
+ from simba.utils.checks import (check_float, check_int, check_str,
47
+ check_valid_array, check_valid_tuple)
45
48
  from simba.utils.data import bucket_data
46
49
  from simba.utils.enums import Formats
47
50
 
@@ -381,9 +384,10 @@ def sliding_min(x: np.ndarray, time_window: float, sample_rate: int) -> np.ndarr
381
384
 
382
385
  def sliding_spearmans_rank(x: np.ndarray,
383
386
  y: np.ndarray,
384
- time_window: float,
385
- sample_rate: int,
386
- batch_size: Optional[int] = int(1.6e+7)) -> np.ndarray:
387
+ time_window: Union[float, int],
388
+ sample_rate: Union[float, int],
389
+ batch_size: Optional[int] = int(1.6e+7),
390
+ verbose: bool = False) -> np.ndarray:
387
391
  """
388
392
  Computes the Spearman's rank correlation coefficient between two 1D arrays `x` and `y`
389
393
  over sliding windows of size `time_window * sample_rate`. The computation is performed
@@ -414,7 +418,13 @@ def sliding_spearmans_rank(x: np.ndarray,
414
418
  >>> sliding_spearmans_rank(x, y, time_window=0.5, sample_rate=2)
415
419
  """
416
420
 
417
- window_size = int(np.ceil(time_window * sample_rate))
421
+ timer = SimbaTimer(start=True)
422
+ check_valid_array(data=x, source=f'{sliding_spearmans_rank.__name__} x', accepted_ndims=(1,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
423
+ check_valid_array(data=y, source=f'{sliding_spearmans_rank.__name__} y', accepted_ndims=(1,), accepted_axis_0_shape=(x.shape[0],), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
424
+ check_float(name=f'{sliding_spearmans_rank.__name__} time_window', value=time_window, allow_zero=False, allow_negative=False, raise_error=True)
425
+ check_float(name=f'{sliding_spearmans_rank.__name__} sample_rate', value=sample_rate, allow_zero=False, allow_negative=False, raise_error=True)
426
+ check_int(name=f'{sliding_spearmans_rank.__name__} batch_size', value=batch_size, allow_zero=False, allow_negative=False, raise_error=True)
427
+ window_size = np.int32(np.ceil(time_window * sample_rate))
418
428
  n = x.shape[0]
419
429
  results = cp.full(n, -1, dtype=cp.float32)
420
430
 
@@ -434,7 +444,11 @@ def sliding_spearmans_rank(x: np.ndarray,
434
444
 
435
445
  results[left + window_size - 1:right] = s
436
446
 
437
- return cp.asnumpy(results)
447
+ r = cp.asnumpy(results)
448
+ timer.stop_timer()
449
+ if verbose: print(f'Sliding Spearmans rank for {x.shape[0]} observations computed (elapsed time: {timer.elapsed_time_str}s)')
450
+ return r
451
+
438
452
 
439
453
 
440
454
 
@@ -539,6 +553,12 @@ def euclidean_distance_to_static_point(data: np.ndarray,
539
553
  """
540
554
  Computes the Euclidean distance between each point in a given 2D array `data` and a static point using GPU acceleration.
541
555
 
556
+ .. seealso::
557
+ For CPU-based distance to static point (ROI center), see :func:`simba.mixins.feature_extraction_mixin.FeatureExtractionMixin.framewise_euclidean_distance_roi`
558
+ For CPU-based framewise Euclidean distance, see :func:`simba.mixins.feature_extraction_mixin.FeatureExtractionMixin.framewise_euclidean_distance`
559
+ For GPU CuPy solution for distance between two sets of points, see :func:`simba.data_processors.cuda.statistics.get_euclidean_distance_cupy`
560
+ For GPU numba CUDA solution for distance between two sets of points, see :func:`simba.data_processors.cuda.statistics.get_euclidean_distance_cuda`
561
+
542
562
  :param data: A 2D array of shape (N, 2), where N is the number of points, and each point is represented by its (x, y) coordinates. The array can represent pixel coordinates.
543
563
  :param point: A tuple of two integers representing the static point (x, y) in the same space as `data`.
544
564
  :param pixels_per_millimeter: A scaling factor that indicates how many pixels correspond to one millimeter. Defaults to 1 if no scaling is necessary.
@@ -790,13 +810,31 @@ def xie_beni(x: np.ndarray, y: np.ndarray) -> float:
790
810
  return xb
791
811
 
792
812
 
793
- def i_index(x: np.ndarray, y: np.ndarray):
813
+ def i_index(x: np.ndarray, y: np.ndarray, verbose: bool = False) -> float:
794
814
  """
795
815
  Calculate the I-Index for evaluating clustering quality.
796
816
 
797
817
  The I-Index is a metric that measures the compactness and separation of clusters.
798
818
  A higher I-Index indicates better clustering with compact and well-separated clusters.
799
819
 
820
+ .. csv-table::
821
+ :header: EXPECTED RUNTIMES
822
+ :file: ../../../docs/tables/i_index_cuda.csv
823
+ :widths: 10, 45, 45
824
+ :align: center
825
+ :header-rows: 1
826
+
827
+ The I-Index is calculated as:
828
+
829
+ .. math::
830
+ I = \frac{SST}{k \times SWC}
831
+
832
+ where:
833
+
834
+ - :math:`SST = \sum_{i=1}^{n} \|x_i - \mu\|^2` is the total sum of squares (sum of squared distances from all points to the global centroid)
835
+ - :math:`k` is the number of clusters
836
+ - :math:`SWC = \sum_{c=1}^{k} \sum_{i \in c} \|x_i - \mu_c\|^2` is the within-cluster sum of squares (sum of squared distances from points to their cluster centroids)
837
+
800
838
  .. seealso::
801
839
  To compute Xie-Beni on the CPU, use :func:`~simba.mixins.statistics_mixin.Statistics.i_index`
802
840
 
@@ -807,17 +845,16 @@ def i_index(x: np.ndarray, y: np.ndarray):
807
845
 
808
846
  :references:
809
847
  .. [1] Zhao, Q., Xu, M., Fränti, P. (2009). Sum-of-Squares Based Cluster Validity Index and Significance Analysis.
810
- In: Kolehmainen, M., Toivanen, P., Beliczynski, B. (eds) Adaptive and Natural Computing Algorithms. ICANNGA 2009.
811
- Lecture Notes in Computer Science, vol 5495. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-04921-7_32
848
+ In: Kolehmainen, M., Toivanen, P., Beliczynski, B. (eds) Adaptive and Natural Computing Algorithms. ICANNGA 2009. Lecture Notes in Computer Science, vol 5495. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-04921-7_32
812
849
 
813
850
  :example:
814
851
  >>> X, y = make_blobs(n_samples=5000, centers=20, n_features=3, random_state=0, cluster_std=0.1)
815
852
  >>> i_index(x=X, y=y)
816
853
  """
854
+ timer = SimbaTimer(start=True)
817
855
  check_valid_array(data=x, accepted_ndims=(2,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
818
- check_valid_array(data=y, accepted_ndims=(1,), accepted_dtypes=Formats.NUMERIC_DTYPES.value,
819
- accepted_axis_0_shape=[x.shape[0], ])
820
- _ = get_unique_values_in_iterable(data=y, name=i_index.__name__, min=2)
856
+ check_valid_array(data=y, accepted_ndims=(1,), accepted_dtypes=Formats.NUMERIC_DTYPES.value, accepted_axis_0_shape=[x.shape[0], ])
857
+ cluster_cnt = get_unique_values_in_iterable(data=y, name=i_index.__name__, min=2)
821
858
  x, y = cp.array(x), cp.array(y)
822
859
  unique_y = cp.unique(y)
823
860
  n_y = unique_y.shape[0]
@@ -831,10 +868,11 @@ def i_index(x: np.ndarray, y: np.ndarray):
831
868
  swc += cp.sum(cp.linalg.norm(cluster_obs - cluster_centroid, axis=1) ** 2)
832
869
 
833
870
  i_idx = sst / (n_y * swc)
834
-
871
+ i_idx = np.float32(i_idx.get()) if hasattr(i_idx, 'get') else np.float32(i_idx)
872
+ timer.stop_timer()
873
+ if verbose: print(f'I-index for {x.shape[0]} observations in {cluster_cnt} clusters computed (elapsed time: {timer.elapsed_time_str}s)')
835
874
  return i_idx
836
875
 
837
-
838
876
  def kullback_leibler_divergence_gpu(x: np.ndarray,
839
877
  y: np.ndarray,
840
878
  fill_value: int = 1,
@@ -307,7 +307,7 @@ def sliding_hjort_parameters_gpu(data: np.ndarray, window_sizes: np.ndarray, sam
307
307
  """
308
308
  Compute Hjorth parameters over sliding windows on the GPU.
309
309
 
310
- .. seelalso::
310
+ .. seealso::
311
311
  For CPU implementation, see :`simba.mixins.timeseries_features_mixin.TimeseriesFeatureMixin.hjort_parameters`
312
312
 
313
313
  :param np.ndarray data: 1D numeric array of signal data.
@@ -52,7 +52,7 @@ class EgocentricalAligner():
52
52
  :param Optional[int] core_cnt: Number of CPU cores to use for video rotation; `-1` uses all available cores.
53
53
 
54
54
  :example:
55
- >>> aligner = EgocentricalAligner(rotate_video=True, anchor_1='tail_base', anchor_2='nose', data_dir=r"/data_dir", videos_dir=r'/videos_dir', save_dir=r"/save_dir", video_info=r"C:\troubleshooting\mitra\project_folder\logs\video_info.csv", direction=0, anchor_location=(250, 250), fill_clr=(0, 0, 0))
55
+ >>> aligner = EgocentricalAligner(rotate_video=True, anchor_1='tail_base', anchor_2='nose', data_dir=r"/data_dir", videos_dir=r'/videos_dir', save_dir=r"/save_dir", video_info=r"C:/troubleshooting/mitra/project_folder/logs/video_info.csv", direction=0, anchor_location=(250, 250), fill_clr=(0, 0, 0))
56
56
  >>> aligner.run()
57
57
  """
58
58
 
@@ -1,10 +1,8 @@
1
1
  import os
2
2
  from typing import Optional, Union
3
-
4
3
  import numpy as np
5
4
  import pandas as pd
6
5
  from numba import typed
7
-
8
6
  from simba.mixins.config_reader import ConfigReader
9
7
  from simba.mixins.feature_extraction_mixin import FeatureExtractionMixin
10
8
  from simba.mixins.timeseries_features_mixin import TimeseriesFeatureMixin
@@ -14,65 +12,73 @@ from simba.utils.checks import (
14
12
  from simba.utils.data import detect_bouts, plug_holes_shortest_bout
15
13
  from simba.utils.enums import Formats
16
14
  from simba.utils.printing import stdout_success
17
- from simba.utils.read_write import (find_files_of_filetypes_in_directory,
18
- get_fn_ext, read_df, read_video_info)
15
+ from simba.utils.read_write import (find_files_of_filetypes_in_directory, get_fn_ext, read_df, read_video_info, get_current_time)
19
16
 
20
17
  NAPE_X, NAPE_Y = 'nape_x', 'nape_y'
21
18
  FREEZING = 'FREEZING'
22
19
 
23
20
  class FreezingDetector(ConfigReader):
24
-
25
21
  """
26
- Detect freezing behavior using heuristic rules.
27
-
22
+ Detect freezing behavior using heuristic rules based on movement velocity thresholds.
23
+ Analyzes pose-estimation data to detect freezing episodes by computing the mean velocity
24
+ of key body parts (nape, nose, and tail-base) and identifying periods where movement falls below
25
+ a specified threshold for a minimum duration.
28
26
  .. important::
29
-
30
27
  Freezing is detected as `present` when **the velocity (computed from the mean movement of the nape, nose, and tail-base body-parts) falls below
31
- the movement threshold for the duration (and longer) of the specied time-window**.
32
-
28
+ the movement threshold for the duration (and longer) of the specified time-window**.
33
29
  Freezing is detected as `absent` when not present.
34
-
35
30
  .. note::
36
-
37
- We pass the names of the left and right ears, as the method will use body-parts to compute the `nape` location of the animal.
38
-
39
- :param Union[str, os.PathLike] data_dir: Path to directory containing pose-estimated body-part data in CSV format.
40
- :param Union[str, os.PathLike] config_path: Path to SimBA project config file.
41
- :param Optional[str] nose_name: The name of the pose-estimated nose body-part. Defaults to 'nose'.
42
- :param Optional[str] left_ear_name: The name of the pose-estimated left ear body-part. Defaults to 'left_ear'.
43
- :param Optional[str] right_ear_name: The name of the pose-estimated right ear body-part. Defaults to 'right_ear'.
44
- :param Optional[str] tail_base_name: The name of the pose-estimated tail base body-part. Defaults to 'tail_base'.
45
- :param Optional[int] time_window: The time window in preceding seconds in which to evaluate freezing. Default: 3.
46
- :param Optional[int] movement_threshold: A movement threshold in millimeters per second. Defaults to 5.
47
- :param Optional[Union[str, os.PathLike]] save_dir: Directory where to store the results. If None, then results are stored in the ``logs`` directory of the SimBA project.
48
-
31
+ The method uses the left and right ear body-parts to compute the `nape` location of the animal
32
+ as the midpoint between the ears. The nape, nose, and tail-base movements are averaged to compute
33
+ overall animal movement velocity.
34
+ :param Union[str, os.PathLike] data_dir: Path to directory containing pose-estimated body-part data in CSV format. Each CSV file should contain pose estimation data for one video.
35
+ :param Union[str, os.PathLike] config_path: Path to SimBA project config file (`.ini` format) containing project settings and video information.
36
+ :param Optional[str] nose_name: The name of the pose-estimated nose body-part column (without _x/_y suffix). Defaults to 'nose'.
37
+ :param Optional[str] left_ear_name: The name of the pose-estimated left ear body-part column (without _x/_y suffix). Defaults to 'Left_ear'.
38
+ :param Optional[str] right_ear_name: The name of the pose-estimated right ear body-part column (without _x/_y suffix). Defaults to 'right_ear'.
39
+ :param Optional[str] tail_base_name: The name of the pose-estimated tail base body-part column (without _x/_y suffix). Defaults to 'tail_base'.
40
+ :param Optional[int] time_window: The minimum time window in seconds that movement must be below the threshold to be considered freezing. Only freezing bouts lasting at least this duration are retained. Defaults to 3.
41
+ :param Optional[int] movement_threshold: Movement threshold in millimeters per second. Frames with mean velocity below this threshold are considered potential freezing. Defaults to 5.
42
+ :param Optional[int] shortest_bout: Minimum duration in milliseconds for a freezing bout to be considered valid. Shorter bouts are filtered out. Defaults to 100.
43
+ :param Optional[Union[str, os.PathLike]] save_dir: Directory where to store the results. If None, then results are stored in a timestamped subdirectory within the ``logs`` directory of the SimBA project.
44
+ :returns: None. Results are saved to CSV files in the specified save directory:
45
+ - Individual video results: One CSV file per video with freezing annotations added as a 'FREEZING' column (1 = freezing, 0 = not freezing)
46
+ - Aggregate results: `aggregate_freezing_results.csv` containing summary statistics for all videos
49
47
  :example:
50
- >>> FreezingDetector(data_dir=r'D:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location', config_path=r"D:\troubleshooting\mitra\project_folder\project_config.ini")
51
-
48
+ >>> FreezingDetector(
49
+ ... data_dir=r'D:\\troubleshooting\\mitra\\project_folder\\csv\\outlier_corrected_movement_location',
50
+ ... config_path=r"D:\\troubleshooting\\mitra\\project_folder\\project_config.ini",
51
+ ... time_window=3,
52
+ ... movement_threshold=5,
53
+ ... shortest_bout=100
54
+ ... ).run()
52
55
  References
53
56
  ----------
57
+ ..
54
58
  .. [1] Sabnis et al., Visual detection of seizures in mice using supervised machine learning, `biorxiv`, doi: https://doi.org/10.1101/2024.05.29.596520.
55
59
  .. [2] Lopez et al., Region-specific Nucleus Accumbens Dopamine Signals Encode Distinct Aspects of Avoidance Learning, `biorxiv`, doi: https://doi.org/10.1101/2024.08.28.610149
56
- .. [3] Lopez, Gabriela C., Louis D. Van Camp, Ryan F. Kovaleski, et al. Region-Specific Nucleus Accumbens Dopamine Signals Encode Distinct Aspects of Avoidance Learning.” `Cell Biology`, Volume 35, Issue 10p2433-2443.e5May 19, 2025. DOI: 10.1016/j.cub.2025.04.006
60
+ .. [3] Lopez, Gabriela C., Louis D. Van Camp, Ryan F. Kovaleski, et al. "Region-Specific Nucleus Accumbens Dopamine Signals Encode Distinct Aspects of Avoidance Learning." `Cell Biology`, Volume 35, Issue 10p2433-2443.e5May 19, 2025. DOI: 10.1016/j.cub.2025.04.006
57
61
  .. [4] Lazaro et al., Brainwide Genetic Capture for Conscious State Transitions, `biorxiv`, doi: https://doi.org/10.1101/2025.03.28.646066
62
+ .. [5] Sabnis et al., Visual detection of seizures in mice using supervised machine learning, 2025, Cell Reports Methods 5, 101242 December 15, 2025.
58
63
  """
59
-
60
64
  def __init__(self,
61
- data_dir: Union[str, os.PathLike],
62
65
  config_path: Union[str, os.PathLike],
63
- nose_name: Optional[str] = 'nose',
64
- left_ear_name: Optional[str] = 'Left_ear',
65
- right_ear_name: Optional[str] = 'right_ear',
66
- tail_base_name: Optional[str] = 'tail_base',
67
- time_window: Optional[int] = 3,
68
- movement_threshold: Optional[int] = 5,
69
- shortest_bout: Optional[int] = 100,
66
+ nose_name: str = 'nose',
67
+ left_ear_name: str = 'Left_ear',
68
+ right_ear_name: str = 'right_ear',
69
+ tail_base_name: str = 'tail_base',
70
+ data_dir: Optional[Union[str, os.PathLike]] = None,
71
+ time_window: int = 4,
72
+ movement_threshold: int = 5,
73
+ shortest_bout: int = 100,
70
74
  save_dir: Optional[Union[str, os.PathLike]] = None):
71
-
72
- check_if_dir_exists(in_dir=data_dir)
73
75
  for bp_name in [nose_name, left_ear_name, right_ear_name, tail_base_name]: check_str(name='body part name', value=bp_name, allow_blank=False)
74
- self.data_paths = find_files_of_filetypes_in_directory(directory=data_dir, extensions=['.csv'])
75
76
  ConfigReader.__init__(self, config_path=config_path, read_video_info=True, create_logger=False)
77
+ if data_dir is not None:
78
+ check_if_dir_exists(in_dir=data_dir)
79
+ else:
80
+ data_dir = self.outlier_corrected_dir
81
+ self.data_paths = find_files_of_filetypes_in_directory(directory=data_dir, extensions=['.csv'])
76
82
  self.nose_heads = [f'{nose_name}_x'.lower(), f'{nose_name}_y'.lower()]
77
83
  self.left_ear_heads = [f'{left_ear_name}_x'.lower(), f'{left_ear_name}_y'.lower()]
78
84
  self.right_ear_heads = [f'{right_ear_name}_x'.lower(), f'{right_ear_name}_y'.lower()]
@@ -82,21 +88,19 @@ class FreezingDetector(ConfigReader):
82
88
  check_int(name='movement_threshold', value=movement_threshold, min_value=1)
83
89
  self.save_dir = save_dir
84
90
  if self.save_dir is None:
85
- self.save_dir = os.path.join(self.logs_path, f'freezing_data_time_{time_window}s_{self.datetime}')
91
+ self.save_dir = os.path.join(self.logs_path, f'freezing_data_time_{time_window}s_{movement_threshold}mm_{self.datetime}')
86
92
  os.makedirs(self.save_dir)
87
93
  else:
88
94
  check_if_dir_exists(in_dir=self.save_dir)
89
95
  self.time_window, self.movement_threshold = time_window, movement_threshold
90
96
  self.movement_threshold, self.shortest_bout = movement_threshold, shortest_bout
91
- self.run()
92
-
93
97
  def run(self):
94
98
  agg_results = pd.DataFrame(columns=['VIDEO', 'FREEZING FRAMES', 'FREEZING TIME (S)', 'FREEZING BOUT COUNTS', 'FREEZING PCT OF SESSION', 'VIDEO TOTAL FRAMES', 'VIDEO TOTAL TIME (S)'])
95
- agg_results_path = os.path.join(self.save_dir, 'aggregate_freezing_results.csv')
99
+ agg_results_path = os.path.join(self.save_dir, f'aggregate_freezing_results_{self.datetime}.csv')
96
100
  check_all_file_names_are_represented_in_video_log(video_info_df=self.video_info_df, data_paths=self.data_paths)
97
101
  for file_cnt, file_path in enumerate(self.data_paths):
98
102
  video_name = get_fn_ext(filepath=file_path)[1]
99
- print(f'Analyzing {video_name}...({file_cnt+1}/{len(self.data_paths)})')
103
+ print(f'[{get_current_time()}] Analyzing freezing {video_name}...(video {file_cnt+1}/{len(self.data_paths)})')
100
104
  save_file_path = os.path.join(self.save_dir, f'{video_name}.csv')
101
105
  df = read_df(file_path=file_path, file_type='csv').reset_index(drop=True)
102
106
  _, px_per_mm, fps = read_video_info(vid_info_df=self.video_info_df, video_name=video_name)
@@ -118,23 +122,23 @@ class FreezingDetector(ConfigReader):
118
122
  mean_movement = np.mean(movement, axis=1)
119
123
  mm_s = TimeseriesFeatureMixin.sliding_descriptive_statistics(data=mean_movement.astype(np.float32), window_sizes=np.array([1], dtype=np.float64), sample_rate=int(fps), statistics=typed.List(["sum"]))[0].flatten()
120
124
  freezing_idx = np.argwhere(mm_s <= self.movement_threshold).astype(np.int32).flatten()
125
+ df[f'Probability_{FREEZING}'] = 0
121
126
  df[FREEZING] = 0
122
127
  df.loc[freezing_idx, FREEZING] = 1
123
128
  df = plug_holes_shortest_bout(data_df=df, clf_name=FREEZING, fps=fps, shortest_bout=self.shortest_bout)
124
129
  bouts = detect_bouts(data_df=df, target_lst=[FREEZING], fps=fps)
125
130
  bouts = bouts[bouts['Bout_time'] >= self.time_window]
126
131
  if len(bouts) > 0:
132
+ df[FREEZING] = 0
127
133
  freezing_idx = list(bouts.apply(lambda x: list(range(int(x["Start_frame"]), int(x["End_frame"]) + 1)), 1))
128
134
  freezing_idx = [x for xs in freezing_idx for x in xs]
129
135
  df.loc[freezing_idx, FREEZING] = 1
136
+ df.loc[freezing_idx, f'Probability_{FREEZING}'] = 1
130
137
  else:
138
+ df[FREEZING] = 0
131
139
  freezing_idx = []
132
140
  df.to_csv(save_file_path)
141
+ print(video_name, len(freezing_idx), round(len(freezing_idx) / fps, 4), df[FREEZING].sum())
133
142
  agg_results.loc[len(agg_results)] = [video_name, len(freezing_idx), round(len(freezing_idx) / fps, 4), len(bouts), round((len(freezing_idx) / len(df)) * 100, 4), len(df), round(len(df)/fps, 2) ]
134
-
135
143
  agg_results.to_csv(agg_results_path)
136
- stdout_success(msg=f'Results saved in {self.save_dir} directory.')
137
-
138
- #
139
- # FreezingDetector(data_dir=r'C:\troubleshooting\mitra\project_folder\csv\outlier_corrected_movement_location',
140
- # config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini")
144
+ self.timer.stop_timer(); stdout_success(msg=f'Results saved in {self.save_dir} directory.', elapsed_time=self.timer.elapsed_time_str)
@@ -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
 
@@ -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