From 5c1e814b7bf535f9ac30dc51691f20e93d8ea602 Mon Sep 17 00:00:00 2001 From: sronilsson Date: Fri, 8 Nov 2024 14:22:32 -0500 Subject: [PATCH] outlier_correction_mp --- docs/_static/img/spatial_density.webp | Bin 0 -> 29042 bytes docs/nb/CLI Example 1.ipynb | 4 +- docs/nb/outlier_correction.ipynb | 184 ++++++++++++----- setup.py | 2 +- simba/mixins/config_reader.py | 8 +- simba/mixins/statistics_mixin.py | 33 ++++ .../outlier_corrector_location.py | 17 +- .../outlier_corrector_location_mp.py | 187 ++++++++++++++++++ .../outlier_corrector_movement.py | 20 +- .../outlier_corrector_movement_mp.py | 178 +++++++++++++++++ simba/utils/checks.py | 12 +- 11 files changed, 557 insertions(+), 88 deletions(-) create mode 100644 docs/_static/img/spatial_density.webp create mode 100644 simba/outlier_tools/outlier_corrector_location_mp.py create mode 100644 simba/outlier_tools/outlier_corrector_movement_mp.py diff --git a/docs/_static/img/spatial_density.webp b/docs/_static/img/spatial_density.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b1d2c534534035c54de6200088afbd8a7bcbaf5 GIT binary patch literal 29042 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 literal 0 HcmV?d00001 diff --git a/docs/nb/CLI Example 1.ipynb b/docs/nb/CLI Example 1.ipynb index 50f0085b5..dd2dc9b56 100644 --- a/docs/nb/CLI Example 1.ipynb +++ b/docs/nb/CLI Example 1.ipynb @@ -438,9 +438,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python_3.6", + "display_name": "simba", "language": "python", - "name": "python_3.6" + "name": "simba" }, "language_info": { "codemirror_mode": { diff --git a/docs/nb/outlier_correction.ipynb b/docs/nb/outlier_correction.ipynb index 41778cf28..d56064eca 100644 --- a/docs/nb/outlier_correction.ipynb +++ b/docs/nb/outlier_correction.ipynb @@ -19,20 +19,20 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 1, "id": "cb913766", "metadata": {}, "outputs": [], "source": [ "from simba.outlier_tools.outlier_corrector_movement import OutlierCorrecterMovement\n", "from simba.outlier_tools.outlier_corrector_location import OutlierCorrecterLocation\n", - "from simba.utils.cli import set_outlier_correction_criteria_cli\n", - "from simba.pose_importers.dlc_importer_csv import import_multiple_dlc_tracking_csv_file" + "from simba.utils.cli.cli_tools import set_outlier_correction_criteria_cli\n", + "from simba.pose_importers.dlc_importer_csv import import_dlc_csv_data" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "id": "f7cc63fa", "metadata": {}, "outputs": [], @@ -41,16 +41,15 @@ "# DATA, AND (III) THE ATTRIBUTES OF OUR NEW VIDEOS (FPS ETC.)\n", "\n", "## Define the path to our SimBA project config ini\n", - "CONFIG_PATH = '/Users/simon/Desktop/envs/troubleshooting/notebook_example/project_folder/project_config.ini'\n", + "CONFIG_PATH = r\"C:\\troubleshooting\\two_black_animals_14bp\\project_folder\\project_config.ini\"\n", "\n", "## Define the path to the directory holding our new DLC CSV pose-estimation data\n", - "DATA_DIR = '/Users/simon/Desktop/envs/troubleshooting/notebook_example/data'\n", + "DATA_DIR = r\"C:\\troubleshooting\\two_black_animals_14bp\\dlc_data\"\n", "\n", "## Define if / how you want to interpolate missing pose-estimation data,\n", "## and if/how you want to smooth the new pose estimation data: here we do neither.\n", - "INTERPOLATION_SETTING = 'None' # OPTIONS: 'None', Animal(s): Nearest', 'Animal(s): Linear', 'Animal(s): Quadratic','Body-parts: Nearest', 'Body-parts: Linear', 'Body-parts: Quadratic'\n", - "SMOOTHING_SETTING = None # OPTIONS: 'Gaussian', 'Savitzky Golay'\n", - "SMOOTHING_TIME = None # TIME IN MILLISECOND\n", + "INTERPOLATION_SETTING = None # OPTIONS: 'None', Animal(s): Nearest', 'Animal(s): Linear', 'Animal(s): Quadratic','Body-parts: Nearest', 'Body-parts: Linear', 'Body-parts: Quadratic'\n", + "SMOOTHING_SETTING = None # OPTIONS: {'time_window': 500, 'method': 'savitzky-golay'}, {'time_window': 500, 'method': 'gaussian'}\n", "\n", "## Define the fps and the pixels per millimeter of the incoming data: has to be the same for all new videos.\n", "## if you have varying fps / px per millimeter / resolutions, then use gui (2023/05)\n", @@ -71,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 3, "id": "c3d8767e", "metadata": {}, "outputs": [ @@ -79,29 +78,41 @@ "name": "stdout", "output_type": "stream", "text": [ - "Importing Aqu_FFJ_Cre_721 to SimBA project...\n", - "Pose-estimation data for video Aqu_FFJ_Cre_721 imported to SimBA project (elapsed time: 0.1718s)...\n", - "Importing Aqu_FFJ_Cre_723 to SimBA project...\n", - "Pose-estimation data for video Aqu_FFJ_Cre_723 imported to SimBA project (elapsed time: 0.1681s)...\n", - "Importing Aqu_FFJ_Cre_722 to SimBA project...\n", - "Pose-estimation data for video Aqu_FFJ_Cre_722 imported to SimBA project (elapsed time: 0.1617s)...\n", - "SIMBA COMPLETE: Imported 3 pose estimation file(s) (elapsed time: 0.5078s) \tcomplete\n" + "Importing Test_1 to SimBA project...\n", + "Pose-estimation data for video Test_1 imported to SimBA project (elapsed time: 0.0546s)...\n", + "Importing Test_2 to SimBA project...\n", + "Pose-estimation data for video Test_2 imported to SimBA project (elapsed time: 0.052s)...\n", + "Importing Test_3 to SimBA project...\n", + "Pose-estimation data for video Test_3 imported to SimBA project (elapsed time: 0.0463s)...\n", + "Importing Test_4 to SimBA project...\n", + "Pose-estimation data for video Test_4 imported to SimBA project (elapsed time: 0.0431s)...\n", + "Importing Test_5 to SimBA project...\n", + "Pose-estimation data for video Test_5 imported to SimBA project (elapsed time: 0.0482s)...\n", + "Importing Test_6 to SimBA project...\n", + "Pose-estimation data for video Test_6 imported to SimBA project (elapsed time: 0.0583s)...\n", + "Importing Test_7 to SimBA project...\n", + "Pose-estimation data for video Test_7 imported to SimBA project (elapsed time: 0.0503s)...\n", + "Importing Test_8 to SimBA project...\n", + "Pose-estimation data for video Test_8 imported to SimBA project (elapsed time: 0.05s)...\n", + "Importing Test_9 to SimBA project...\n", + "Pose-estimation data for video Test_9 imported to SimBA project (elapsed time: 0.058s)...\n", + "SIMBA COMPLETE: Imported 9 pose estimation file(s) to directory (elapsed time: 0.4758s) \tcomplete\n" ] } ], "source": [ "# WE RUN THE DATA IMPORTER FOR OUR DIRECTORY OF FILES\n", "## This imports your DLC files in the ``DATA_DIR`` according to the smoothing / interpolation settings defined above\n", - "import_multiple_dlc_tracking_csv_file(config_path=CONFIG_PATH,\n", - " interpolation_setting=INTERPOLATION_SETTING,\n", - " smoothing_setting=SMOOTHING_SETTING,\n", - " smoothing_time=SMOOTHING_TIME,\n", - " data_dir=DATA_DIR)" + "\n", + "import_dlc_csv_data(config_path=CONFIG_PATH,\n", + " interpolation_settings=INTERPOLATION_SETTING,\n", + " smoothing_settings=SMOOTHING_SETTING,\n", + " data_path=DATA_DIR)" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 5, "id": "38306f7e", "metadata": {}, "outputs": [ @@ -109,51 +120,136 @@ "name": "stdout", "output_type": "stream", "text": [ - "Processing video Aqu_FFJ_Cre_721. Video 1/3...\n", - "Corrected movement outliers for file Aqu_FFJ_Cre_721 (elapsed time: 0.2929s)...\n", - "Processing video Aqu_FFJ_Cre_723. Video 2/3...\n", - "Corrected movement outliers for file Aqu_FFJ_Cre_723 (elapsed time: 0.2713s)...\n", - "Processing video Aqu_FFJ_Cre_722. Video 3/3...\n", - "Corrected movement outliers for file Aqu_FFJ_Cre_722 (elapsed time: 0.2674s)...\n", - "SIMBA COMPLETE: Log for corrected \"movement outliers\" saved in project_folder/logs (elapsed time: 0.8572s) \tcomplete\n", - "Processing video Aqu_FFJ_Cre_721. Video 1/3..\n", - "Corrected location outliers for file Aqu_FFJ_Cre_721 (elapsed time: 49.6797s)...\n", - "Processing video Aqu_FFJ_Cre_723. Video 2/3..\n", - "Corrected location outliers for file Aqu_FFJ_Cre_723 (elapsed time: 24.645s)...\n", - "Processing video Aqu_FFJ_Cre_722. Video 3/3..\n", - "Corrected location outliers for file Aqu_FFJ_Cre_722 (elapsed time: 15.1142s)...\n", - "SIMBA COMPLETE: Log for corrected \"location outliers\" saved in project_folder/logs (elapsed time: 89.4743s) \tcomplete\n" + "SIMBA COMPLETE: Outlier parameters set (elapsed time: 0.003s) \tcomplete\n" ] } ], "source": [ - "#We set the outlier criteria in the project_config.ini and run the outlier correction. NOTE: You can also set this manually in the project_config.ini or thrugh\n", + "#We set the outlier criteria in the project_config.ini NOTE: You can also set this manually in the project_config.ini or thrugh\n", "# the SimBA GUI. If this has already been done, there is **no need** to call `set_outlier_correction_criteria_cli`.\n", "set_outlier_correction_criteria_cli(config_path=CONFIG_PATH,\n", " aggregation=AGGREGATION_METHOD,\n", " body_parts=BODY_PARTS,\n", " movement_criterion=MOVEMENT_CRITERION,\n", - " location_criterion=LOCATION_CRITERION)\n", - "\n", - "\n", + " location_criterion=LOCATION_CRITERION)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ff58e186", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processing video Test_1. Video 1/9...\n", + "Corrected movement outliers for file Test_1 (elapsed time: 0.435s)...\n", + "Processing video Test_2. Video 2/9...\n", + "Corrected movement outliers for file Test_2 (elapsed time: 0.1166s)...\n", + "Processing video Test_3. Video 3/9...\n", + "Corrected movement outliers for file Test_3 (elapsed time: 0.1158s)...\n", + "Processing video Test_4. Video 4/9...\n", + "Corrected movement outliers for file Test_4 (elapsed time: 0.114s)...\n", + "Processing video Test_5. Video 5/9...\n", + "Corrected movement outliers for file Test_5 (elapsed time: 0.1183s)...\n", + "Processing video Test_6. Video 6/9...\n", + "Corrected movement outliers for file Test_6 (elapsed time: 0.1124s)...\n", + "Processing video Test_7. Video 7/9...\n", + "Corrected movement outliers for file Test_7 (elapsed time: 0.1151s)...\n", + "Processing video Test_8. Video 8/9...\n", + "Corrected movement outliers for file Test_8 (elapsed time: 0.117s)...\n", + "Processing video Test_9. Video 9/9...\n", + "Corrected movement outliers for file Test_9 (elapsed time: 0.1152s)...\n", + "SIMBA COMPLETE: Log for corrected \"movement outliers\" saved in C:\\troubleshooting\\two_black_animals_14bp\\project_folder\\logs (elapsed time: 1.3693s) \tcomplete\n", + "Processing video Test_1. Video 1/9..\n", + "Corrected location outliers for file Test_1 (elapsed time: 0.8654s)...\n", + "Processing video Test_2. Video 2/9..\n", + "Corrected location outliers for file Test_2 (elapsed time: 0.877s)...\n", + "Processing video Test_3. Video 3/9..\n", + "Corrected location outliers for file Test_3 (elapsed time: 0.8534s)...\n", + "Processing video Test_4. Video 4/9..\n", + "Corrected location outliers for file Test_4 (elapsed time: 0.8611s)...\n", + "Processing video Test_5. Video 5/9..\n", + "Corrected location outliers for file Test_5 (elapsed time: 0.8581s)...\n", + "Processing video Test_6. Video 6/9..\n", + "Corrected location outliers for file Test_6 (elapsed time: 0.8512s)...\n", + "Processing video Test_7. Video 7/9..\n", + "Corrected location outliers for file Test_7 (elapsed time: 0.8616s)...\n", + "Processing video Test_8. Video 8/9..\n", + "Corrected location outliers for file Test_8 (elapsed time: 0.8641s)...\n", + "Processing video Test_9. Video 9/9..\n", + "Corrected location outliers for file Test_9 (elapsed time: 0.8626s)...\n", + "SIMBA COMPLETE: Log for corrected \"location outliers\" saved in project_folder/logs (elapsed time: 7.8084s) \tcomplete\n" + ] + } + ], + "source": [ + "# Finally, we run the outlier correction (NOTE: SEE CELL BELOW FOR ALTERNATIVE WAY OF RUNNING OUTLIER CORRECTION ACROSS MULTIPLE CORES)\n", "_ = OutlierCorrecterMovement(config_path=CONFIG_PATH).run()\n", "_ = OutlierCorrecterLocation(config_path=CONFIG_PATH).run()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "47975a2a", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Video Test_1 complete...\n", + "Video Test_2 complete...\n", + "Video Test_3 complete...\n", + "Video Test_4 complete...\n", + "Video Test_5 complete...\n", + "Video Test_6 complete...\n", + "Video Test_7 complete...\n", + "Video Test_8 complete...\n", + "Video Test_9 complete...\n", + "SIMBA COMPLETE: Log for corrected \"movement outliers\" saved in C:\\troubleshooting\\two_black_animals_14bp\\project_folder\\logs (elapsed time: 4.2945s) \tcomplete\n", + "Video Test_1 complete...\n", + "Video Test_2 complete...\n", + "Video Test_3 complete...\n", + "Video Test_4 complete...\n", + "Video Test_5 complete...\n", + "Video Test_6 complete...\n", + "Video Test_7 complete...\n", + "Video Test_8 complete...\n", + "Video Test_9 complete...\n", + "SIMBA COMPLETE: Log for corrected \"location outliers\" saved in project_folder/logs (elapsed time: 4.0219s) \tcomplete\n" + ] + } + ], + "source": [ + "# OPTIONAL: If you find that the outlier correction - as run in the immediate above cell - is slow, we could run outlier \n", + "# correction over multiple cores. If you choose this approach, make sure you are running the latest version of SimBA.\n", + "# You can update SimBA by running `pip install simba-uw-tf-dev --upgrade`\n", + "\n", + "from simba.outlier_tools.outlier_corrector_location_mp import OutlierCorrecterLocationMultiprocess\n", + "from simba.outlier_tools.outlier_corrector_movement_mp import OutlierCorrecterMovementMultiProcess\n", + "\n", + "_ = OutlierCorrecterMovementMultiProcess(config_path=CONFIG_PATH).run()\n", + "_ = OutlierCorrecterLocationMultiprocess(config_path=CONFIG_PATH).run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61fb78fa", + "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:simba_dev] *", + "display_name": "simba", "language": "python", - "name": "conda-env-simba_dev-py" + "name": "simba" }, "language_info": { "codemirror_mode": { diff --git a/setup.py b/setup.py index 56709a1af..131c0dad1 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ # Setup configuration setuptools.setup( name="Simba-UW-tf-dev", - version="2.2.8", + version="2.3.1", author="Simon Nilsson, Jia Jie Choong, Sophia Hwang", author_email="sronilsson@gmail.com", description="Toolkit for computer classification and analysis of behaviors in experimental animals", diff --git a/simba/mixins/config_reader.py b/simba/mixins/config_reader.py index 47ee64443..7e90618ff 100644 --- a/simba/mixins/config_reader.py +++ b/simba/mixins/config_reader.py @@ -152,15 +152,11 @@ def __init__( self.outlier_corrected_movement_dir + f"/*.{self.file_type}" ) self.cpu_cnt, self.cpu_to_use = find_core_cnt() - self.machine_results_paths = glob.glob( - self.machine_results_dir + f"/*.{self.file_type}" - ) + self.machine_results_paths = glob.glob(self.machine_results_dir + f"/*.{self.file_type}") self.logs_path = os.path.join(self.project_path, "logs") self.body_parts_path = os.path.join(self.project_path, Paths.BP_NAMES.value) check_file_exist_and_readable(file_path=self.body_parts_path) - self.body_parts_lst = ( - pd.read_csv(self.body_parts_path, header=None).iloc[:, 0].to_list() - ) + self.body_parts_lst = (pd.read_csv(self.body_parts_path, header=None).iloc[:, 0].to_list()) self.body_parts_lst = [x for x in self.body_parts_lst if str(x) != "nan"] self.get_body_part_names() self.get_bp_headers() diff --git a/simba/mixins/statistics_mixin.py b/simba/mixins/statistics_mixin.py index b5261a4a7..f6178bea9 100644 --- a/simba/mixins/statistics_mixin.py +++ b/simba/mixins/statistics_mixin.py @@ -4182,3 +4182,36 @@ def normalized_google_distance(x: np.ndarray, y: np.ndarray) -> float: return -1.0 else: return N / D + + def symmetry_index(x: np.ndarray, y: np.ndarray, agg_type: Literal['mean', 'median'] = 'mean') -> float: + + """ + Calculate the Symmetry Index (SI) between two arrays of measurements, `x` and `y`, over a given time series. + The Symmetry Index quantifies the relative difference between two measurements at each time point, expressed as a percentage. + The function returns either the mean or median Symmetry Index over the entire series, based on the specified aggregation type. + + Zero indicates perfect symmetry. Positive values pepresent increasing asymmetry between the two measurements. + + :param np.ndarray x: A 1-dimensional array of measurements from one side (e.g., left side), representing a time series or sequence of measurements. + :param np.ndarray y: A 1-dimensional array of measurements from the other side (e.g., right side), of the same length as `x`. + :param Literal['mean', 'median'] agg_type: The aggregation method used to summarize the Symmetry Index across all time points. + :return: The aggregated Symmetry Index over the series, either as the mean or median SI. + :rtype: float + + :example: + >>> x = np.random.randint(0, 155, (100,)) + >>>y = np.random.randint(0, 155, (100,)) + >>> symmetry_index(x=x, y=y) + """ + + check_valid_array(data=x, source=f'{Statistics.symmetry_index.__name__} x', accepted_ndims=(1,), min_axis_0=1, + accepted_dtypes=Formats.NUMERIC_DTYPES.value) + check_valid_array(data=x, source=f'{Statistics.symmetry_index.__name__} y', accepted_ndims=(1,), min_axis_0=1, + accepted_axis_0_shape=[x.shape[0]], accepted_dtypes=Formats.NUMERIC_DTYPES.value) + check_str(name=f'{Statistics.symmetry_index.__name__} agg_type', value=agg_type, options=('mean', 'median')) + si_values = np.abs(x - y) / (0.5 * (x + y)) * 100 + if agg_type == 'mean': + return np.float32(np.nanmean(si_values)) + else: + return np.float32(np.nanmedian(si_values)) + diff --git a/simba/outlier_tools/outlier_corrector_location.py b/simba/outlier_tools/outlier_corrector_location.py index 5fc2a8b52..b09d30ab2 100644 --- a/simba/outlier_tools/outlier_corrector_location.py +++ b/simba/outlier_tools/outlier_corrector_location.py @@ -30,8 +30,7 @@ class OutlierCorrecterLocation(ConfigReader): :param Union[str, os.PathLike] config_path: path to SimBA project config file in Configparser format - Examples - ---------- + :example: >>> _ = OutlierCorrecterLocation(config_path='MyProjectConfig').run() """ @@ -54,18 +53,8 @@ def __init__(self, self.outlier_bp_dict = {} for animal_name in self.animal_bp_dict.keys(): self.outlier_bp_dict[animal_name] = {} - self.outlier_bp_dict[animal_name]["bp_1"] = read_config_entry( - self.config, - ConfigKey.OUTLIER_SETTINGS.value, - "location_bodypart1_{}".format(animal_name.lower()), - "str", - ) - self.outlier_bp_dict[animal_name]["bp_2"] = read_config_entry( - self.config, - ConfigKey.OUTLIER_SETTINGS.value, - "location_bodypart2_{}".format(animal_name.lower()), - "str", - ) + self.outlier_bp_dict[animal_name]["bp_1"] = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, "location_bodypart1_{}".format(animal_name.lower()),"str") + self.outlier_bp_dict[animal_name]["bp_2"] = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, "location_bodypart2_{}".format(animal_name.lower()), "str") def __find_location_outliers(self): for animal_name, animal_data in self.bp_dict.items(): diff --git a/simba/outlier_tools/outlier_corrector_location_mp.py b/simba/outlier_tools/outlier_corrector_location_mp.py new file mode 100644 index 000000000..55663cbe0 --- /dev/null +++ b/simba/outlier_tools/outlier_corrector_location_mp.py @@ -0,0 +1,187 @@ +__author__ = "Simon Nilsson" + + +import os +from typing import Union, Optional, Dict + +import numpy as np +import pandas as pd +import multiprocessing +import functools + +from simba.utils.checks import check_float, check_if_dir_exists, check_int +from simba.mixins.config_reader import ConfigReader +from simba.mixins.feature_extraction_mixin import FeatureExtractionMixin +from simba.utils.enums import ConfigKey, Dtypes, Defaults +from simba.utils.printing import SimbaTimer, stdout_success +from simba.utils.read_write import (get_fn_ext, read_config_entry, read_df, write_df, find_core_cnt, find_files_of_filetypes_in_directory) + +def _location_outlier_corrector(data_path: str, + config: FeatureExtractionMixin, + animal_bp_dict: dict, + outlier_dict: dict, + file_type: str, + save_dir: str, + criterion: float): + + def __find_location_outliers(bp_dict: dict, animal_criteria: dict): + above_criteria_dict, below_criteria_dict = {}, {} + for animal_name, animal_data in bp_dict.items(): + animal_criterion = animal_criteria[animal_name] + above_criteria_dict[animal_name]= {} + for first_bp_cnt, (first_body_part_name, first_bp_cords) in enumerate(animal_data.items()): + second_bp_names = [x for x in list(animal_data.keys()) if x != first_body_part_name] + above_criterion_frms = [] + for second_bp_cnt, second_bp in enumerate(second_bp_names): + second_bp_cords = animal_data[second_bp] + distances = config.framewise_euclidean_distance(location_1=first_bp_cords, location_2=second_bp_cords, px_per_mm=1.0, centimeter=False) + above_criterion_frms.extend(np.argwhere(distances > animal_criterion).flatten()) + unique, counts = np.unique(above_criterion_frms, return_counts=True) + above_criteria_dict[animal_name][first_body_part_name] = np.sort(unique[counts > 1]) + return above_criteria_dict + + def __correct_outliers(df: pd.DataFrame, above_criteria_dict: dict): + for animal_name, animal_data in above_criteria_dict.items(): + for body_part_name, frm_idx in animal_data.items(): + col_names = [f'{body_part_name}_x', f'{body_part_name}_y'] + if len(frm_idx) > 0: + df.loc[frm_idx, col_names] = np.nan + return df.fillna(method='ffill', axis=1).fillna(0) + + video_timer = SimbaTimer(start=True) + _, video_name, _ = get_fn_ext(data_path) + print(f"Processing video {video_name}..") + save_path = os.path.join(save_dir, f"{video_name}.{file_type}") + above_criterion_dict, below_criterion_dict, animal_criteria, bp_dict = {}, {}, {}, {} + df = read_df(data_path, file_type) + for animal_name, animal_bps in outlier_dict.items(): + animal_bp_distances = np.sqrt((df[animal_bps["bp_1"] + "_x"] - df[animal_bps["bp_2"] + "_x"]) ** 2 + (df[animal_bps["bp_1"] + "_y"] - df[animal_bps["bp_2"] + "_y"]) ** 2) + animal_criteria[animal_name] = (animal_bp_distances.mean() * criterion) + for animal_name, animal_bps in animal_bp_dict.items(): + bp_col_names = np.array([[i, j] for i, j in zip(animal_bps["X_bps"], animal_bps["Y_bps"])]).ravel() + animal_arr = df[bp_col_names].to_numpy() + bp_dict[animal_name] = {} + for bp_cnt, bp_col_start in enumerate(range(0, animal_arr.shape[1], 2)): + bp_name = animal_bps["X_bps"][bp_cnt][:-2] + bp_dict[animal_name][bp_name] = animal_arr[:, bp_col_start: bp_col_start + 2] + + above_criteria_dict = __find_location_outliers(bp_dict=bp_dict, animal_criteria=animal_criteria) + df = __correct_outliers(df=df, above_criteria_dict=above_criteria_dict) + write_df(df=df, file_type=file_type, save_path=save_path) + video_timer.stop_timer() + print(f"Corrected location outliers for file {video_name} (elapsed time: {video_timer.elapsed_time_str}s)...") + return video_name, above_criteria_dict, len(df) + +class OutlierCorrecterLocationMultiprocess(ConfigReader, FeatureExtractionMixin): + """ + Detect and amend outliers in pose-estimation data based in the location of the body-parts + in the current frame relative to the location of the body-part in the preceding frame using heuristic rules. + + Uses heuristic rules critera is grabbed from the SimBA project project_config.ini under the [Outlier settings] header. + + .. note:: + `Documentation `_. + + .. image:: _static/img/location_outlier.png + :width: 500 + :align: center + + :param Union[str, os.PathLike] config_path: path to SimBA project config file in Configparser format + :param Optional[Union[str, os.PathLike]] data_dir: The directory storing the input data. If None, then the ``outlier_corrected_movement`` directory of the SimBA project. + :param Optional[Union[str, os.PathLike]] save_dir: The directory to store the results. If None, then the ``outlier_corrected_movement_location`` directory of the SimBA project. + :param Optional[int] core_cnt: The number of cores to use. If -1, then all available cores. Default: -1. + :param Optional[Dict[str, Dict[str, str]]] animal_dict: Dictionary holding the animal names, and the two body-parts to use to measure the mean or median size of the animals. If None, grabs the info from the SimBA project config. + :param Optional[float] criterion: The criterion multiplier. If None, grabs the info from the SimBA project config. + + :example: + >>> _ = OutlierCorrecterLocationMultiprocess(config_path='MyProjectConfig').run() + """ + + def __init__(self, + config_path: Union[str, os.PathLike], + data_dir: Optional[Union[str, os.PathLike]] = None, + save_dir: Optional[Union[str, os.PathLike]] = None, + core_cnt: Optional[int] = -1, + animal_dict: Optional[Dict[str, Dict[str, str]]] = None, + criterion: Optional[float] = None): + + ConfigReader.__init__(self, config_path=config_path, create_logger=False, read_video_info=False) + FeatureExtractionMixin.__init__(self) + if not os.path.exists(self.outlier_corrected_dir): + os.makedirs(self.outlier_corrected_dir) + if criterion is None: + self.criterion = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, ConfigKey.LOCATION_CRITERION.value, Dtypes.FLOAT.value) + else: + check_float(name=f'{criterion} criterion', value=criterion, min_value=10e-10) + self.criterion = criterion + if data_dir is not None: + check_if_dir_exists(in_dir=data_dir, source=self.__class__.__name__) + self.data_dir = data_dir + else: + self.data_dir = self.outlier_corrected_movement_dir + if save_dir is not None: + check_if_dir_exists(in_dir=save_dir, source=self.__class__.__name__) + self.save_dir = save_dir + else: + self.save_dir = self.outlier_corrected_dir + check_int(name=f'{self.__class__.__name__} core_cnt', value=core_cnt, min_value=-1, unaccepted_vals=[0]) + self.core_cnt = core_cnt + if self.core_cnt == -1: + self.core_cnt = find_core_cnt()[0] + + self.above_criterion_dict_dict, self.below_criterion_dict_dict = {},{} + if animal_dict is None: + self.outlier_bp_dict = {} + if self.animal_cnt == 1: + self.animal_id = read_config_entry(self.config, ConfigKey.MULTI_ANIMAL_ID_SETTING.value, ConfigKey.MULTI_ANIMAL_IDS.value, Dtypes.STR.value) + if self.animal_id != "None": + self.animal_bp_dict[self.animal_id] = self.animal_bp_dict.pop("Animal_1") + + for animal_name in self.animal_bp_dict.keys(): + self.outlier_bp_dict[animal_name] = {} + self.outlier_bp_dict[animal_name]["bp_1"] = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, "location_bodypart1_{}".format(animal_name.lower()),"str") + self.outlier_bp_dict[animal_name]["bp_2"] = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, "location_bodypart2_{}".format(animal_name.lower()),"str") + else: + self.outlier_bp_dict = animal_dict + + def run(self): + self.logs, self.frm_cnts = {}, {} + data_paths = find_files_of_filetypes_in_directory(directory=self.data_dir, extensions=[f'.{self.file_type}'], raise_error=True) + data_path_tuples = [(x) for x in data_paths] + with multiprocessing.Pool(self.core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value) as pool: + constants = functools.partial(_location_outlier_corrector, + config=self, + animal_bp_dict=self.animal_bp_dict, + outlier_dict=self.outlier_bp_dict, + save_dir=self.save_dir, + file_type=self.file_type, + criterion=self.criterion) + for cnt, (video_name, above_critera_dict, frm_cnt) in enumerate(pool.imap(constants, data_path_tuples, chunksize=1)): + self.frm_cnts[video_name] = frm_cnt + self.logs[video_name] = above_critera_dict + print(f"Video {video_name} complete...") + self.__save_log_file() + + def __save_log_file(self): + out_df = pd.DataFrame(columns=['VIDEO', 'ANIMAL', 'BODY-PART', 'CORRECTION COUNT', 'CORRECTION RATIO']) + for video_name, video_data in self.logs.items(): + for animal_name, animal_data in video_data.items(): + for bp_name, bp_data in animal_data.items(): + correction_ratio = round(len(bp_data) / self.frm_cnts[video_name], 6) + out_df.loc[len(out_df)] = [video_name, animal_name, bp_name, len(bp_data), correction_ratio] + self.logs_path = os.path.join(self.logs_path, f"Outliers_location_{self.datetime}.csv") + out_df.to_csv(self.logs_path) + self.timer.stop_timer() + stdout_success(msg='Log for corrected "location outliers" saved in project_folder/logs', elapsed_time=self.timer.elapsed_time_str) + +# if __name__ == "__main__": +# test = OutlierCorrecterLocationMultiprocess(config_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\project_config.ini") +# #test = OutlierCorrecterLocationMultiprocess(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini") +# test.run() + + +# test = OutlierCorrecterLocation(config_path='/Users/simon/Desktop/envs/troubleshooting/naresh/project_folder/project_config.ini') +# test.run() + +# test = OutlierCorrecterLocation(config_path='/Users/simon/Desktop/envs/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini') +# test.correct_location_outliers() diff --git a/simba/outlier_tools/outlier_corrector_movement.py b/simba/outlier_tools/outlier_corrector_movement.py index b413706da..9ba3107f2 100644 --- a/simba/outlier_tools/outlier_corrector_movement.py +++ b/simba/outlier_tools/outlier_corrector_movement.py @@ -70,34 +70,24 @@ def __init__(self, @staticmethod @jit(nopython=True) - def __corrector(data=np.ndarray, criterion=float): + def __corrector(data: np.ndarray, criterion: float): results, current_value, cnt = np.full(data.shape, np.nan), data[0, :], 0 for i in range(data.shape[0]): dist = abs(np.linalg.norm(current_value - data[i, :])) if dist <= criterion: current_value = data[i, :] + else: cnt += 1 results[i, :] = current_value return results, cnt def __outlier_replacer(self): for animal_name, animal_body_parts in self.animal_bp_dict.items(): - for bp_x_name, bp_y_name in zip( - animal_body_parts["X_bps"], animal_body_parts["Y_bps"] - ): - vals, cnt = self.__corrector( - data=self.data_df[[bp_x_name, bp_y_name]].values, - criterion=self.animal_criteria[animal_name], - ) + for bp_x_name, bp_y_name in zip(animal_body_parts["X_bps"], animal_body_parts["Y_bps"]): + vals, cnt = self.__corrector(data=self.data_df[[bp_x_name, bp_y_name]].values,criterion=self.animal_criteria[animal_name]) df = pd.DataFrame(vals, columns=[bp_x_name, bp_y_name]) self.data_df.update(df) - self.log.loc[len(self.log)] = [ - self.video_name, - animal_name, - bp_x_name[:-2], - cnt, - round(cnt / len(df), 6), - ] + self.log.loc[len(self.log)] = [self.video_name, animal_name, bp_x_name[:-2], cnt, round(cnt / len(df), 6)] def run(self): """ diff --git a/simba/outlier_tools/outlier_corrector_movement_mp.py b/simba/outlier_tools/outlier_corrector_movement_mp.py new file mode 100644 index 000000000..acda61eaa --- /dev/null +++ b/simba/outlier_tools/outlier_corrector_movement_mp.py @@ -0,0 +1,178 @@ +__author__ = "Simon Nilsson" + +import os +from typing import Union, Optional, Dict + +import numpy as np +import pandas as pd +from numba import jit +import multiprocessing +import functools + +from simba.mixins.config_reader import ConfigReader +from simba.mixins.feature_extraction_mixin import FeatureExtractionMixin +from simba.utils.enums import ConfigKey, Dtypes +from simba.utils.printing import SimbaTimer, stdout_success +from simba.utils.read_write import (get_fn_ext, read_config_entry, read_df, write_df, find_core_cnt, find_files_of_filetypes_in_directory) +from simba.utils.checks import check_int, check_float, check_if_dir_exists + + +def _movement_outlier_corrector(data_path: str, + config: ConfigReader, + animal_bp_dict: dict, + outlier_dict: dict, + file_type: str, + save_dir: str, + criterion: float): + + + @jit(nopython=True) + def _corrector(data: np.ndarray, criterion: float): + results, current_value, cnt = np.full(data.shape, np.nan), data[0, :], 0 + for i in range(data.shape[0]): + dist = abs(np.linalg.norm(current_value - data[i, :])) + if dist <= criterion: + current_value = data[i, :] + else: + cnt += 1 + results[i, :] = current_value + return results, cnt + + + def _outlier_replacer(data_df: pd.DataFrame, + animal_criteria: dict, + video_name: str): + + log = pd.DataFrame(columns=["VIDEO", "ANIMAL", "BODY-PART", "CORRECTION COUNT", "CORRECTION PCT"]) + for animal_name, animal_body_parts in animal_bp_dict.items(): + for bp_x_name, bp_y_name in zip(animal_body_parts["X_bps"], animal_body_parts["Y_bps"]): + vals, cnt = _corrector(data=data_df[[bp_x_name, bp_y_name]].values, criterion=animal_criteria[animal_name]) + df = pd.DataFrame(vals, columns=[bp_x_name, bp_y_name]) + data_df.update(df) + log.loc[len(log)] = [video_name, animal_name, bp_x_name[:-2], cnt, round(cnt / len(df), 6)] + + return data_df, log + + + video_timer = SimbaTimer(start=True) + _, video_name, _ = get_fn_ext(filepath=data_path) + save_path = os.path.join(save_dir, f"{video_name}.{file_type}") + df = read_df(data_path, file_type, check_multiindex=True) + df = config.insert_column_headers_for_outlier_correction(data_df=df, new_headers=config.bp_headers, filepath=data_path) + animal_criteria = {} + for animal_name, animal_bps in outlier_dict.items(): + animal_bp_distances = np.sqrt((df[animal_bps["bp_1"] + "_x"] - df[animal_bps["bp_2"] + "_x"]) ** 2 + (df[animal_bps["bp_1"] + "_y"] - df[animal_bps["bp_2"] + "_y"]) ** 2) + animal_criteria[animal_name] = (animal_bp_distances.mean() * criterion) + df, log = _outlier_replacer(animal_criteria=animal_criteria, data_df=df, video_name=video_name) + write_df(df=df, file_type=file_type, save_path=save_path) + video_timer.stop_timer() + print(f"Corrected movement outliers for file {video_name} (elapsed time: {video_timer.elapsed_time_str}s)...") + + return video_name, log + + + +class OutlierCorrecterMovementMultiProcess(ConfigReader, FeatureExtractionMixin): + """ + Detect and ammend outliers in pose-estimation data based on movement lenghth (Euclidean) of the body-parts + in the current frame from preceeding frame. If not passed, then uses critera stored in the SimBA project project_config.ini + under the [Outlier settings] headed. Uses multiprocessing. + + :param Union[str, os.PathLike] config_path: path to SimBA project config file in Configparser format + :param Optional[Union[str, os.PathLike]] data_dir: The directory storing the input data. If None, then the ``input_csv`` directory of the SimBA project. + :param Optional[Union[str, os.PathLike]] save_dir: The directory to store the results. If None, then the ``outlier_corrected_movement`` directory of the SimBA project. + :param Optional[int] core_cnt: The number of cores to use. If -1, then all available cores. Default: -1. + :param Optional[Dict[str, Dict[str, str]]] animal_dict: Dictionary holding the animal names, and the two body-parts to use to measure the mean or median size of the animals. If None, grabs the info from the SimBA project config. + :param Optional[float] criterion: The criterion multiplier. If None, grabs the info from the SimBA project config. + + .. image:: _static/img/movement_outlier.png + :width: 500 + :align: center + + .. note:: + `Outlier correction documentation `__. + + :example: + >>> outlier_correcter_movement = OutlierCorrecterMovementMultiProcess(config_path='MyProjectConfig') + >>> outlier_correcter_movement.run() + """ + + def __init__(self, + config_path: Union[str, os.PathLike], + data_dir: Optional[Union[str, os.PathLike]] = None, + save_dir: Optional[Union[str, os.PathLike]] = None, + core_cnt: Optional[int] = -1, + animal_dict: Optional[Dict[str, Dict[str, str]]] = None, + criterion: Optional[float] = None): + + ConfigReader.__init__(self, config_path=config_path, create_logger=False) + FeatureExtractionMixin.__init__(self) + if not os.path.exists(self.outlier_corrected_movement_dir): + os.makedirs(self.outlier_corrected_movement_dir) + if criterion is None: + self.criterion = read_config_entry(self.config, ConfigKey.OUTLIER_SETTINGS.value, ConfigKey.MOVEMENT_CRITERION.value, Dtypes.FLOAT.value) + else: + check_float(name=f'{criterion} criterion', value=criterion, min_value=10e-10) + self.criterion = criterion + if data_dir is not None: + check_if_dir_exists(in_dir=data_dir, source=self.__class__.__name__) + self.data_dir = data_dir + else: + self.data_dir = self.input_csv_dir + + if save_dir is not None: + check_if_dir_exists(in_dir=save_dir, source=self.__class__.__name__) + self.save_dir = save_dir + else: + self.save_dir = self.outlier_corrected_movement_dir + + check_int(name=f'{self.__class__.__name__} core_cnt', value=core_cnt, min_value=-1, unaccepted_vals=[0]) + self.core_cnt = core_cnt + if self.core_cnt == -1: + self.core_cnt = find_core_cnt()[0] + + self.outlier_bp_dict, self.above_criterion_dict_dict = {}, {} + if animal_dict is None: + if self.animal_cnt == 1: + self.animal_id = read_config_entry(self.config, ConfigKey.MULTI_ANIMAL_ID_SETTING.value, ConfigKey.MULTI_ANIMAL_IDS.value, Dtypes.STR.value) + if self.animal_id != "None": + self.animal_bp_dict[self.animal_id] = self.animal_bp_dict.pop("Animal_1") + + for animal_name in self.animal_bp_dict.keys(): + self.outlier_bp_dict[animal_name] = {} + self.outlier_bp_dict[animal_name]["bp_1"] = read_config_entry(self.config,"Outlier settings", "movement_bodypart1_{}".format(animal_name.lower()),"str") + self.outlier_bp_dict[animal_name]["bp_2"] = read_config_entry(self.config,"Outlier settings", "movement_bodypart2_{}".format(animal_name.lower()),"str") + else: + self.outlier_bp_dict = animal_dict + + def run(self): + self.logs = [] + data_paths = find_files_of_filetypes_in_directory(directory=self.data_dir, extensions=[f'.{self.file_type}'], raise_error=True) + data_path_tuples = [(x) for x in data_paths] + with multiprocessing.Pool(self.core_cnt, maxtasksperchild=self.maxtasksperchild) as pool: + constants = functools.partial(_movement_outlier_corrector, + config=self, + animal_bp_dict=self.animal_bp_dict, + outlier_dict=self.outlier_bp_dict, + save_dir=self.save_dir, + file_type=self.file_type, + criterion=self.criterion) + for cnt, (video_name, log) in enumerate(pool.imap(constants, data_path_tuples, chunksize=1)): + print(f"Video {video_name} complete...") + self.logs.append(log) + + self.__save_log_file() + + def __save_log_file(self): + log_fn = os.path.join(self.logs_path, f"Outliers_movement_{self.datetime}.csv") + self.logs = pd.concat(self.logs, axis=0) + self.logs.to_csv(log_fn) + self.timer.stop_timer() + stdout_success(msg=f'Log for corrected "movement outliers" saved in {self.logs_path}', elapsed_time=self.timer.elapsed_time_str) + +# +# if __name__ == "__main__": +# #test = OutlierCorrecterMovementMultiProcess(config_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\project_config.ini") +# test = OutlierCorrecterMovementMultiProcess(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini") +# test.run() +# diff --git a/simba/utils/checks.py b/simba/utils/checks.py index d2b5ed234..13eccb469 100644 --- a/simba/utils/checks.py +++ b/simba/utils/checks.py @@ -850,12 +850,12 @@ def check_valid_array(data: np.ndarray, ) if accepted_axis_0_shape is not None: - if data.ndim != 2: - raise ArrayError( - msg=f"Array not of acceptable dimension. Found {data.ndim}, accepted: 2, {source}", - source=check_valid_array.__name__, - ) - elif data.shape[0] not in accepted_axis_0_shape: + # if data.ndim != 2: + # raise ArrayError( + # msg=f"Array not of acceptable dimension. Found {data.ndim}, accepted: 2, {source}", + # source=check_valid_array.__name__, + # ) + if data.shape[0] not in accepted_axis_0_shape: raise ArrayError( msg=f"Array not of acceptable shape. Found {data.shape[0]} rows, accepted: {accepted_axis_0_shape}, {source}", source=check_valid_array.__name__,