simba-uw-tf-dev 4.6.2__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.
- simba/assets/.recent_projects.txt +1 -0
- simba/assets/lookups/tooptips.json +6 -1
- simba/data_processors/agg_clf_counter_mp.py +52 -53
- simba/data_processors/blob_location_computer.py +1 -1
- simba/data_processors/circling_detector.py +30 -13
- simba/data_processors/cuda/geometry.py +45 -27
- simba/data_processors/cuda/image.py +1648 -1598
- simba/data_processors/cuda/statistics.py +72 -26
- simba/data_processors/cuda/timeseries.py +1 -1
- simba/data_processors/cue_light_analyzer.py +5 -9
- simba/data_processors/egocentric_aligner.py +25 -7
- simba/data_processors/freezing_detector.py +55 -47
- simba/data_processors/kleinberg_calculator.py +61 -29
- simba/feature_extractors/feature_subsets.py +14 -7
- simba/feature_extractors/mitra_feature_extractor.py +2 -2
- simba/feature_extractors/straub_tail_analyzer.py +4 -6
- simba/labelling/standard_labeller.py +1 -1
- simba/mixins/config_reader.py +5 -2
- simba/mixins/geometry_mixin.py +22 -36
- simba/mixins/image_mixin.py +24 -28
- simba/mixins/plotting_mixin.py +28 -10
- simba/mixins/statistics_mixin.py +48 -11
- simba/mixins/timeseries_features_mixin.py +1 -1
- simba/mixins/train_model_mixin.py +67 -29
- simba/model/inference_batch.py +1 -1
- simba/model/yolo_seg_inference.py +3 -3
- simba/outlier_tools/skip_outlier_correction.py +1 -1
- simba/plotting/ROI_feature_visualizer_mp.py +3 -5
- simba/plotting/clf_validator_mp.py +4 -5
- simba/plotting/cue_light_visualizer.py +6 -7
- simba/plotting/directing_animals_visualizer_mp.py +2 -3
- simba/plotting/distance_plotter_mp.py +378 -378
- simba/plotting/gantt_creator.py +29 -10
- simba/plotting/gantt_creator_mp.py +96 -33
- simba/plotting/geometry_plotter.py +270 -272
- simba/plotting/heat_mapper_clf_mp.py +4 -6
- simba/plotting/heat_mapper_location_mp.py +2 -2
- simba/plotting/light_dark_box_plotter.py +2 -2
- simba/plotting/path_plotter_mp.py +26 -29
- simba/plotting/plot_clf_results_mp.py +455 -454
- simba/plotting/pose_plotter_mp.py +28 -29
- simba/plotting/probability_plot_creator_mp.py +288 -288
- simba/plotting/roi_plotter_mp.py +31 -31
- simba/plotting/single_run_model_validation_video_mp.py +427 -427
- simba/plotting/spontaneous_alternation_plotter.py +2 -3
- simba/plotting/yolo_pose_track_visualizer.py +32 -27
- simba/plotting/yolo_pose_visualizer.py +35 -36
- simba/plotting/yolo_seg_visualizer.py +2 -3
- simba/pose_importers/simba_blob_importer.py +3 -3
- simba/roi_tools/roi_aggregate_stats_mp.py +5 -4
- simba/roi_tools/roi_clf_calculator_mp.py +4 -4
- simba/sandbox/analyze_runtimes.py +30 -0
- simba/sandbox/cuda/egocentric_rotator.py +374 -374
- simba/sandbox/get_cpu_pool.py +5 -0
- simba/sandbox/proboscis_to_tip.py +28 -0
- simba/sandbox/test_directionality.py +47 -0
- simba/sandbox/test_nonstatic_directionality.py +27 -0
- simba/sandbox/test_pycharm_cuda.py +51 -0
- simba/sandbox/test_simba_install.py +41 -0
- simba/sandbox/test_static_directionality.py +26 -0
- simba/sandbox/test_static_directionality_2d.py +26 -0
- simba/sandbox/verify_env.py +42 -0
- simba/third_party_label_appenders/transform/coco_keypoints_to_yolo.py +3 -3
- simba/third_party_label_appenders/transform/coco_keypoints_to_yolo_bbox.py +2 -2
- simba/ui/pop_ups/clf_plot_pop_up.py +2 -2
- simba/ui/pop_ups/fsttc_pop_up.py +27 -25
- simba/ui/pop_ups/gantt_pop_up.py +31 -6
- simba/ui/pop_ups/kleinberg_pop_up.py +39 -40
- simba/ui/pop_ups/video_processing_pop_up.py +37 -29
- simba/ui/tkinter_functions.py +3 -0
- simba/utils/custom_feature_extractor.py +1 -1
- simba/utils/data.py +90 -14
- simba/utils/enums.py +1 -0
- simba/utils/errors.py +441 -440
- simba/utils/lookups.py +1203 -1203
- simba/utils/printing.py +124 -124
- simba/utils/read_write.py +3769 -3721
- simba/utils/yolo.py +10 -1
- simba/video_processors/blob_tracking_executor.py +2 -2
- simba/video_processors/clahe_ui.py +1 -1
- simba/video_processors/egocentric_video_rotator.py +44 -41
- simba/video_processors/multi_cropper.py +1 -1
- simba/video_processors/video_processing.py +5264 -5222
- simba/video_processors/videos_to_frames.py +43 -33
- {simba_uw_tf_dev-4.6.2.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/METADATA +4 -3
- {simba_uw_tf_dev-4.6.2.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/RECORD +90 -80
- {simba_uw_tf_dev-4.6.2.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/LICENSE +0 -0
- {simba_uw_tf_dev-4.6.2.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/WHEEL +0 -0
- {simba_uw_tf_dev-4.6.2.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/entry_points.txt +0 -0
- {simba_uw_tf_dev-4.6.2.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,
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
..
|
|
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
|
|
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,
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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
|
|
46
|
-
:param Optional[int] movement_threshold:
|
|
47
|
-
:param Optional[
|
|
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(
|
|
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.
|
|
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:
|
|
64
|
-
left_ear_name:
|
|
65
|
-
right_ear_name:
|
|
66
|
-
tail_base_name:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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, '
|
|
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)
|