diff --git a/simba/mixins/pop_up_mixin.py b/simba/mixins/pop_up_mixin.py index 0f028ec7a..f9a1ce52f 100644 --- a/simba/mixins/pop_up_mixin.py +++ b/simba/mixins/pop_up_mixin.py @@ -9,14 +9,11 @@ from PIL import ImageTk from simba.mixins.config_reader import ConfigReader -from simba.ui.tkinter_functions import (DropDownMenu, Entry_Box, FileSelect, - SimbaButton, hxtScrollbar) -from simba.utils.checks import (check_float, check_instance, check_int, - check_valid_lst) +from simba.ui.tkinter_functions import (DropDownMenu, Entry_Box, FileSelect, SimbaButton, hxtScrollbar) +from simba.utils.checks import (check_float, check_instance, check_int, check_valid_lst) from simba.utils.enums import Formats, Options from simba.utils.errors import CountError, NoFilesFoundError -from simba.utils.lookups import (get_color_dict, get_icons_paths, - get_named_colors) +from simba.utils.lookups import (get_color_dict, get_icons_paths, get_named_colors) from simba.utils.read_write import find_core_cnt diff --git a/simba/roi_tools/ROI_multiply.py b/simba/roi_tools/ROI_multiply.py index e8d70ec22..59d14cf9a 100644 --- a/simba/roi_tools/ROI_multiply.py +++ b/simba/roi_tools/ROI_multiply.py @@ -78,7 +78,7 @@ def multiply_ROIs(config_path: Union[str, os.PathLike], :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 + :return: None. The results are stored in the ``/project_folder/logs/measures\ROI_definitions.h5`` of the SimBA project :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") diff --git a/simba/roi_tools/ROI_reset.py b/simba/roi_tools/ROI_reset.py index 37b35b7ba..23964f625 100644 --- a/simba/roi_tools/ROI_reset.py +++ b/simba/roi_tools/ROI_reset.py @@ -1,101 +1,87 @@ +from typing import Union import os -from configparser import ConfigParser from tkinter import * import pandas as pd - -from simba.utils.enums import ConfigKey, Formats, Keys, Paths +from simba.utils.enums import ConfigKey, Keys, Paths, Links from simba.utils.errors import NoROIDataError from simba.utils.printing import stdout_trash -from simba.utils.read_write import get_fn_ext, read_config_file - - -def reset_video_ROIs(config_path, filename): - _, file_name_wo_ext, VideoExtension = get_fn_ext(filename) - config = ConfigParser() - configFile = str(config_path) - config.read(configFile) - vidInfPath = config.get( - ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value - ) - logFolderPath = os.path.join(vidInfPath, "logs") - ROIcoordinatesPath = os.path.join(logFolderPath, Paths.ROI_DEFINITIONS.value) - if not os.path.isfile(ROIcoordinatesPath): - raise NoROIDataError( - msg="Cannot delete ROI definitions: no definitions exist to delete" - ) - - else: - 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) - store = pd.HDFStore(ROIcoordinatesPath, mode="w") - - try: - rectanglesInfo = rectanglesInfo[rectanglesInfo["Video"] != file_name_wo_ext] - except KeyError: - pass - store["rectangles"] = rectanglesInfo - - try: - circleInfo = circleInfo[circleInfo["Video"] != file_name_wo_ext] - except KeyError: - pass - store["circleDf"] = circleInfo +from simba.utils.read_write import get_fn_ext, read_config_file, remove_files +from simba.utils.checks import check_file_exist_and_readable,check_valid_dataframe +from simba.ui.tkinter_functions import TwoOptionQuestionPopUp + + +def reset_video_ROIs(config_path: Union[str, os.PathLike], + filename: Union[str, os.PathLike]) -> None: + + """ + Delete drawn ROIs for a single video in a 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. The results are stored in the ``/project_folder/logs/measures\ROI_definitions.h5`` of the SimBA project + + :example: + >>> reset_video_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) + project_path = config.get(ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value) + roi_coordinates_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value) + if not os.path.isfile(roi_coordinates_path): + raise NoROIDataError(msg=f"Cannot reset/delete ROI definitions: no ROI definitions exist in SimBA project. Could find find a file at expected location {roi_coordinates_path}. Create ROIs before deleting ROIs.", source=reset_video_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=reset_video_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'{reset_video_ROIs.__name__} rectangles_df', required_fields=['Video']) + check_valid_dataframe(df=circles_df, source=f'{reset_video_ROIs.__name__} circles_df', required_fields=['Video']) + check_valid_dataframe(df=polygon_df, source=f'{reset_video_ROIs.__name__} polygon_df', required_fields=['Video']) + video_rectangle_roi_records = rectangles_df[rectangles_df["Video"] == video_name] + video_circle_roi_records = circles_df[circles_df["Video"] == video_name] + video_polygon_roi_records = polygon_df[polygon_df["Video"] == video_name] + video_roi_cnt = len(video_rectangle_roi_records) + len(video_circle_roi_records) + len(video_polygon_roi_records) + if video_roi_cnt == 0: + raise NoROIDataError(msg=f"Cannot delete ROIs for video {video_name}: no ROI records exist for {video_name}. Create ROIs for for video {video_name} first", source=reset_video_ROIs.__name__) + + store = pd.HDFStore(roi_coordinates_path, mode="w") + store[Keys.ROI_RECTANGLES.value] = rectangles_df[rectangles_df["Video"] != video_name] + store[Keys.ROI_CIRCLES.value] = circles_df[circles_df["Video"] != video_name] + store[Keys.ROI_POLYGONS.value] = polygon_df[polygon_df["Video"] != video_name] + store.close() + stdout_trash(msg=f"Deleted ROI records for video {video_name}. Deleted rectangle count: {len(video_rectangle_roi_records)}, circles: {len(video_circle_roi_records)}, polygons: {len(video_polygon_roi_records)}.") - try: - polygonInfo = polygonInfo[polygonInfo["Video"] != file_name_wo_ext] - except KeyError: - pass - store["polygons"] = polygonInfo +def delete_all_ROIs(config_path: Union[str, os.PathLike]) -> None: + """ + Launches a pop-up asking if to delete all SimBA roi definitions. If click yes, then the ``/project_folder/logs/measures\ROI_definitions.h5`` of the SimBA project is deleted. - print("Deleted ROI record: " + str(file_name_wo_ext)) - store.close() + :param config_path: Path to SimBA project config file. + :return: None + :example: + >>> delete_all_ROIs(config_path=r"C:\troubleshooting\ROI_movement_test\project_folder\project_config.ini") + """ -def delete_all_ROIs(config_path: str): - def delete_file(config_path): + question = TwoOptionQuestionPopUp(title="WARNING!", question="Do you want to delete all defined ROIs in the project?", option_one="YES", option_two="NO", link=Links.ROI.value) + if question.selected_option == "YES": + check_file_exist_and_readable(file_path=config_path) config = read_config_file(config_path=config_path) - project_path = config.get( - ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value - ) - roi_data_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value) - - if not os.path.isfile(roi_data_path): - raise NoROIDataError( - msg=f"No ROI definitions exist in this SimBA project. Expected file at path {roi_data_path}" - ) + project_path = config.get(ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value) + roi_coordinates_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value) + if not os.path.isfile(roi_coordinates_path): + raise NoROIDataError(msg=f"Cannot delete ROI definitions: no ROI definitions exist in SimBA project. Could find find a file at expected location {roi_coordinates_path}. Create ROIs before deleting ROIs.", source=reset_video_ROIs.__name__) else: - os.remove(roi_data_path) - close_window() - stdout_trash( - msg=f"SIMBA COMPLETE: All ROI definitions deleted in this SimBA project ({roi_data_path})" - ) - - def close_window(): - delete_confirm_win.destroy() - delete_confirm_win.update() - - delete_confirm_win = Toplevel() - delete_confirm_win.minsize(200, 200) - - question_frame = LabelFrame( - delete_confirm_win, text="Confirm", font=("Arial", 16, "bold"), padx=5, pady=5 - ) - question_lbl = Label( - question_frame, - text="Do you want to delete all defined ROIs in the project?", - font=Formats.LABELFRAME_HEADER_FORMAT.value, - ) + remove_files(file_paths=[roi_coordinates_path], raise_error=True) + stdout_trash(msg=f"Deleted all ROI records for video for the SimBA project (Deleted file {roi_coordinates_path}). USe the Define ROIs menu to create new ROIs.") + else: + pass - yes_button = Button( - question_frame, text="YES", fg="black", command=lambda: delete_file(config_path) - ) - no_button = Button( - question_frame, text="NO", fg="black", command=lambda: close_window() - ) - question_frame.grid(row=0, sticky=W) - question_lbl.grid(row=1, column=0, sticky=W, pady=10, padx=10) - yes_button.grid(row=2, column=1, sticky=W, pady=10, padx=10) - no_button.grid(row=2, column=2, sticky=W, pady=10, padx=10) +#delete_all_ROIs(config_path=r"C:\troubleshooting\ROI_movement_test\project_folder\project_config.ini") \ No newline at end of file diff --git a/simba/ui/tkinter_functions.py b/simba/ui/tkinter_functions.py index cfeaae7cf..1fd3f3bbb 100644 --- a/simba/ui/tkinter_functions.py +++ b/simba/ui/tkinter_functions.py @@ -363,14 +363,12 @@ class TwoOptionQuestionPopUp(object): :parameter Optional[str] link: If not None, then a link to documentation presenting background info about the user choices. """ - def __init__( - self, - question: str, - option_one: str, - option_two: str, - title: str, - link: Optional[str] = None, - ): + def __init__(self, + question: str, + option_one: str, + option_two: str, + title: str, + link: Optional[str] = None): self.main_frm = Toplevel() self.main_frm.geometry("600x200") @@ -378,28 +376,14 @@ def __init__( question_frm = Frame(self.main_frm) question_frm.pack(expand=True, fill="both") - Label( - question_frm, text=question, font=Formats.LABELFRAME_HEADER_FORMAT.value - ).pack() - button_one = Button( - question_frm, - text=option_one, - fg="blue", - command=lambda: self.run(option_one), - ) - button_two = Button( - question_frm, - text=option_two, - fg="red", - command=lambda: self.run(option_two), - ) + Label(question_frm, text=question, font=Formats.LABELFRAME_HEADER_FORMAT.value).pack() + + button_one = SimbaButton(parent=question_frm, txt=option_one, txt_clr="blue", bg_clr="lightgrey", img='check_blue', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_one}, font=Formats.FONT_LARGE.value) + button_two = SimbaButton(parent=question_frm, txt=option_two, txt_clr="red", bg_clr="lightgrey", img='close', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_two}, font=Formats.FONT_LARGE.value) + #button_one = Button(question_frm, text=option_one, fg="blue", command=lambda: self.run(option_one)) + #button_two = Button(question_frm, text=option_two, fg="red", command=lambda: self.run(option_two)) if link: - link_lbl = Label( - question_frm, - text="Click here for more information.", - cursor="hand2", - fg="blue", - ) + link_lbl = Label(question_frm, text="Click here for more information.", cursor="hand2", fg="blue") link_lbl.bind("", lambda e: callback(link)) link_lbl.place(relx=0.5, rely=0.30, anchor=CENTER) button_one.place(relx=0.5, rely=0.50, anchor=CENTER) diff --git a/simba/utils/read_write.py b/simba/utils/read_write.py index fb5dbee91..7538f836c 100644 --- a/simba/utils/read_write.py +++ b/simba/utils/read_write.py @@ -1702,9 +1702,7 @@ def copy_files_in_directory(in_dir: Union[str, os.PathLike], shutil.copy(file_path, out_dir) -def remove_files( - file_paths: List[Union[str, os.PathLike]], raise_error: Optional[bool] = False -) -> None: +def remove_files(file_paths: List[Union[str, os.PathLike]], raise_error: Optional[bool] = False) -> None: """ Delete (remove) the files specified within a list of filepaths. @@ -1718,10 +1716,7 @@ def remove_files( for file_path in file_paths: if not os.path.isfile(file_path) and raise_error: - raise NoFilesFoundError( - msg=f"Cannot delete {file_path}. File does not exist", - source=remove_files.__name__, - ) + raise NoFilesFoundError(msg=f"Cannot delete {file_path}. File does not exist", source=remove_files.__name__) elif not os.path.isfile(file_path): pass else: @@ -1729,10 +1724,7 @@ def remove_files( os.remove(file_path) except: if raise_error: - raise PermissionError( - msg=f"Cannot read {file_path}. Is the file open in an alternative app?", - source=remove_files.__name__, - ) + raise PermissionError(msg=f"Cannot read {file_path}. Is the file open in an alternative app?", source=remove_files.__name__) else: pass