From 6a9f88621474bf350578bffe9865e0544d985553 Mon Sep 17 00:00:00 2001 From: sronilsson Date: Mon, 5 Aug 2024 19:03:51 -0400 Subject: [PATCH] labelling --- setup.py | 2 +- simba/SimBA.py | 4 +- .../labelling/labelling_advanced_interface.py | 26 +-- simba/labelling/labelling_interface.py | 59 ++--- simba/labelling/play_annotation_video.py | 215 +++++++----------- simba/mixins/plotting_mixin.py | 2 +- .../ui/pop_ups/clf_add_remove_print_pop_up.py | 100 +++----- simba/utils/enums.py | 2 +- simba/utils/lookups.py | 51 ++++- 9 files changed, 206 insertions(+), 255 deletions(-) diff --git a/setup.py b/setup.py index e2cade522..aa6864889 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ # Setup configuration setuptools.setup( name="Simba-UW-tf-dev", - version="1.99.9", + version="2.0.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/SimBA.py b/simba/SimBA.py index fa0e38beb..c9010e2d3 100644 --- a/simba/SimBA.py +++ b/simba/SimBA.py @@ -353,10 +353,10 @@ def activate(box, *args): feature_tools_frm = LabelFrame(tab5, text="FEATURE TOOLS", pady=5, font=Formats.FONT_HEADER.value) - compute_feature_subset_btn = SimbaButton(parent=feature_tools_frm, txt="CALCULATE FEATURE SUBSETS", txt_clr='blue', font=Formats.FONT_REGULAR.value, cmd=FeatureSubsetExtractorPopUp, cmd_kwargs={'config_path': lambda:self.config_path}, thread=False) + compute_feature_subset_btn = SimbaButton(parent=feature_tools_frm, txt="CALCULATE FEATURE SUBSETS", txt_clr='blue', font=Formats.FONT_REGULAR.value, cmd=FeatureSubsetExtractorPopUp, cmd_kwargs={'config_path': lambda: self.config_path}, thread=False) label_behavior_frm = CreateLabelFrameWithIcon(parent=tab7, header="LABEL BEHAVIOR", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.LABEL_BEHAVIOR.value) - select_video_btn_new = SimbaButton(parent=label_behavior_frm, txt="Select video (create new video annotation)", img='label_blue', txt_clr='navy', cmd=select_labelling_video, cmd_kwargs={'config_path': lambda:self.config_path, 'threshold_dict': lambda:None, 'setting': lambda: "from_scratch", lambda: 'continuing': False}, thread=False) + select_video_btn_new = SimbaButton(parent=label_behavior_frm, txt="Select video (create new video annotation)", img='label_blue', txt_clr='navy', cmd=select_labelling_video, cmd_kwargs={'config_path': lambda :self.config_path, 'threshold_dict': lambda: None, 'setting': lambda: "from_scratch", 'continuing': lambda: False}, thread=False) select_video_btn_continue = SimbaButton(parent=label_behavior_frm, txt="Select video (continue existing video annotation)", img='label_yellow', txt_clr='darkgoldenrod', cmd=select_labelling_video, cmd_kwargs={'config_path': lambda: self.config_path, 'threshold_dict': lambda:None, 'setting': lambda:None, 'continuing': lambda:True}, thread=False) label_thirdpartyann = CreateLabelFrameWithIcon(parent=tab7, header="IMPORT THIRD-PARTY BEHAVIOR ANNOTATIONS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.THIRD_PARTY_ANNOTATION.value) diff --git a/simba/labelling/labelling_advanced_interface.py b/simba/labelling/labelling_advanced_interface.py index 0c851dbb1..c26a79ceb 100644 --- a/simba/labelling/labelling_advanced_interface.py +++ b/simba/labelling/labelling_advanced_interface.py @@ -15,13 +15,12 @@ from simba.mixins.config_reader import ConfigReader from simba.utils.checks import check_file_exist_and_readable, check_int from simba.utils.enums import Formats, Options, TagNames -from simba.utils.errors import (AdvancedLabellingError, FrameRangeError, - NoDataError) -from simba.utils.lookups import get_labelling_img_kbd_bindings +from simba.utils.errors import (AdvancedLabellingError, FrameRangeError, NoDataError) +from simba.utils.lookups import get_labelling_img_kbd_bindings, get_labelling_video_kbd_bindings from simba.utils.printing import log_event, stdout_success from simba.utils.read_write import (get_all_clf_names, get_fn_ext, get_video_meta_data, read_config_entry, - read_df, write_df) + read_df) class AdvancedLabellingInterface(ConfigReader): @@ -57,7 +56,9 @@ def __init__(self, config_path: str, file_path: str, continuing: bool): self.machine_results_file_path = os.path.join(self.machine_results_dir, self.video_name + "." + self.file_type) self.cap = cv2.VideoCapture(self.video_path) self.img_kbd_bindings = get_labelling_img_kbd_bindings() + self.video_kbd_bindings = get_labelling_video_kbd_bindings() self.__create_key_presses_lbl() + self.__create_video_key_presses_lbl() self.video_meta_data = get_video_meta_data(video_path=self.video_path) self.frame_lst = list(range(0, self.video_meta_data["frame_count"])) self.max_frm_no = max(self.frame_lst) @@ -179,14 +180,7 @@ def __init__(self, config_path: str, file_path: str, continuing: bool): self.video_player_frm.grid(row=0, column=2, sticky=N) self.play_video_btn = Button(self.video_player_frm, text="Open Video", command=self.play_video) self.play_video_btn.grid(sticky=N, pady=10) - self.video_key_lbls = Label(self.video_player_frm, - text="\n\n Keyboard shortcuts for video navigation: \n p = Pause/Play" - "\n\n After pressing pause:" - "\n o = +2 frames \n e = +10 frames \n w = +1 second" - "\n\n t = -2 frames \n s = -10 frames \n x = -1 second" - "\n\n q = Close video window \n\n") - - self.video_key_lbls.grid(sticky=W) + Label(self.video_player_frm, text=self.video_presses_lbl).grid(sticky=W) self.update_img_from_video = Button(self.video_player_frm, text="Show current video frame", command=self.update_frame_from_video) self.update_img_from_video.grid(sticky=N) self.bind_shortcut_keys() @@ -209,6 +203,12 @@ def bind_shortcut_keys(self): self.main_window.bind(self.img_kbd_bindings['last_frame']['kbd'], lambda x: self.advance_frame(new_frm_number=self.max_frm_no)) self.main_window.bind(self.img_kbd_bindings['first_frame']['kbd'], lambda x: self.advance_frame(0)) + def __create_video_key_presses_lbl(self): + self.video_presses_lbl = "\n\n Keyboard shortcuts for video navigation: " + for k, v in self.video_kbd_bindings.items(): + self.video_presses_lbl += '\n' + self.video_presses_lbl += v['label'] + def find_last_next_annotation(self, forwards: bool, present: bool): if forwards: sliced = self.data_df_targets.loc[self.current_frm_n.get() + 1 :,].sum( @@ -558,5 +558,5 @@ def select_labelling_video_advanced( ) # # -# test = select_labelling_video_advanced(config_path='/Users/simon/Desktop/envs/simba/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini', +# test = select_labelling_video_advanced(config_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\project_config.ini", # continuing=False) diff --git a/simba/labelling/labelling_interface.py b/simba/labelling/labelling_interface.py index 50e25e1a9..42425b832 100644 --- a/simba/labelling/labelling_interface.py +++ b/simba/labelling/labelling_interface.py @@ -22,11 +22,9 @@ check_int, check_that_column_exist) from simba.utils.enums import Options, TagNames from simba.utils.errors import FrameRangeError, NoDataError -from simba.utils.lookups import get_labelling_img_kbd_bindings +from simba.utils.lookups import get_labelling_img_kbd_bindings, get_labelling_video_kbd_bindings from simba.utils.printing import log_event, stdout_success -from simba.utils.read_write import (get_all_clf_names, get_fn_ext, - get_video_meta_data, read_config_entry, - read_df, write_df) +from simba.utils.read_write import (get_all_clf_names, get_fn_ext, get_video_meta_data, read_config_entry, read_df, write_df) PLAY_VIDEO_SCRIPT_PATH = os.path.join(os.path.dirname(simba.__file__), "labelling/play_annotation_video.py") PADDING = 5 @@ -72,7 +70,9 @@ def __init__(self, self.video_path = file_path self.cap = cv2.VideoCapture(self.video_path) self.img_kbd_bindings = get_labelling_img_kbd_bindings() - self.__create_key_presses_lbl() + self.video_kbd_bindings = get_labelling_video_kbd_bindings() + self.__create_frm_key_presses_lbl() + self.__create_video_key_presses_lbl() self.video_meta_data = get_video_meta_data(video_path=self.video_path) self.frame_lst = list(range(0, self.video_meta_data["frame_count"])) self.max_frm_no = max(self.frame_lst) @@ -188,29 +188,30 @@ def __init__(self, self.video_player_frm.grid(row=0, column=2, sticky=N) self.play_video_btn = Button(self.video_player_frm, text="Open Video", command=self.__play_video) self.play_video_btn.grid(sticky=N, pady=10) - self.video_key_lbls = Label( - self.video_player_frm, - text="\n\n Keyboard shortcuts for video navigation: \n p = Pause/Play" - "\n\n After pressing pause:" - "\n o = +2 frames \n e = +10 frames \n w = +1 second" - "\n\n t = -2 frames \n s = -10 frames \n x = -1 second" - "\n\n q = Close video window \n\n", - ) - self.video_key_lbls.grid(sticky=W) + Label(self.video_player_frm, text=self.video_presses_lbl).grid(sticky=W) self.update_img_from_video = Button(self.video_player_frm, text="Show current video frame", command=self.__update_frame_from_video) self.update_img_from_video.grid(sticky=N) - self.__bind_shortcut_keys() + self.__bind_frm_shortcut_keys() + + Label(self.video_player_frm, text=self.key_presses_lbl).grid(sticky=S) self.__advance_frame(new_frm_number=self.frm_no) self.main_window.mainloop() - def __create_key_presses_lbl(self): + def __create_frm_key_presses_lbl(self): self.key_presses_lbl = "\n\n Keyboard shortcuts for frame navigation: " for k, v in self.img_kbd_bindings.items(): self.key_presses_lbl += '\n' self.key_presses_lbl += v['label'] - def __bind_shortcut_keys(self): + def __create_video_key_presses_lbl(self): + self.video_presses_lbl = "\n\n Keyboard shortcuts for video navigation: " + for k, v in self.video_kbd_bindings.items(): + self.video_presses_lbl += '\n' + self.video_presses_lbl += v['label'] + + + def __bind_frm_shortcut_keys(self): self.main_window.bind(self.img_kbd_bindings['save']['kbd'], lambda x: self.__save_results()) self.main_window.bind(self.img_kbd_bindings['frame+1_keep_choices']['kbd'], lambda x: self.__advance_frame(new_frm_number=int(self.current_frm_n.get() + 1), save_and_keep_checks=True)) self.main_window.bind(self.img_kbd_bindings['print_annotation_statistic']['kbd'], lambda x: self.print_annotation_statistics()) @@ -219,6 +220,9 @@ def __bind_shortcut_keys(self): self.main_window.bind(self.img_kbd_bindings['last_frame']['kbd'], lambda x: self.__advance_frame(new_frm_number=self.max_frm_no)) self.main_window.bind(self.img_kbd_bindings['first_frame']['kbd'], lambda x: self.__advance_frame(0)) + + + def print_annotation_statistics(self): table_view = [["Video name", self.video_name], ["Video frames", self.video_meta_data["frame_count"]]] for target in self.target_lst: @@ -263,17 +267,17 @@ def __advance_frame(self, new_frm_number: int, save_and_keep_checks=False): print("FRAME {} CANNOT BE SHOWN - YOU ARE VIEWING THE FINAL FRAME OF THE VIDEO (FRAME NUMBER {})".format(str(new_frm_number), str(self.max_frm_no))) self.current_frm_n = IntVar(value=self.max_frm_no) self.change_frm_box.delete(0, END) - self.change_frm_box.insert(0, self.current_frm_n.get()) + self.change_frm_box.insert(0, str(self.current_frm_n.get())) elif new_frm_number < 0: print("FRAME {} CANNOT BE SHOWN - YOU ARE VIEWING THE FIRST FRAME OF THE VIDEO (FRAME NUMBER {})".format(str(new_frm_number), str(self.max_frm_no))) self.current_frm_n = IntVar(value=0) self.change_frm_box.delete(0, END) - self.change_frm_box.insert(0, self.current_frm_n.get()) + self.change_frm_box.insert(0, str(self.current_frm_n.get())) else: self.__create_print_statements() self.current_frm_n = IntVar(value=new_frm_number) self.change_frm_box.delete(0, END) - self.change_frm_box.insert(0, self.current_frm_n.get()) + self.change_frm_box.insert(0, str(self.current_frm_n.get())) if not save_and_keep_checks: for target in self.target_lst: self.checkboxes[target]["var"].set(bool(self.data_df_targets[target].loc[int(self.current_frm_n.get())])) @@ -351,13 +355,12 @@ def __create_print_statements( ) -def select_labelling_video( - config_path: Union[str, os.PathLike], - threshold_dict: Optional[Dict[str, float]] = None, - setting: str = None, - continuing: bool = None, - video_file_path=Union[str, os.PathLike], -): +def select_labelling_video(config_path: Union[str, os.PathLike], + threshold_dict: Optional[Dict[str, float]] = None, + setting: str = None, + continuing: bool = None, + video_file_path=Union[str, os.PathLike]): + if setting is not "pseudo": video_file_path = filedialog.askopenfilename( filetypes=[("Video files", Options.ALL_VIDEO_FORMAT_STR_OPTIONS.value)] @@ -383,7 +386,7 @@ def select_labelling_video( ) -# test = select_labelling_video(config_path='/Users/simon/Desktop/envs/simba/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini', +# test = select_labelling_video(config_path=r"C:\troubleshooting\two_black_animals_14bp\project_folder\project_config.ini", # threshold_dict={'Attack': 0.4}, # setting='from_scratch', # continuing=False) diff --git a/simba/labelling/play_annotation_video.py b/simba/labelling/play_annotation_video.py index e1d0d0f0f..4d7856e3f 100644 --- a/simba/labelling/play_annotation_video.py +++ b/simba/labelling/play_annotation_video.py @@ -3,13 +3,15 @@ import os import signal import sys - import cv2 +import numpy as np from simba.utils.checks import check_file_exist_and_readable from simba.utils.enums import TextOptions -from simba.utils.read_write import get_video_meta_data +from simba.utils.read_write import get_video_meta_data, read_frm_of_video, get_fn_ext from simba.utils.warnings import FrameRangeWarning +from simba.mixins.plotting_mixin import PlottingMixin +from simba.utils.lookups import get_labelling_video_kbd_bindings def annotation_video_player(): @@ -22,51 +24,48 @@ def labelling_log_writer(frame_number: int) -> None: f.flush() os.fsync(f.fileno()) - def print_video_txt(frame_number: int, video_info: dict) -> None: - current_time = round((frame_number / video_info["fps"]), 2) - cv2.putText( - frame, - f"F~ {frame_number}", - ( - TextOptions.BORDER_BUFFER_X.value, - int((video_info["height"] - spacing_scale)), - ), - TextOptions.FONT.value, - font_size, - TextOptions.COLOR.value, - TextOptions.TEXT_THICKNESS.value + 1, - ) - cv2.putText( - frame, - f"T~ {current_time}", - ( - TextOptions.BORDER_BUFFER_X.value, - int((video_info["height"] - spacing_scale * 2)), - ), - TextOptions.FONT.value, - font_size, - TextOptions.COLOR.value, - TextOptions.TEXT_THICKNESS.value + 1, - ) - video_path = sys.stdin.readline().encode().decode() check_file_exist_and_readable(file_path=video_path) project_dir = os.path.dirname(os.path.dirname(video_path)) cap = cv2.VideoCapture(video_path) + _, video_name, _ = get_fn_ext(filepath=video_path) video_meta_data = get_video_meta_data(video_path=video_path) - space_scale, res_scale, font_scale = 60, 1500, 2 - max_dim = max(video_meta_data["width"], video_meta_data["height"]) - font_size = float(font_scale / (res_scale / max_dim)) - spacing_scale = int(space_scale / (res_scale / max_dim)) - + font_size, space_x, spacing_scale = PlottingMixin().get_optimal_font_scales(text='999999', accepted_px_width=int(video_meta_data["width"] / 8), accepted_px_height=int(video_meta_data["width"] / 8)) + kbd_bindings = get_labelling_video_kbd_bindings() f = open(os.path.join(project_dir, "labelling_info.txt"), "w") time_between_frames = int(1000 / video_meta_data["fps"]) - cv2.namedWindow("Video", cv2.WINDOW_NORMAL) + cv2.namedWindow(video_name, cv2.WINDOW_NORMAL) + + + def print_video_txt(frame: np.ndarray, + frame_number: int, + video_info: dict) -> np.ndarray: + + current_time = round((frame_number / video_info["fps"]), 2) + frame = PlottingMixin().put_text(img=frame, text=f"F~ {frame_number}", pos=(TextOptions.BORDER_BUFFER_X.value, int((video_info["height"] - spacing_scale))), font_size=font_size, font_thickness=TextOptions.TEXT_THICKNESS.value + 1, text_bg_alpha=0.6, text_color=(255, 255, 255)) + frame = PlottingMixin().put_text(img=frame, text=f"T~ {current_time}", pos=(TextOptions.BORDER_BUFFER_X.value, int((video_info["height"] - spacing_scale * 2))), font_size=font_size, font_thickness=TextOptions.TEXT_THICKNESS.value + 1, text_bg_alpha=0.6, text_color=(255, 255, 255)) + return frame + + + pause_key = kbd_bindings['Pause/Play']['kbd'] + f_2_key = kbd_bindings['forward_two_frames']['kbd'] + f_10_key = kbd_bindings['forward_ten_frames']['kbd'] + f_1s_key = kbd_bindings['forward_one_second']['kbd'] + b_2_key = kbd_bindings['backwards_two_frames']['kbd'] + b_10_key = kbd_bindings['backwards_ten_frames']['kbd'] + b_1s_key = kbd_bindings['backwards_one_second']['kbd'] + close = kbd_bindings['close_window']['kbd'] + + while True: ret, frame = cap.read() + if frame is None: + cap.release() + cv2.destroyAllWindows() + break key = cv2.waitKey(time_between_frames) & 0xFF - if key == ord("p"): ### THE VIDEO IS PAUSED + if (isinstance(pause_key, int) and key == pause_key) or (isinstance(pause_key, str) and key == ord(pause_key)): while True: second_key = cv2.waitKey(1) current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) @@ -75,137 +74,85 @@ def print_video_txt(frame_number: int, video_info: dict) -> None: f.truncate() f.flush() os.fsync(f.fileno()) - if second_key == ord("t"): ## BACK UP TWO FRAME - if (current_video_position - 2) < 0: - FrameRangeWarning( - msg=f"FRAME {current_video_position - 2} CANNOT BE SHOWN", - source=annotation_video_player.__name__, - ) + if (isinstance(b_2_key, int) and second_key == b_2_key) or (isinstance(b_2_key, str) and second_key == ord(b_2_key)): + if (current_video_position - 3) < 0: + FrameRangeWarning(msg=f"FRAME {current_video_position - 3} CANNOT BE SHOWN", source=annotation_video_player.__name__) else: - cap.set(cv2.CAP_PROP_POS_FRAMES, (current_video_position - 2)) - ret, frame = cap.read() - current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + current_video_position = current_video_position - 3 + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) + frame = print_video_txt(frame=frame, frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - elif second_key == ord("s"): ### BACK UP TEN FRAME + elif (isinstance(b_10_key, int) and second_key == b_10_key) or (isinstance(b_10_key, str) and second_key == ord(b_10_key)): if (current_video_position - 11) < 0: - FrameRangeWarning( - msg=f"FRAME {current_video_position - 11} CANNOT BE SHOWN", - source=annotation_video_player.__name__, - ) + FrameRangeWarning(msg=f"FRAME {current_video_position - 11} CANNOT BE SHOWN", source=annotation_video_player.__name__) else: - cap.set(cv2.CAP_PROP_POS_FRAMES, (current_video_position - 11)) - ret, frame = cap.read() - current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + current_video_position = current_video_position - 11 + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) + frame = print_video_txt(frame=frame,frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - elif second_key == ord("x"): ### BACK UP 1s + elif (isinstance(b_1s_key, int) and second_key == b_1s_key) or (isinstance(b_1s_key, str) and second_key == ord(b_1s_key)): if (current_video_position - video_meta_data["fps"]) < 0: - FrameRangeWarning( - msg=f'FRAME {current_video_position - video_meta_data["fps"]} CANNOT BE SHOWN', - source=annotation_video_player.__name__, - ) + FrameRangeWarning(msg=f'FRAME {current_video_position - video_meta_data["fps"]} CANNOT BE SHOWN', source=annotation_video_player.__name__) else: - cap.set( - cv2.CAP_PROP_POS_FRAMES, - (current_video_position - video_meta_data["fps"]), - ) - ret, frame = cap.read() + current_video_position = int(current_video_position - video_meta_data["fps"]) + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + frame = print_video_txt(frame=frame,frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - elif second_key == ord("w"): ### FORWARD 1s - if ( - current_video_position + video_meta_data["fps"] - ) > video_meta_data["frame_count"]: - FrameRangeWarning( - msg=f'FRAME {current_video_position + video_meta_data["fps"]} CANNOT BE SHOWN', - source=annotation_video_player.__name__, - ) + elif (isinstance(f_1s_key, int) and second_key == f_1s_key) or (isinstance(f_1s_key, str) and second_key == ord(f_1s_key)): + if (current_video_position + video_meta_data["fps"]) > video_meta_data["frame_count"]: + FrameRangeWarning(msg=f'FRAME {current_video_position + video_meta_data["fps"]} CANNOT BE SHOWN', source=annotation_video_player.__name__) else: - cap.set( - cv2.CAP_PROP_POS_FRAMES, - (current_video_position + video_meta_data["fps"]), - ) - ret, frame = cap.read() - current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + current_video_position = int(current_video_position + video_meta_data["fps"]) + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) + frame = print_video_txt(frame=frame,frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - elif second_key == ord("o"): ### FORWARD TWO FRAMES + elif (isinstance(f_2_key, int) and second_key == f_2_key) or (isinstance(f_2_key, str) and second_key == ord(f_2_key)): if (current_video_position + 1) > video_meta_data["frame_count"]: - FrameRangeWarning( - msg=f"FRAME {current_video_position + 1} CANNOT BE SHOWN", - source=annotation_video_player.__name__, - ) + FrameRangeWarning( msg=f"FRAME {current_video_position + 1} CANNOT BE SHOWN", source=annotation_video_player.__name__) else: - cap.set(cv2.CAP_PROP_POS_FRAMES, (current_video_position + 1)) - ret, frame = cap.read() - current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + current_video_position = int(current_video_position + 1) + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) + frame = print_video_txt(frame=frame,frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - - elif second_key == ord("e"): ### FORWARD TEN FRAMES + elif (isinstance(f_10_key, int) and second_key == f_10_key) or (isinstance(f_10_key, str) and second_key == ord(f_10_key)): if (current_video_position + 9) > video_meta_data["frame_count"]: - FrameRangeWarning( - msg=f"FRAME {current_video_position + 9} CANNOT BE SHOWN", - source=annotation_video_player.__name__, - ) + FrameRangeWarning(msg=f"FRAME {current_video_position + 9} CANNOT BE SHOWN", source=annotation_video_player.__name__) else: - cap.set(cv2.CAP_PROP_POS_FRAMES, (current_video_position + 9)) - ret, frame = cap.read() - current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt( - frame_number=current_video_position, - video_info=video_meta_data, - ) - cv2.imshow("Video", frame) + current_video_position = int(current_video_position + 9) + frame = read_frm_of_video(video_path=cap, frame_index=current_video_position) + frame = print_video_txt(frame=frame, frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) labelling_log_writer(frame_number=current_video_position) - elif second_key == ord("p"): + elif (isinstance(pause_key, int) and second_key == pause_key) or (isinstance(pause_key, str) and second_key == ord(pause_key)): break - if (second_key == ord("q")) | (cv2.getWindowProperty("Video", 1) == -1): + if (isinstance(close, int) and second_key == close) or (isinstance(close, str) and second_key == ord(close)) or (cv2.getWindowProperty(video_name, 1) == -1): cap.release() cv2.destroyAllWindows() path = os.path.join(project_dir, "subprocess.txt") txtFile = open(path) line = txtFile.readline() - if second_key == ord("q"): + if (isinstance(close, int) and second_key == close) or (isinstance(close, str) and second_key == ord(close)): os.kill(int(line), signal.SIGTERM) break else: try: os.kill(int(line), signal.SIGTERM) except OSError: - print( - "OSError: Cannot save/read latest image file CSV. Please try again" - ) + print("OSError: Cannot save/read latest image file CSV. Please try again") current_video_position = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - print_video_txt(frame_number=current_video_position, video_info=video_meta_data) - cv2.imshow("Video", frame) - if key == ord("q"): + frame = print_video_txt(frame=frame, frame_number=current_video_position, video_info=video_meta_data) + cv2.imshow(video_name, frame) + if (isinstance(close, int) and key == close) or (isinstance(close, str) and key == ord(close)): break - if cv2.getWindowProperty("Video", 1) == -1: + if cv2.getWindowProperty(video_name, 1) == -1: break cap.release() diff --git a/simba/mixins/plotting_mixin.py b/simba/mixins/plotting_mixin.py index 2b4f4afcb..05dcd0661 100644 --- a/simba/mixins/plotting_mixin.py +++ b/simba/mixins/plotting_mixin.py @@ -1826,7 +1826,7 @@ def put_text(self, img: np.ndarray, text: str, pos: Tuple[int, int], - font_size: int, + font_size: Union[int, float], font_thickness: Optional[int] = 2, font: Optional[int] = cv2.FONT_HERSHEY_DUPLEX, text_color: Optional[Tuple[int, int, int]] = (255, 255, 255), diff --git a/simba/ui/pop_ups/clf_add_remove_print_pop_up.py b/simba/ui/pop_ups/clf_add_remove_print_pop_up.py index d73c226ec..f33c5edce 100644 --- a/simba/ui/pop_ups/clf_add_remove_print_pop_up.py +++ b/simba/ui/pop_ups/clf_add_remove_print_pop_up.py @@ -7,116 +7,70 @@ from simba.mixins.config_reader import ConfigReader from simba.mixins.pop_up_mixin import PopUpMixin from simba.pose_processors.pose_reset import PoseResetter -from simba.ui.tkinter_functions import (CreateLabelFrameWithIcon, DropDownMenu, - Entry_Box, FileSelect, SimbaButton, - TwoOptionQuestionPopUp) +from simba.ui.tkinter_functions import (CreateLabelFrameWithIcon, DropDownMenu, Entry_Box, FileSelect, SimbaButton, TwoOptionQuestionPopUp) from simba.utils.checks import check_str from simba.utils.enums import ConfigKey, Keys, Links from simba.utils.printing import stdout_success, stdout_trash from simba.utils.read_write import tabulate_clf_info +from simba.utils.errors import DuplicationError class AddClfPopUp(PopUpMixin, ConfigReader): def __init__(self, config_path: Union[str, os.PathLike]): - PopUpMixin.__init__(self, config_path=config_path, title="ADD CLASSIFIER") - ConfigReader.__init__(self, config_path=config_path, read_video_info=False) .clf_eb = Entry_Box(self.main_frm, "CLASSIFIER NAME", "15") - add_btn = Button(self.main_frm, text="ADD CLASSIFIER", command=lambda: self.run()) + ConfigReader.__init__(self, config_path=config_path, read_video_info=False) + self.clf_eb = Entry_Box(self.main_frm, "CLASSIFIER NAME:", "15") + add_btn = SimbaButton(parent=self.main_frm, txt="ADD CLASSIFIER", cmd=self.run, img='rocket') self.clf_eb.grid(row=0, column=0, sticky=NW) add_btn.grid(row=1, column=0, sticky=NW) def run(self): clf_name = self.clf_eb.entry_get.strip() check_str(name="CLASSIFIER NAME", value=clf_name) - self.config.set( - ConfigKey.SML_SETTINGS.value, - ConfigKey.TARGET_CNT.value, - str(self.clf_cnt + 1), - ) - self.config.set( - ConfigKey.SML_SETTINGS.value, f"model_path_{str(self.clf_cnt + 1)}", "" - ) - self.config.set( - ConfigKey.SML_SETTINGS.value, - f"target_name_{str(self.clf_cnt + 1)}", - clf_name, - ) - self.config.set( - ConfigKey.THRESHOLD_SETTINGS.value, - f"threshold_{str(self.clf_cnt + 1)}", - "None", - ) - self.config.set( - ConfigKey.MIN_BOUT_LENGTH.value, f"min_bout_{str(self.clf_cnt + 1)}", "None" - ) + if clf_name in self.clf_names: + raise DuplicationError(msg=f'The classifier name {clf_name} already exist in the SimBA project.', source=self.__class__.__name__) + self.config.set( ConfigKey.SML_SETTINGS.value, ConfigKey.TARGET_CNT.value, str(self.clf_cnt + 1)) + self.config.set(ConfigKey.SML_SETTINGS.value, f"model_path_{str(self.clf_cnt + 1)}", "") + self.config.set(ConfigKey.SML_SETTINGS.value, f"target_name_{str(self.clf_cnt + 1)}", clf_name) + self.config.set(ConfigKey.THRESHOLD_SETTINGS.value, f"threshold_{str(self.clf_cnt + 1)}", "None") + self.config.set(ConfigKey.MIN_BOUT_LENGTH.value, f"min_bout_{str(self.clf_cnt + 1)}", "None") with open(self.config_path, "w") as f: self.config.write(f) - stdout_success( - msg=f"{clf_name} classifier added to SimBA project", - source=self.__class__.__name__, - ) + stdout_success(msg=f"{clf_name} classifier added to SimBA project", source=self.__class__.__name__) class RemoveAClassifierPopUp(PopUpMixin, ConfigReader): - def __init__(self, config_path: str): + def __init__(self, config_path: Union[str, os.PathLike]): PopUpMixin.__init__(self, title="Warning: Remove classifier(s) settings") - ConfigReader.__init__(self, config_path=config_path) - self.remove_clf_frm = CreateLabelFrameWithIcon( - parent=self.main_frm, - header="SELECT A CLASSIFIER TO REMOVE", - icon_name=Keys.DOCUMENTATION.value, - icon_link=Links.REMOVE_CLF.value, - ) - self.clf_dropdown = DropDownMenu( - self.remove_clf_frm, "Classifier", self.clf_names, "12" - ) + ConfigReader.__init__(self, config_path=config_path, read_video_info=False) + self.remove_clf_frm = CreateLabelFrameWithIcon( parent=self.main_frm, header="SELECT A CLASSIFIER TO REMOVE", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.REMOVE_CLF.value) + self.clf_dropdown = DropDownMenu(self.remove_clf_frm, "Classifier", self.clf_names, "12") self.clf_dropdown.setChoices(self.clf_names[0]) - run_btn = Button( - self.main_frm, text="REMOVE CLASSIFIER", command=lambda: self.run() - ) + run_btn = SimbaButton(parent=self.main_frm, txt="REMOVE CLASSIFIER", cmd=self.run, img='trash') self.remove_clf_frm.grid(row=0, sticky=W) self.clf_dropdown.grid(row=0, sticky=W) run_btn.grid(row=1, pady=10) def run(self): for i in range(len(self.clf_names)): - self.config.remove_option( - "SML settings", "model_path_{}".format(str(i + 1)) - ) - self.config.remove_option( - "SML settings", "target_name_{}".format(str(i + 1)) - ) - self.config.remove_option( - "threshold_settings", "threshold_{}".format(str(i + 1)) - ) - self.config.remove_option( - "Minimum_bout_lengths", "min_bout_{}".format(str(i + 1)) - ) + self.config.remove_option("SML settings", f"model_path_{i+1}") + self.config.remove_option("SML settings", f"target_name_{i+1}") + self.config.remove_option("threshold_settings", f"threshold_{i+1}") + self.config.remove_option("Minimum_bout_lengths", f"min_bout_{i+1}") self.clf_names.remove(self.clf_dropdown.getChoices()) self.config.set("SML settings", "no_targets", str(len(self.clf_names))) for clf_cnt, clf_name in enumerate(self.clf_names): - self.config.set( - "SML settings", "model_path_{}".format(str(clf_cnt + 1)), "" - ) - self.config.set( - "SML settings", "target_name_{}".format(str(clf_cnt + 1)), clf_name - ) - self.config.set( - "threshold_settings", "threshold_{}".format(str(clf_cnt + 1)), "None" - ) - self.config.set( - "Minimum_bout_lengths", "min_bout_{}".format(str(clf_cnt + 1)), "None" - ) + self.config.set("SML settings", f"model_path_{clf_cnt + 1}", "") + self.config.set("SML settings", f"target_name_{clf_cnt + 1}", clf_name) + self.config.set("threshold_settings", f"threshold_{clf_cnt + 1}", "None") + self.config.set("Minimum_bout_lengths", f"min_bout_{clf_cnt + 1}", "None") with open(self.config_path, "w") as f: self.config.write(f) - stdout_trash( - msg=f"{self.clf_dropdown.getChoices()} classifier removed from SimBA project.", - source=self.__class__.__name__, - ) + stdout_trash(msg=f"{self.clf_dropdown.getChoices()} classifier removed from SimBA project.", source=self.__class__.__name__) # _ = RemoveAClassifierPopUp(config_path='/Users/simon/Desktop/envs/troubleshooting/Two_animals_16bps/project_folder/project_config.ini') diff --git a/simba/utils/enums.py b/simba/utils/enums.py index f27e2d725..766008d0a 100644 --- a/simba/utils/enums.py +++ b/simba/utils/enums.py @@ -20,7 +20,7 @@ class ConfigKey(Enum): VIDEO_INFO_CSV = "video_info.csv" FOLDER_PATH = "folder_path" FILE_TYPE = "workflow_file_type" - TARGET_CNT = "No_targets" + TARGET_CNT = "no_targets" ANIMAL_CNT = "animal_no" PROJECT_NAME = "project_name" OS = "OS_system" diff --git a/simba/utils/lookups.py b/simba/utils/lookups.py index f204b5478..22096c729 100644 --- a/simba/utils/lookups.py +++ b/simba/utils/lookups.py @@ -547,10 +547,10 @@ def video_quality_to_preset_lookup() -> Dict[str, str]: return {"Low": "fast", "Medium": "medium", "High": "slow"} -def get_labelling_img_kbd_bindings(): +def get_labelling_img_kbd_bindings() -> dict: """ Returns dictionary of tkinter keyboard bindings. - .. notes:: + .. note:: Change ``kbd`` values to change keyboard shortcuts. For example: Some possible examples: @@ -583,6 +583,53 @@ def get_labelling_img_kbd_bindings(): 'kbd': ""} } +def get_labelling_video_kbd_bindings() -> dict: + """ + Returns dictionary of OpenCV keyboard bindings. + + .. note:: + Change ``kbd`` values to change keyboard shortcuts. Note that OpenCV keyboard bindings are different from tkinter + keyboard bindings as used in ``get_labelling_img_kbd_bindings``. + + Use either letters as below, or integer ASCII code for non-letters. For example: + - Space bar: 32 + - Left arrow: 81 + - Right arrow: 83 + + e.g., {'Pause/Play': {'label': 'p = Pause/Play', 'kbd': 32} would remap the space-bar to pause. + + """ + + + return \ + {'Pause/Play': # + {'label': 'p = Pause/Play', + 'kbd': 32}, + 'forward_two_frames': # + {'label': 'o = +2 frames', + 'kbd': "o"}, + 'forward_ten_frames': # + {'label': 'e = +10 frames', + 'kbd': "e"}, + 'forward_one_second': # + {'label': 'w = +1 second', + 'kbd': "w"}, + 'backwards_two_frames': # + {'label': 't = -2 frames', + 'kbd': "t"}, + 'backwards_ten_frames': # + {'label': 's = -10 frames', + 'kbd': "s"}, + 'backwards_one_second': # + {'label': 'x = -1 second', + 'kbd': "x"}, + 'close_window': # + {'label': 'q = Close video window', + 'kbd': "q"}, + } + + + def get_fonts(): """ Returns a dictionary with all fonts available in OS, with the font name as key and font path as value""" font_dict = {f.name: f.fname for f in matplotlib.font_manager.fontManager.ttflist if not f.name.startswith('.')}