From 388ea4032fd822321d17244c7e7e647345c2ba23 Mon Sep 17 00:00:00 2001
From: sronilsson <sronilsson@gmail.com>
Date: Sat, 18 Jan 2025 15:09:10 -0500
Subject: [PATCH] roi_nb

---
 docs/nb/define_rois.ipynb                     | 152 ++++++++++++++++
 docs/notebooks.rst                            |   1 +
 setup.py                                      |   2 +-
 simba/data_processors/cuda/statistics.py      |  11 +-
 .../feature_extraction_supplement_mixin.py    |   5 +-
 simba/mixins/statistics_mixin.py              |  22 ++-
 simba/roi_tools/ROI_define.py                 |  23 +--
 simba/roi_tools/ROI_multiply.py               | 166 ++++++++++--------
 simba/utils/read_write.py                     |   9 +-
 9 files changed, 286 insertions(+), 105 deletions(-)
 create mode 100644 docs/nb/define_rois.ipynb

diff --git a/docs/nb/define_rois.ipynb b/docs/nb/define_rois.ipynb
new file mode 100644
index 000000000..f52b31b92
--- /dev/null
+++ b/docs/nb/define_rois.ipynb
@@ -0,0 +1,152 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "e741f10a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from simba.utils.read_write import find_files_of_filetypes_in_directory\n",
+    "from simba.utils.enums import Options\n",
+    "from simba.roi_tools.ROI_define import ROI_definitions\n",
+    "from simba.roi_tools.ROI_multiply import multiply_ROIs"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "cd6d77d7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# DEFINE THE PATH TO THE SIMBA PROJECT CONFIG, AND THE PATH TO THE DIRECTORY IN SIMBA WHERE THE VIDEOS ARE STORED.\n",
+    "PROJECT_CONFIG_PATH = r\"C:\\troubleshooting\\mitra\\project_folder\\project_config.ini\"\n",
+    "VIDEO_DIR_PATH = r'C:\\troubleshooting\\mitra\\project_folder\\videos'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "6efef9a3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#CREATE A LIST OF PATHS TO THE VIDEO FILES THAT EXIST IN THE SIMBA PROJECT\n",
+    "video_file_paths = find_files_of_filetypes_in_directory(directory=VIDEO_DIR_PATH, extensions=Options.ALL_VIDEO_FORMAT_OPTIONS.value)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "c9d03e3c",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['C:\\\\troubleshooting\\\\mitra\\\\project_folder\\\\videos\\\\501_MA142_Gi_CNO_0514.mp4', 'C:\\\\troubleshooting\\\\mitra\\\\project_folder\\\\videos\\\\501_MA142_Gi_CNO_0516.mp4', 'C:\\\\troubleshooting\\\\mitra\\\\project_folder\\\\videos\\\\501_MA142_Gi_CNO_0521.mp4', 'C:\\\\troubleshooting\\\\mitra\\\\project_folder\\\\videos\\\\501_MA142_Gi_DCZ_0603.mp4', 'C:\\\\troubleshooting\\\\mitra\\\\project_folder\\\\videos\\\\501_MA142_Gi_Saline_0513.mp4']\n"
+     ]
+    }
+   ],
+   "source": [
+    "#WE CAN PRINT IT OUT THE FIRST 5 VIDEO PATHS IN THIS LIST TO SEE HOW IT LOOKS.\n",
+    "print(video_file_paths[:5])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "aa4ad831",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "C:\\Users\\sroni\\.conda\\envs\\simba\\lib\\_collections_abc.py:666: MatplotlibDeprecationWarning:\n",
+      "\n",
+      "The global colormaps dictionary is no longer considered public API.\n",
+      "\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "SIMBA COMPLETE: ROI definitions saved for video: 501_MA142_Gi_CNO_0514 \tcomplete\n"
+     ]
+    }
+   ],
+   "source": [
+    "# WE RUN THE ROI DRAWING INTERFACE AND DRAW ROIs ON THE FIRST VIDEO IN THE LIST.\n",
+    "# ONCE THE ROIs ARE DRAWN ON THIS VIDEO, REMEMBER TO CLICK \"SAVE ROI DATA\", AND THEN CLOSE ALL OPEN THE INTERFACE WINDOWS.\n",
+    "_ = ROI_definitions(config_path=PROJECT_CONFIG_PATH, video_path=video_file_paths[0])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "bc51d1d4",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "SIMBA COMPLETE: ROIs for 501_MA142_Gi_CNO_0514 applied to a further 99 videos (Duplicated rectangles count: 1, circles: 0, polygons: 0). \tcomplete\n",
+      "\n",
+      "Next, click on \"draw\" to modify ROI location(s) or click on \"reset\" to remove ROI drawing(s)\n"
+     ]
+    }
+   ],
+   "source": [
+    "#NEXT, WE MULTIPLY ALL THE ROIs ON THE FIRST VIDEO ON THE LIST ON ALL OTHE VIDEOS IN THE SIMBA PROJECT (THIS PROJECT CONTAINS A TOTAL OF 100 VIDEOS)\n",
+    "multiply_ROIs(config_path=PROJECT_CONFIG_PATH, filename=video_file_paths[0])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "a065f176",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#FINALLY, WE START TO ITERATE OVER ALL OTHER VIDEOS IN THE PROJECT (OMITTING THE FIRST VIDEO), AND CORRECT THE ROIs.\n",
+    "# I DON'T HAVE A GOOD WAY OF AUTMATICALLY OPENING THE NEXT VIDEO ONCE A VIDEO IS CLOSED AT THE MOMENT, \n",
+    "# SO WILL HAVE TO MANUALLY CHANGE `video_file_paths[1]` TO `video_file_paths[2]` etc.\n",
+    "_ = ROI_definitions(config_path=PROJECT_CONFIG_PATH, video_path=video_file_paths[1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8b2a1d89",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.13"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/notebooks.rst b/docs/notebooks.rst
index 3daa05ca5..fb389f4f8 100644
--- a/docs/notebooks.rst
+++ b/docs/notebooks.rst
@@ -59,6 +59,7 @@ Miscellaneous
 
    nb/import_sleap_h5
    nb/multiclass
+   nb/define_rois
 
 
 
diff --git a/setup.py b/setup.py
index a99612582..5b9561748 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@
 # Setup configuration
 setuptools.setup(
     name="simba-uw-tf-dev",
-    version="2.5.2",
+    version="2.5.4",
     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/data_processors/cuda/statistics.py b/simba/data_processors/cuda/statistics.py
index 200cd016b..fe4a2abd9 100644
--- a/simba/data_processors/cuda/statistics.py
+++ b/simba/data_processors/cuda/statistics.py
@@ -532,17 +532,16 @@ def dunn_index(x: np.ndarray, y: np.ndarray) -> float:
     .. seelalso:
        For CPU-based method, use :func:`simba.mixins.statistics_mixin.Statistics.dunn_index`
 
-    .. math::
+    The Dunn Index is given by:
 
-        Dunn\ Index = \frac{\\min_{i \\neq j} \\delta(c_i, c_j)}{\\max_k \\Delta(c_k)}
+    .. math::
+       D = \frac{\min_{i \neq j} \{ \delta(C_i, C_j) \}}{\max_k \{ \Delta(C_k) \}}
 
-    Where:
-    - :math:`\\delta(c_i, c_j)` is the distance between clusters :math:`c_i` and :math:`c_j`.
-    - :math:`\\Delta(c_k)` is the diameter (i.e., maximum intra-cluster distance) of cluster :math:`c_k`.
+    where :math:`\delta(C_i, C_j)` is the distance between clusters :math:`C_i` and :math:`C_j`, and
+    :math:`\Delta(C_k)` is the diameter of cluster :math:`C_k`.
 
     The higher the Dunn Index, the better the clustering, as a higher value indicates that the clusters are well-separated relative to their internal cohesion.
 
-
     .. csv-table::
        :header: EXPECTED RUNTIMES
        :file: ../../../docs/tables/dunn_index_cuda.csv
diff --git a/simba/mixins/feature_extraction_supplement_mixin.py b/simba/mixins/feature_extraction_supplement_mixin.py
index a17f00a3d..f4d4c44bf 100644
--- a/simba/mixins/feature_extraction_supplement_mixin.py
+++ b/simba/mixins/feature_extraction_supplement_mixin.py
@@ -750,13 +750,16 @@ def distance_and_velocity(x: np.ndarray,
         :param x: Array containing movement data. For example, created by ``simba.mixins.FeatureExtractionMixin.framewise_euclidean_distance``. If its a 2-dimensional array, then we assume its pixel coordinates. If it's a 1d array, we assume its frame-wise euclidean distances.
         :param fps: Frames per second of the data.
         :param pixels_per_mm: Conversion factor from pixels to millimeters.
-        :param Optional[bool] centimeters: If True, results are returned in centimeters and centimeters per second. Defaults to True.
+        :param Optional[bool] centimeters: If True, results are returned in centimeters and centimeters per second. Defaults to True. If false, then milimeters and millimeters per second.
         :return: A tuple containing total movement and mean velocity.
         :rtype: Tuple[float, float]
 
         :example:
         >>> x = np.random.randint(0, 100, (100,))
         >>> sum_movement, avg_velocity = FeatureExtractionSupplemental.distance_and_velocity(x=x, fps=10, pixels_per_mm=10, centimeters=True)
+
+        >>> x = np.random.randint(0, 100, (100, 2))
+        >>> sum_movement, avg_velocity = FeatureExtractionSupplemental.distance_and_velocity(x=x, fps=10, pixels_per_mm=10, centimeters=True)
         """
 
         check_valid_array(data=x, source=FeatureExtractionSupplemental.distance_and_velocity.__name__, accepted_ndims=(1, 2), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
diff --git a/simba/mixins/statistics_mixin.py b/simba/mixins/statistics_mixin.py
index 639d8f5a8..9c58b1f89 100644
--- a/simba/mixins/statistics_mixin.py
+++ b/simba/mixins/statistics_mixin.py
@@ -3756,7 +3756,7 @@ def dunn_index(x: np.ndarray, y: np.ndarray, sample: Optional[float] = None) ->
         The Dunn Index is given by:
 
         .. math::
-           D = \\frac{\min_{i \neq j} \{ \delta(C_i, C_j) \}}{\max_k \{ \Delta(C_k) \}}
+           D = \frac{\min_{i \neq j} \{ \delta(C_i, C_j) \}}{\max_k \{ \Delta(C_k) \}}
 
         where :math:`\delta(C_i, C_j)` is the distance between clusters :math:`C_i` and :math:`C_j`, and
         :math:`\Delta(C_k)` is the diameter of cluster :math:`C_k`.
@@ -3983,8 +3983,13 @@ def xie_beni(x: np.ndarray, y: np.ndarray) -> float:
         A lower Xie-Beni index indicates better clustering quality, signifying well-separated and compact clusters.
 
         .. seealso::
-           To compute Xie-Beni on the GPU, use :func:`~simba.mixins.statistics_mixin.Statistics.xie_beni`
+           To compute Xie-Beni on the GPU, use :func:`~simba.mixins.statistics_mixin.Statistics.xie_beni`.
+           Significant GPU savings detected at about 1m features, 25 clusters
 
+        .. math::
+           \\text{XB} = \\frac{\\frac{1}{n} \\sum_{i=1}^{n} \\| x_i - c_{y_i} \\|^2}{\\min_{i \\neq j} \\| c_i - c_j \\|^2}
+
+        where :math:`n` is the total number of points in the dataset, :math:`x_i` is the :math:`i`-th data point, :math:`c_{y_i}` is the centroid of the cluster to which :math:`x_i` belongs, and :math:`\\| \\cdot \\|` denotes the Euclidean norm.
 
         :param np.ndarray x: The dataset as a 2D NumPy array of shape (n_samples, n_features).
         :param np.ndarray y: Cluster labels for each data point as a 1D NumPy array of shape (n_samples,).
@@ -4864,7 +4869,6 @@ def kumar_hassebrook_similarity(self, x: np.ndarray, y: np.ndarray) -> float:
 
     def wave_hedges_distance(self, x: np.ndarray, y: np.ndarray) -> float:
         """
-
         Computes the Wave-Hedges distance between two 1-dimensional arrays `x` and `y`. The Wave-Hedges distance is a measure of dissimilarity between arrays.
 
         .. note::
@@ -4879,6 +4883,9 @@ def wave_hedges_distance(self, x: np.ndarray, y: np.ndarray) -> float:
         >>> x = np.random.randint(0, 500, (1000,))
         >>> y = np.random.randint(0, 500, (1000,))
         >>> Statistics().wave_hedges_distance(x=x, y=y)
+
+        :references:
+           .. [1] Hedges, T. S. (1976). An empirical modification to linear wave theory. Proceedings of the Institution of Civil Engineers, Part 2, 61(3), 575–579. https://doi.org/10.1680/iicep.1976.3408
         """
 
         check_valid_array(data=x, source=f'{Statistics.wave_hedges_distance.__name__} x', accepted_ndims=(1,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
@@ -4907,6 +4914,11 @@ def gower_distance(x: np.ndarray, y: np.ndarray) -> np.ndarray:
         >>> x, y = np.random.randint(0, 500, (1000, 6000)), np.random.randint(0, 500, (1000, 6000))
         >>> Statistics.gower_distance(x=x, y=y)
 
+
+        :references:
+           .. [1] Gower, J. C. (1971). A general coefficient of similarity and some of its properties. Biometrics, 27(4), 857–874. https://doi.org/10.2307/2528823
+
+
         """
         check_valid_array(data=x, source=f'{Statistics.gower_distance.__name__} x', accepted_ndims=(1, 2), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
         check_valid_array(data=y, source=f'{Statistics.gower_distance.__name__} y', accepted_ndims=(x.ndim,), accepted_shapes=(x.shape,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
@@ -4952,6 +4964,10 @@ def normalized_google_distance(x: np.ndarray, y: np.ndarray) -> float:
         :example:
         >>> x, y = np.random.randint(0, 500, (1000,200)), np.random.randint(0, 500, (1000,200))
         >>> Statistics.normalized_google_distance(x=y, y=x)
+
+        :references:
+           .. [1] Cilibrasi, R., & Vitányi, P. (2007). Clustering by compression. IEEE Transactions on Information Theory, 51(4), 1523-1545. https://doi.org/10.1109/TIT.2005.862080
+
         """
         check_valid_array(data=x, source=f'{Statistics.normalized_google_distance.__name__} x', accepted_ndims=(1, 2), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
         check_valid_array(data=y, source=f'{Statistics.normalized_google_distance.__name__} y', accepted_ndims=(x.ndim,), accepted_shapes=(x.shape,), accepted_dtypes=Formats.NUMERIC_DTYPES.value)
diff --git a/simba/roi_tools/ROI_define.py b/simba/roi_tools/ROI_define.py
index ecd6d594c..7eaea3bcc 100644
--- a/simba/roi_tools/ROI_define.py
+++ b/simba/roi_tools/ROI_define.py
@@ -1,9 +1,12 @@
+from typing import Union
 import copy
 import os
 from tkinter import *
 
 import cv2
 import pandas as pd
+import warnings
+warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)
 import PIL.Image
 from PIL import ImageTk
 
@@ -39,17 +42,17 @@ class ROI_definitions(ConfigReader, PopUpMixin):
     video_path: str
         path to video file for which ROIs should be defined.
 
-    Notes
-    ----------
-    `ROI tutorials <https://github.com/sgoldenlab/simba/blob/master/docs/ROI_tutorial_new.md>`__.
+    .. note::
+       `ROI tutorials <https://github.com/sgoldenlab/simba/blob/master/docs/ROI_tutorial_new.md>`__.
 
-    Examples
-    ----------
+    :example:
     >>> _ = ROI_definitions(config_path='MyProjectConfig', video_path='MyVideoPath')
 
     """
 
-    def __init__(self, config_path: str, video_path: str):
+    def __init__(self,
+                 config_path: Union[str, os.PathLike],
+                 video_path: Union[str, os.PathLike]):
 
         check_file_exist_and_readable(file_path=config_path)
         check_file_exist_and_readable(file_path=video_path)
@@ -67,9 +70,6 @@ def __init__(self, config_path: str, video_path: str):
 
         self.menu_icons = get_icons_paths()
 
-        for k in self.menu_icons.keys():
-            self.menu_icons[k]["img"] = ImageTk.PhotoImage(image=PIL.Image.open(os.path.join(os.path.dirname(__file__), self.menu_icons[k]["icon_path"])))
-
         self.roi_root = Toplevel()
         self.roi_root.minsize(WINDOW_SIZE[0], WINDOW_SIZE[1])
         self.screen_width = self.roi_root.winfo_screenwidth()
@@ -79,6 +79,9 @@ def __init__(self, config_path: str, video_path: str):
         self.roi_root.wm_title("Region of Interest Settings")
         self.roi_root.protocol("WM_DELETE_WINDOW", self._terminate)
 
+        for k in self.menu_icons.keys():
+            self.menu_icons[k]["img"] = ImageTk.PhotoImage(image=PIL.Image.open(os.path.join(os.path.dirname(__file__), self.menu_icons[k]["icon_path"])))
+
         self.shape_thickness_list = list(range(1, 26))
         self.ear_tag_size_list = list(range(1, 26))
         self.select_color = "red"
@@ -113,7 +116,7 @@ def __init__(self, config_path: str, video_path: str):
         if len(self.video_ROIs) > 0:
             self.update_delete_ROI_menu()
 
-        self.master.mainloop()
+        #self.master.mainloop()
 
     def _terminate(self):
         self.Exit()
diff --git a/simba/roi_tools/ROI_multiply.py b/simba/roi_tools/ROI_multiply.py
index 8a6ded2e8..89a4dbac1 100644
--- a/simba/roi_tools/ROI_multiply.py
+++ b/simba/roi_tools/ROI_multiply.py
@@ -1,17 +1,19 @@
 __author__ = "Simon Nilsson"
 __email__ = "sronilsson@gmail.com"
 
-
-
-import glob
+from typing import Union
 import os
-
+from copy import deepcopy
 import pandas as pd
+import warnings
+warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)
+
 
-from simba.utils.enums import ConfigKey, Keys, Paths
-from simba.utils.errors import NoROIDataError
+from simba.utils.enums import ConfigKey, Keys, Paths, Options
+from simba.utils.errors import NoROIDataError, NotDirectoryError
 from simba.utils.printing import stdout_success
-from simba.utils.read_write import get_fn_ext, read_config_file
+from simba.utils.read_write import get_fn_ext, read_config_file, find_files_of_filetypes_in_directory
+from simba.utils.checks import check_file_exist_and_readable, check_valid_dataframe
 
 
 def create_emty_df(shape_type):
@@ -64,74 +66,84 @@ def create_emty_df(shape_type):
     return pd.DataFrame(columns=col_list)
 
 
-def multiply_ROIs(config_path, filename):
-    _, CurrVidName, ext = get_fn_ext(filename)
+def multiply_ROIs(config_path: Union[str, os.PathLike],
+                  filename: Union[str, os.PathLike]) -> None:
+
+    """
+    Reproduce ROIs in one video to all other videos in SimBA project.
+
+    :param Union[str, os.PathLike] config_path: Path to SimBA project config file.
+    :param Union[str, os.PathLike] filename: Path to video in project for which ROIs should be duplicated in the other videos in the project
+    :return: None
+
+    :example:
+    >>> multiply_ROIs(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini", filename=r"C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0514.mp4")
+    """
+
+    check_file_exist_and_readable(file_path=config_path)
+    check_file_exist_and_readable(file_path=filename)
+    _, video_name, video_ext = get_fn_ext(filename)
     config = read_config_file(config_path=config_path)
-    projectPath = config.get(
-        ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value
-    )
-    videoPath = os.path.join(projectPath, "videos")
-    ROIcoordinatesPath = os.path.join(projectPath, "logs", Paths.ROI_DEFINITIONS.value)
-
-    if not os.path.isfile(ROIcoordinatesPath):
-        raise NoROIDataError(
-            msg="Cannot multiply ROI definitions: no ROI definitions exist in SimBA project"
-        )
-    rectanglesInfo = pd.read_hdf(ROIcoordinatesPath, key=Keys.ROI_RECTANGLES.value)
-    circleInfo = pd.read_hdf(ROIcoordinatesPath, key=Keys.ROI_CIRCLES.value)
-    polygonInfo = pd.read_hdf(ROIcoordinatesPath, key=Keys.ROI_POLYGONS.value)
-
-    try:
-        r_df = rectanglesInfo[rectanglesInfo["Video"] == CurrVidName]
-    except KeyError:
-        r_df = create_emty_df("rectangle")
-
-    try:
-        c_df = circleInfo.loc[circleInfo["Video"] == str(CurrVidName)]
-    except KeyError:
-        c_df = create_emty_df("circle")
-
-    try:
-        p_df = polygonInfo.loc[polygonInfo["Video"] == str(CurrVidName)]
-    except KeyError:
-        p_df = create_emty_df("polygon")
-
-    if len(r_df) == 0 and len(c_df) == 0 and len(p_df) == 0:
-        print(
-            "Cannot replicate ROIs to all videos: no ROI records exist for "
-            + str(CurrVidName)
-        )
-
-    else:
-        videofilesFound = glob.glob(videoPath + "/*.mp4") + glob.glob(
-            videoPath + "/*.avi"
-        )
-        duplicatedRec, duplicatedCirc, duplicatedPoly = (
-            r_df.copy(),
-            c_df.copy(),
-            p_df.copy(),
-        )
-        for vids in videofilesFound:
-            _, vid_name, ext = get_fn_ext(vids)
-            duplicatedRec["Video"], duplicatedCirc["Video"], duplicatedPoly["Video"] = (
-                vid_name,
-                vid_name,
-                vid_name,
-            )
-            r_df = r_df.append(duplicatedRec, ignore_index=True)
-            c_df = c_df.append(duplicatedCirc, ignore_index=True)
-            p_df = p_df.append(duplicatedPoly, ignore_index=True)
-        r_df = r_df.drop_duplicates(subset=["Video", "Name"], keep="first")
-        c_df = c_df.drop_duplicates(subset=["Video", "Name"], keep="first")
-        p_df = p_df.drop_duplicates(subset=["Video", "Name"], keep="first")
-
-        store = pd.HDFStore(ROIcoordinatesPath, mode="w")
-        store["rectangles"] = r_df
-        store["circleDf"] = c_df
-        store["polygons"] = p_df
-        store.close()
-        stdout_success(msg=f"ROI(s) for {CurrVidName} applied to all videos")
-        print()
-        print(
-            'Next, click on "draw" to modify ROI location(s) or click on "reset" to remove ROI drawing(s)'
-        )
+    project_path = config.get(ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value)
+    videos_dir = os.path.join(project_path, "videos")
+    roi_coordinates_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value)
+    if not os.path.isdir(videos_dir):
+        raise NotDirectoryError(msg=f'Could not find the videos directory in the SimBA project. SimBA expected a directory at location: {videos_dir}')
+    if not os.path.isfile(roi_coordinates_path):
+        raise NoROIDataError(msg=f"Cannot multiply ROI definitions: no ROI definitions exist in SimBA project. Could find find a file at expected location {roi_coordinates_path}", source=multiply_ROIs.__name__)
+
+    with pd.HDFStore(roi_coordinates_path) as hdf: roi_data_keys = [x[1:] for x in hdf.keys()]
+    missing_keys = [x for x in roi_data_keys if x not in [Keys.ROI_RECTANGLES.value, Keys.ROI_CIRCLES.value, Keys.ROI_POLYGONS.value]]
+    if len(missing_keys) > 0:
+        raise NoROIDataError(msg=f'The ROI data file {roi_coordinates_path} is corrupted. Missing the following keys: {missing_keys}', source=multiply_ROIs.__name__)
+
+    rectangles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_RECTANGLES.value)
+    circles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_CIRCLES.value)
+    polygon_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_POLYGONS.value)
+
+    check_valid_dataframe(df=rectangles_df, source=f'{multiply_ROIs.__name__} rectangles_df', required_fields=['Video', 'Name'])
+    check_valid_dataframe(df=circles_df, source=f'{multiply_ROIs.__name__} circles_df', required_fields=['Video', 'Name'])
+    check_valid_dataframe(df=polygon_df, source=f'{multiply_ROIs.__name__} polygon_df', required_fields=['Video', 'Name'])
+
+    videos_w_rectangles = list(rectangles_df["Video"].unique())
+    videos_w_circles = list(circles_df["Video"].unique())
+    videos_w_polygons = list(polygon_df["Video"].unique())
+    videos_w_shapes = list(set(videos_w_rectangles + videos_w_circles + videos_w_polygons))
+    if video_name not in videos_w_shapes:
+        raise NoROIDataError(msg=f"Cannot replicate ROIs to all other videos: no ROI records exist for {video_name}. Create ROIs for for video {video_name}", source=multiply_ROIs.__name__)
+
+    other_video_file_paths = find_files_of_filetypes_in_directory(directory=videos_dir, extensions=Options.ALL_VIDEO_FORMAT_OPTIONS.value)
+    other_video_file_paths = [x for x in other_video_file_paths if x != filename]
+    if len(other_video_file_paths) == 0:
+        raise NoROIDataError(msg=f"Cannot replicate ROIs to other videos. No other videos exist in project {videos_dir} directory.", source=multiply_ROIs.__name__)
+
+    r_df = [create_emty_df(Keys.ROI_RECTANGLES.value) if video_name not in videos_w_rectangles else rectangles_df[rectangles_df["Video"] == video_name]][0]
+    c_df = [create_emty_df(Keys.ROI_CIRCLES.value) if video_name not in videos_w_circles else circles_df[circles_df["Video"] == video_name]][0]
+    p_df = [create_emty_df(Keys.ROI_POLYGONS.value) if video_name not in videos_w_polygons else polygon_df[polygon_df["Video"] == video_name]][0]
+
+    rectangle_results, circle_results, polygon_results = deepcopy(r_df), deepcopy(c_df), deepcopy(p_df)
+    for other_video_file_name in other_video_file_paths:
+        _, other_vid_name, ext = get_fn_ext(other_video_file_name)
+        if len(r_df) > 0:
+            x = deepcopy(r_df); x['Video'] = other_vid_name
+            rectangle_results = pd.concat([rectangle_results, x], axis=0)
+        if len(circle_results) > 0:
+            x = deepcopy(c_df); x['Video'] = other_vid_name
+            circle_results = pd.concat([circle_results, x], axis=0)
+        if len(polygon_results) > 0:
+            x = deepcopy(p_df); x['Video'] = other_vid_name
+            polygon_results = pd.concat([polygon_results, x], axis=0)
+
+    rectangle_results = rectangle_results.drop_duplicates(subset=["Video", "Name"], keep="first")
+    circle_results = circle_results.drop_duplicates(subset=["Video", "Name"], keep="first")
+    polygon_results = polygon_results.drop_duplicates(subset=["Video", "Name"], keep="first")
+
+    store = pd.HDFStore(roi_coordinates_path, mode="w")
+    store[Keys.ROI_RECTANGLES.value] = rectangle_results
+    store[Keys.ROI_CIRCLES.value] = circle_results
+    store[Keys.ROI_POLYGONS.value] = polygon_results
+    store.close()
+    stdout_success(msg=f"ROIs for {video_name} applied to a further {len(other_video_file_paths)} videos (Duplicated rectangles count: {len(r_df)}, circles: {len(c_df)}, polygons: {len(p_df)}).")
+    print('\nNext, click on "draw" to modify ROI location(s) or click on "reset" to remove ROI drawing(s)')
+
+#multiply_ROIs(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini", filename=r"C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0514.mp4")
\ No newline at end of file
diff --git a/simba/utils/read_write.py b/simba/utils/read_write.py
index c0c23457b..fb5dbee91 100644
--- a/simba/utils/read_write.py
+++ b/simba/utils/read_write.py
@@ -835,15 +835,10 @@ def find_files_of_filetypes_in_directory(directory: Union[str, os.PathLike],
     """
 
     try:
-        all_files_in_folder = [
-            f for f in next(os.walk(directory))[2] if not f[0] == "."
-        ]
+        all_files_in_folder = [f for f in next(os.walk(directory))[2] if not f[0] == "."]
     except StopIteration:
         if raise_warning:
-            raise NoFilesFoundError(
-                msg=f"No files found in the {directory} directory with accepted extensions {str(extensions)}",
-                source=find_files_of_filetypes_in_directory.__name__,
-            )
+            raise NoFilesFoundError(msg=f"No files found in the {directory} directory with accepted extensions {str(extensions)}", source=find_files_of_filetypes_in_directory.__name__)
         else:
             all_files_in_folder = []
             pass