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
@@ -3,7 +3,9 @@ __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
+
8
+ from simba.utils.printing import SimbaTimer
7
9
 
8
10
  try:
9
11
  from typing import Literal
@@ -19,14 +21,17 @@ from simba.utils.warnings import GPUToolsWarning
19
21
 
20
22
  try:
21
23
  import cupy as cp
22
- from cuml.metrics import kl_divergence as kl_divergence_gpu
23
- from cuml.metrics.cluster.adjusted_rand_index import adjusted_rand_score
24
- from cuml.metrics.cluster.silhouette_score import cython_silhouette_score
25
24
  from cupyx.scipy.spatial.distance import cdist
26
- except:
27
- GPUToolsWarning(msg='GPU tools not detected, reverting to CPU')
25
+ except Exception as e:
26
+ GPUToolsWarning(msg=f'GPU tools not detected, reverting to CPU: {e.args}')
28
27
  import numpy as cp
29
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}')
30
35
  from scipy.stats import entropy as kl_divergence_gpu
31
36
  from sklearn.metrics import adjusted_rand_score
32
37
  from sklearn.metrics import silhouette_score as cython_silhouette_score
@@ -38,8 +43,8 @@ except:
38
43
 
39
44
  from simba.data_processors.cuda.utils import _cuda_are_rows_equal
40
45
  from simba.mixins.statistics_mixin import Statistics
41
- from simba.utils.checks import (check_int, check_str, check_valid_array,
42
- check_valid_tuple)
46
+ from simba.utils.checks import (check_float, check_int, check_str,
47
+ check_valid_array, check_valid_tuple)
43
48
  from simba.utils.data import bucket_data
44
49
  from simba.utils.enums import Formats
45
50
 
@@ -227,7 +232,6 @@ def get_euclidean_distance_cupy(x: np.ndarray,
227
232
  using CuPy for GPU acceleration. The computation is performed in batches to handle large
228
233
  datasets efficiently.
229
234
 
230
-
231
235
  .. seealso::
232
236
  For CPU function see :func:`~simba.mixins.feature_extraction_mixin.FeatureExtractionMixin.framewise_euclidean_distance`.
233
237
  For CUDA JIT function see :func:`~simba.data_processors.cuda.statistics.get_euclidean_distance_cuda`.
@@ -380,9 +384,10 @@ def sliding_min(x: np.ndarray, time_window: float, sample_rate: int) -> np.ndarr
380
384
 
381
385
  def sliding_spearmans_rank(x: np.ndarray,
382
386
  y: np.ndarray,
383
- time_window: float,
384
- sample_rate: int,
385
- 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:
386
391
  """
387
392
  Computes the Spearman's rank correlation coefficient between two 1D arrays `x` and `y`
388
393
  over sliding windows of size `time_window * sample_rate`. The computation is performed
@@ -413,7 +418,13 @@ def sliding_spearmans_rank(x: np.ndarray,
413
418
  >>> sliding_spearmans_rank(x, y, time_window=0.5, sample_rate=2)
414
419
  """
415
420
 
416
- 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))
417
428
  n = x.shape[0]
418
429
  results = cp.full(n, -1, dtype=cp.float32)
419
430
 
@@ -433,7 +444,11 @@ def sliding_spearmans_rank(x: np.ndarray,
433
444
 
434
445
  results[left + window_size - 1:right] = s
435
446
 
436
- 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
+
437
452
 
438
453
 
439
454
 
@@ -538,6 +553,12 @@ def euclidean_distance_to_static_point(data: np.ndarray,
538
553
  """
539
554
  Computes the Euclidean distance between each point in a given 2D array `data` and a static point using GPU acceleration.
540
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
+
541
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.
542
563
  :param point: A tuple of two integers representing the static point (x, y) in the same space as `data`.
543
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.
@@ -789,13 +810,31 @@ def xie_beni(x: np.ndarray, y: np.ndarray) -> float:
789
810
  return xb
790
811
 
791
812
 
792
- def i_index(x: np.ndarray, y: np.ndarray):
813
+ def i_index(x: np.ndarray, y: np.ndarray, verbose: bool = False) -> float:
793
814
  """
794
815
  Calculate the I-Index for evaluating clustering quality.
795
816
 
796
817
  The I-Index is a metric that measures the compactness and separation of clusters.
797
818
  A higher I-Index indicates better clustering with compact and well-separated clusters.
798
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
+
799
838
  .. seealso::
800
839
  To compute Xie-Beni on the CPU, use :func:`~simba.mixins.statistics_mixin.Statistics.i_index`
801
840
 
@@ -806,17 +845,16 @@ def i_index(x: np.ndarray, y: np.ndarray):
806
845
 
807
846
  :references:
808
847
  .. [1] Zhao, Q., Xu, M., Fränti, P. (2009). Sum-of-Squares Based Cluster Validity Index and Significance Analysis.
809
- In: Kolehmainen, M., Toivanen, P., Beliczynski, B. (eds) Adaptive and Natural Computing Algorithms. ICANNGA 2009.
810
- 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
811
849
 
812
850
  :example:
813
851
  >>> X, y = make_blobs(n_samples=5000, centers=20, n_features=3, random_state=0, cluster_std=0.1)
814
852
  >>> i_index(x=X, y=y)
815
853
  """
854
+ timer = SimbaTimer(start=True)
816
855
  check_valid_array(data=x, accepted_ndims=(2,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
817
- check_valid_array(data=y, accepted_ndims=(1,), accepted_dtypes=Formats.NUMERIC_DTYPES.value,
818
- accepted_axis_0_shape=[x.shape[0], ])
819
- _ = 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)
820
858
  x, y = cp.array(x), cp.array(y)
821
859
  unique_y = cp.unique(y)
822
860
  n_y = unique_y.shape[0]
@@ -830,12 +868,16 @@ def i_index(x: np.ndarray, y: np.ndarray):
830
868
  swc += cp.sum(cp.linalg.norm(cluster_obs - cluster_centroid, axis=1) ** 2)
831
869
 
832
870
  i_idx = sst / (n_y * swc)
833
-
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)')
834
874
  return i_idx
835
875
 
836
-
837
- def kullback_leibler_divergence_gpu(x: np.ndarray, y: np.ndarray, fill_value: int = 1, bucket_method: Literal[
838
- "fd", "doane", "auto", "scott", "stone", "rice", "sturges", "sqrt"] = "scott") -> float:
876
+ def kullback_leibler_divergence_gpu(x: np.ndarray,
877
+ y: np.ndarray,
878
+ fill_value: int = 1,
879
+ bucket_method: Literal["fd", "doane", "auto", "scott", "stone", "rice", "sturges", "sqrt"] = "scott",
880
+ verbose: bool = False) -> float:
839
881
  """
840
882
  Compute Kullback-Leibler divergence between two distributions.
841
883
 
@@ -847,7 +889,6 @@ def kullback_leibler_divergence_gpu(x: np.ndarray, y: np.ndarray, fill_value: in
847
889
  .. seealso::
848
890
  For CPU implementation, see :func:`simba.mixins.statistics_mixin.Statistics.kullback_leibler_divergence`.
849
891
 
850
-
851
892
  :param ndarray x: First 1d array representing feature values.
852
893
  :param ndarray y: Second 1d array representing feature values.
853
894
  :param Optional[int] fill_value: Optional pseudo-value to use to fill empty buckets in ``y`` histogram
@@ -860,13 +901,18 @@ def kullback_leibler_divergence_gpu(x: np.ndarray, y: np.ndarray, fill_value: in
860
901
  >>> kl = kullback_leibler_divergence_gpu(x=x, y=y)
861
902
  """
862
903
 
904
+ timer = SimbaTimer(start=True)
905
+
863
906
  bin_width, bin_count = bucket_data(data=x, method=bucket_method)
864
907
  r = np.array([np.min(x), np.max(x)])
865
908
  x_hist = Statistics._hist_1d(data=x, bin_count=bin_count, range=r)
866
909
  y_hist = Statistics._hist_1d(data=y, bin_count=bin_count, range=r)
867
910
  y_hist[y_hist == 0] = fill_value
868
911
  x_hist, y_hist = x_hist / np.sum(x_hist), y_hist / np.sum(y_hist)
869
- return kl_divergence_gpu(P=x_hist.astype(np.float32), Q=y_hist.astype(np.float32), convert_dtype=False)
912
+ r = kl_divergence_gpu(P=x_hist.astype(np.float32), Q=y_hist.astype(np.float32), convert_dtype=False)
913
+ timer.stop_timer()
914
+ if verbose: print(f'KL divergence performed on {x.shape[0]} observations (elapsed time: {timer.elapsed_time_str}s)')
915
+ return r
870
916
 
871
917
 
872
918
  @cuda.jit()
@@ -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.
@@ -1,13 +1,9 @@
1
1
  __author__ = "Simon Nilsson; sronilsson@gmail.com"
2
2
 
3
3
  import functools
4
- import glob
5
- import itertools
6
4
  import multiprocessing
7
5
  import os
8
- import platform
9
- import time
10
- from typing import Dict, List, Optional, Union
6
+ from typing import Dict, List, Union
11
7
 
12
8
  import cv2
13
9
  import numpy as np
@@ -17,9 +13,9 @@ from simba.mixins.config_reader import ConfigReader
17
13
  from simba.mixins.statistics_mixin import Statistics
18
14
  from simba.utils.checks import (
19
15
  check_all_file_names_are_represented_in_video_log, check_if_dir_exists,
20
- check_if_valid_img, check_int, check_nvidea_gpu_available,
21
- check_valid_boolean, check_valid_lst)
22
- from simba.utils.data import detect_bouts, slice_roi_dict_from_attribute
16
+ check_if_valid_img, check_int, check_valid_boolean, check_valid_lst)
17
+ from simba.utils.data import (detect_bouts, slice_roi_dict_from_attribute,
18
+ terminate_cpu_pool)
23
19
  from simba.utils.enums import Defaults, Keys
24
20
  from simba.utils.errors import NoROIDataError
25
21
  from simba.utils.printing import SimbaTimer, stdout_success
@@ -220,7 +216,7 @@ class CueLightAnalyzer(ConfigReader):
220
216
  else: self.intensities[key] = subdict
221
217
  if self.verbose:
222
218
  print(f'Batch {int(np.ceil(cnt + 1 / self.core_cnt))} complete...')
223
- pool.terminate(); pool.join()
219
+ terminate_cpu_pool(pool=pool, force=False)
224
220
  kmeans = self._get_kmeans(intensities=self.intensities)
225
221
  self.data_df = self._append_light_data(data_df=self.data_df, kmeans_data=kmeans)
226
222
  self.data_df = self._remove_outlier_events(data_df=self.data_df)
@@ -7,7 +7,8 @@ import pandas as pd
7
7
  from simba.utils.checks import (check_if_dir_exists, check_if_valid_rgb_tuple,
8
8
  check_int, check_str, check_valid_boolean,
9
9
  check_valid_dataframe, check_valid_tuple)
10
- from simba.utils.data import egocentrically_align_pose_numba
10
+ from simba.utils.data import (egocentrically_align_pose_numba, get_cpu_pool,
11
+ terminate_cpu_pool)
11
12
  from simba.utils.enums import Formats, Options
12
13
  from simba.utils.errors import InvalidInputError
13
14
  from simba.utils.printing import SimbaTimer, stdout_success
@@ -51,7 +52,7 @@ class EgocentricalAligner():
51
52
  :param Optional[int] core_cnt: Number of CPU cores to use for video rotation; `-1` uses all available cores.
52
53
 
53
54
  :example:
54
- >>> 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))
55
56
  >>> aligner.run()
56
57
  """
57
58
 
@@ -73,7 +74,7 @@ class EgocentricalAligner():
73
74
  check_str(name=f'{self.__class__.__name__} anchor_1', value=anchor_1, allow_blank=False)
74
75
  check_str(name=f'{self.__class__.__name__} anchor_2', value=anchor_2, allow_blank=False)
75
76
  check_int(name=f'{self.__class__.__name__} core_cnt', value=core_cnt, min_value=-1, max_value=find_core_cnt()[0], unaccepted_vals=[0])
76
- if core_cnt == -1: self.core_cnt = find_core_cnt()[0]
77
+ self.core_cnt = find_core_cnt()[0] if core_cnt == -1 or core_cnt > find_core_cnt()[0] else core_cnt
77
78
  check_int(name=f'{self.__class__.__name__} direction', value=direction, min_value=0, max_value=360)
78
79
  if isinstance(anchor_location, tuple):
79
80
  check_valid_tuple(x=anchor_location, source=f'{self.__class__.__name__} anchor_location', accepted_lengths=(2,), valid_dtypes=(int,))
@@ -98,6 +99,7 @@ class EgocentricalAligner():
98
99
 
99
100
  def run(self):
100
101
  timer = SimbaTimer(start=True)
102
+ self.pool = None if not self.rotate_video else get_cpu_pool(core_cnt=self.core_cnt, source=self.__class__.__name__)
101
103
  for file_cnt, file_path in enumerate(self.data_paths):
102
104
  video_timer = SimbaTimer(start=True)
103
105
  _, self.video_name, _ = get_fn_ext(filepath=file_path)
@@ -127,8 +129,7 @@ class EgocentricalAligner():
127
129
  if self.verbose:
128
130
  print(f'{self.video_name} complete, saved at {save_path} (elapsed time: {video_timer.elapsed_time_str}s)')
129
131
  if self.rotate_video:
130
- if self.verbose:
131
- print(f'Rotating video {self.video_name}...')
132
+ if self.verbose: print(f'Rotating video {self.video_name}...')
132
133
  video_path = find_video_of_file(video_dir=self.videos_dir, filename=self.video_name, raise_error=False)
133
134
  save_path = os.path.join(self.save_dir, f'{self.video_name}.mp4')
134
135
  video_rotator = EgocentricVideoRotator(video_path=video_path,
@@ -139,11 +140,13 @@ class EgocentricalAligner():
139
140
  gpu=self.gpu,
140
141
  fill_clr=self.fill_clr,
141
142
  core_cnt=self.core_cnt,
142
- save_path=save_path)
143
+ save_path=save_path,
144
+ pool=self.pool)
143
145
  video_rotator.run()
144
146
  if self.verbose:
145
147
  print(f'Rotated data for video {self.video_name} ({file_cnt+1}/{len(self.data_paths)}) saved in {self.save_dir}.')
146
148
  timer.stop_timer()
149
+ terminate_cpu_pool(pool=self.pool, source=self.__class__.__name__)
147
150
  stdout_success(msg=f'Egocentrically aligned data for {len(self.data_paths)} files saved in {self.save_dir}', elapsed_time=timer.elapsed_time_str)
148
151
 
149
152
 
@@ -156,9 +159,24 @@ class EgocentricalAligner():
156
159
  # direction=0,
157
160
  # gpu=True,
158
161
  # anchor_location=(600, 300),
159
- # fill_clr=(128,128,128))
162
+ # fill_clr=(128,128,128),
163
+ # core_cnt=18)
160
164
  # aligner.run()
161
165
 
166
+
167
+ if __name__ == "__main__":
168
+ aligner = EgocentricalAligner(anchor_1='butt/proximal tail',
169
+ anchor_2='snout',
170
+ data_dir=r'C:\troubleshooting\open_field_below\project_folder\csv\outlier_corrected_movement_location',
171
+ videos_dir=r'C:\troubleshooting\open_field_below\project_folder\videos',
172
+ save_dir=r"C:\troubleshooting\open_field_below\project_folder\videos\rotated",
173
+ direction=0,
174
+ gpu=True,
175
+ anchor_location=(600, 300),
176
+ fill_clr=(128,128,128),
177
+ core_cnt=18)
178
+ aligner.run()
179
+
162
180
  # aligner = EgocentricalAligner(anchor_1='tail_base',
163
181
  # anchor_2='nose',
164
182
  # data_dir=r'C:\Users\sroni\OneDrive\Desktop\rotate_ex\data',
@@ -15,64 +15,74 @@ from simba.utils.data import detect_bouts, plug_holes_shortest_bout
15
15
  from simba.utils.enums import Formats
16
16
  from simba.utils.printing import stdout_success
17
17
  from simba.utils.read_write import (find_files_of_filetypes_in_directory,
18
- get_fn_ext, read_df, read_video_info)
18
+ get_current_time, get_fn_ext, read_df,
19
+ read_video_info)
19
20
 
20
21
  NAPE_X, NAPE_Y = 'nape_x', 'nape_y'
21
22
  FREEZING = 'FREEZING'
22
23
 
23
24
  class FreezingDetector(ConfigReader):
24
-
25
25
  """
26
- Detect freezing behavior using heuristic rules.
27
-
26
+ Detect freezing behavior using heuristic rules based on movement velocity thresholds.
27
+ Analyzes pose-estimation data to detect freezing episodes by computing the mean velocity
28
+ of key body parts (nape, nose, and tail-base) and identifying periods where movement falls below
29
+ a specified threshold for a minimum duration.
28
30
  .. important::
29
-
30
31
  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
-
32
+ the movement threshold for the duration (and longer) of the specified time-window**.
33
33
  Freezing is detected as `absent` when not present.
34
-
35
34
  .. 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
-
35
+ The method uses the left and right ear body-parts to compute the `nape` location of the animal
36
+ as the midpoint between the ears. The nape, nose, and tail-base movements are averaged to compute
37
+ overall animal movement velocity.
38
+ :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.
39
+ :param Union[str, os.PathLike] config_path: Path to SimBA project config file (`.ini` format) containing project settings and video information.
40
+ :param Optional[str] nose_name: The name of the pose-estimated nose body-part column (without _x/_y suffix). Defaults to 'nose'.
41
+ :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'.
42
+ :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'.
43
+ :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'.
44
+ :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.
45
+ :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.
46
+ :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.
47
+ :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.
48
+ :returns: None. Results are saved to CSV files in the specified save directory:
49
+ - Individual video results: One CSV file per video with freezing annotations added as a 'FREEZING' column (1 = freezing, 0 = not freezing)
50
+ - Aggregate results: `aggregate_freezing_results.csv` containing summary statistics for all videos
49
51
  :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
-
52
+ >>> FreezingDetector(
53
+ ... data_dir=r'D:\\troubleshooting\\mitra\\project_folder\\csv\\outlier_corrected_movement_location',
54
+ ... config_path=r"D:\\troubleshooting\\mitra\\project_folder\\project_config.ini",
55
+ ... time_window=3,
56
+ ... movement_threshold=5,
57
+ ... shortest_bout=100
58
+ ... ).run()
52
59
  References
53
60
  ----------
61
+ ..
54
62
  .. [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
63
  .. [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
64
+ .. [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
65
  .. [4] Lazaro et al., Brainwide Genetic Capture for Conscious State Transitions, `biorxiv`, doi: https://doi.org/10.1101/2025.03.28.646066
66
+ .. [5] Sabnis et al., Visual detection of seizures in mice using supervised machine learning, 2025, Cell Reports Methods 5, 101242 December 15, 2025.
58
67
  """
59
-
60
68
  def __init__(self,
61
- data_dir: Union[str, os.PathLike],
62
69
  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,
70
+ nose_name: str = 'nose',
71
+ left_ear_name: str = 'Left_ear',
72
+ right_ear_name: str = 'right_ear',
73
+ tail_base_name: str = 'tail_base',
74
+ data_dir: Optional[Union[str, os.PathLike]] = None,
75
+ time_window: int = 4,
76
+ movement_threshold: int = 5,
77
+ shortest_bout: int = 100,
70
78
  save_dir: Optional[Union[str, os.PathLike]] = None):
71
-
72
- check_if_dir_exists(in_dir=data_dir)
73
79
  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
80
  ConfigReader.__init__(self, config_path=config_path, read_video_info=True, create_logger=False)
81
+ if data_dir is not None:
82
+ check_if_dir_exists(in_dir=data_dir)
83
+ else:
84
+ data_dir = self.outlier_corrected_dir
85
+ self.data_paths = find_files_of_filetypes_in_directory(directory=data_dir, extensions=['.csv'])
76
86
  self.nose_heads = [f'{nose_name}_x'.lower(), f'{nose_name}_y'.lower()]
77
87
  self.left_ear_heads = [f'{left_ear_name}_x'.lower(), f'{left_ear_name}_y'.lower()]
78
88
  self.right_ear_heads = [f'{right_ear_name}_x'.lower(), f'{right_ear_name}_y'.lower()]
@@ -82,21 +92,19 @@ class FreezingDetector(ConfigReader):
82
92
  check_int(name='movement_threshold', value=movement_threshold, min_value=1)
83
93
  self.save_dir = save_dir
84
94
  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}')
95
+ self.save_dir = os.path.join(self.logs_path, f'freezing_data_time_{time_window}s_{movement_threshold}mm_{self.datetime}')
86
96
  os.makedirs(self.save_dir)
87
97
  else:
88
98
  check_if_dir_exists(in_dir=self.save_dir)
89
99
  self.time_window, self.movement_threshold = time_window, movement_threshold
90
100
  self.movement_threshold, self.shortest_bout = movement_threshold, shortest_bout
91
- self.run()
92
-
93
101
  def run(self):
94
102
  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')
103
+ agg_results_path = os.path.join(self.save_dir, f'aggregate_freezing_results_{self.datetime}.csv')
96
104
  check_all_file_names_are_represented_in_video_log(video_info_df=self.video_info_df, data_paths=self.data_paths)
97
105
  for file_cnt, file_path in enumerate(self.data_paths):
98
106
  video_name = get_fn_ext(filepath=file_path)[1]
99
- print(f'Analyzing {video_name}...({file_cnt+1}/{len(self.data_paths)})')
107
+ print(f'[{get_current_time()}] Analyzing freezing {video_name}...(video {file_cnt+1}/{len(self.data_paths)})')
100
108
  save_file_path = os.path.join(self.save_dir, f'{video_name}.csv')
101
109
  df = read_df(file_path=file_path, file_type='csv').reset_index(drop=True)
102
110
  _, px_per_mm, fps = read_video_info(vid_info_df=self.video_info_df, video_name=video_name)
@@ -118,23 +126,23 @@ class FreezingDetector(ConfigReader):
118
126
  mean_movement = np.mean(movement, axis=1)
119
127
  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
128
  freezing_idx = np.argwhere(mm_s <= self.movement_threshold).astype(np.int32).flatten()
129
+ df[f'Probability_{FREEZING}'] = 0
121
130
  df[FREEZING] = 0
122
131
  df.loc[freezing_idx, FREEZING] = 1
123
132
  df = plug_holes_shortest_bout(data_df=df, clf_name=FREEZING, fps=fps, shortest_bout=self.shortest_bout)
124
133
  bouts = detect_bouts(data_df=df, target_lst=[FREEZING], fps=fps)
125
134
  bouts = bouts[bouts['Bout_time'] >= self.time_window]
126
135
  if len(bouts) > 0:
136
+ df[FREEZING] = 0
127
137
  freezing_idx = list(bouts.apply(lambda x: list(range(int(x["Start_frame"]), int(x["End_frame"]) + 1)), 1))
128
138
  freezing_idx = [x for xs in freezing_idx for x in xs]
129
139
  df.loc[freezing_idx, FREEZING] = 1
140
+ df.loc[freezing_idx, f'Probability_{FREEZING}'] = 1
130
141
  else:
142
+ df[FREEZING] = 0
131
143
  freezing_idx = []
132
144
  df.to_csv(save_file_path)
145
+ print(video_name, len(freezing_idx), round(len(freezing_idx) / fps, 4), df[FREEZING].sum())
133
146
  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
147
  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")
148
+ self.timer.stop_timer(); stdout_success(msg=f'Results saved in {self.save_dir} directory.', elapsed_time=self.timer.elapsed_time_str)