-
Notifications
You must be signed in to change notification settings - Fork 150
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
31c8b25
commit 85ed746
Showing
2 changed files
with
573 additions
and
763 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import os | ||
from tkinter import * | ||
import cv2 | ||
from PIL import Image, ImageTk | ||
from subprocess import Popen, PIPE | ||
from typing import Union, Optional, Tuple, Callable | ||
try: | ||
from typing import Literal | ||
except: | ||
from typing_extensions import Literal | ||
from simba.mixins.config_reader import ConfigReader | ||
from simba.utils.read_write import (get_fn_ext, read_df, write_df, get_video_meta_data, get_all_clf_names) | ||
from simba.utils.enums import Labelling | ||
from simba.utils.checks import check_int | ||
from simba.utils.data import find_time_stamp_from_frame_numbers | ||
from simba.utils.errors import FrameRangeError | ||
from simba.ui.tkinter_functions import Entry_Box | ||
from simba.video_processors.video_processing import clip_video_in_range | ||
from simba.utils.printing import SimbaTimer, stdout_success | ||
|
||
|
||
MAX_FRM_SIZE = 1280, 650 | ||
|
||
class AnnotatorMixin(ConfigReader): | ||
|
||
def __init__(self, | ||
config_path: Union[str, os.PathLike], | ||
video_path: Union[str, os.PathLike], | ||
data_path: Union[str, os.PathLike], | ||
frame_size: Optional[Tuple[int]] = MAX_FRM_SIZE): | ||
|
||
ConfigReader.__init__(self, config_path=config_path) | ||
self.video_path = video_path | ||
self.cap = cv2.VideoCapture(self.video_path) | ||
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.video_name, _ = get_fn_ext(filepath=video_path) | ||
_, self.pixels_per_mm, self.fps = self.read_video_info(video_name=self.video_name) | ||
self.target_lst = get_all_clf_names(config=self.config, target_cnt=self.clf_cnt) | ||
self.data_df = read_df(file_path=data_path, file_type=self.file_type) | ||
self.max_frm_no, self.min_frm_no = max(self.data_df.index), min(self.data_df.index) | ||
self.main_frm = Toplevel() | ||
self.max_size = frame_size | ||
self.main_frm.title(f'SIMBA CLIP ANNOTATOR - {self.video_name}') | ||
|
||
def video_frm_label(self, | ||
frm_number: int, | ||
max_size: Optional[Tuple[int, int]] = None, | ||
loc: Tuple[int] = (0, 0)) -> None: | ||
|
||
""" Returns a video frame as a tkinter label""" | ||
if max_size is None: max_size = self.max_size | ||
self.cap.set(1, frm_number) | ||
_, frm = self.cap.read() | ||
frm = cv2.cvtColor(frm, cv2.COLOR_RGB2BGR) | ||
frm = Image.fromarray(frm) | ||
frm.thumbnail(max_size, Image.ANTIALIAS) | ||
frm = ImageTk.PhotoImage(master=self.main_frm, image=frm) | ||
video_frame = Label(self.main_frm, image=frm) | ||
video_frame.image = frm | ||
video_frame.grid(row=loc[0], column=loc[1], sticky=NW) | ||
|
||
|
||
def h_nav_bar(self, parent: Frame, | ||
change_frm_func: Callable[[int], None], | ||
width: int = 700, | ||
height: int = 300) -> Frame: | ||
|
||
""" Create a horizontal frame navigation bar""" | ||
out_frm = Frame(parent, bd=2, width=width, height=height) | ||
nav_frm = Frame(out_frm) | ||
jump_frame = Frame(out_frm, bd=2) | ||
jump_lbl = Label(jump_frame, text="JUMP SIZE:") | ||
jump_size = Scale(jump_frame, from_=0, to=100, orient=HORIZONTAL, length=200) | ||
jump_size.set(0) | ||
self.frm_box = Entry_Box(nav_frm, 'FRAME NUMBER', '15') | ||
self.frm_box.entry_set(val=self.min_frm_no) | ||
forward_btn = Button(nav_frm, text=">", command=lambda: change_frm_func(int(self.frm_box.entry_get)+1)) | ||
backward_btn = Button(nav_frm, text="<", command=lambda: change_frm_func(int(self.frm_box.entry_get)-1)) | ||
forward_max_btn = Button(nav_frm, text=">>", command= lambda: change_frm_func(self.max_frm_no-1)) | ||
backward_max_btn = Button(nav_frm, text="<<", command= lambda: change_frm_func(self.min_frm_no)) | ||
jump_back = Button(jump_frame, text="<<", command=lambda: change_frm_func(int(self.frm_box.entry_get) - jump_size.get())) | ||
jump_forward = Button(jump_frame, text=">>", command= lambda: change_frm_func(int(self.frm_box.entry_get) + jump_size.get())) | ||
select_frm_btn = Button(nav_frm, text="VIEW SELECTED FRAME", command=change_frm_func(int(self.frm_box.entry_get))) | ||
|
||
nav_frm.grid(row=0, column=0) | ||
self.frm_box.grid(row=0, column=0) | ||
backward_max_btn.grid(row=0, column=1, sticky=NE, padx=Labelling.PADDING.value) | ||
backward_btn.grid(row=0, column=2, sticky=NE, padx=Labelling.PADDING.value) | ||
forward_btn.grid(row=0, column=3, sticky=NE, padx=Labelling.PADDING.value) | ||
forward_max_btn.grid(row=0, column=4, sticky=NE, padx=Labelling.PADDING.value) | ||
select_frm_btn.grid(row=1, column=0, sticky=NE, padx=Labelling.PADDING.value) | ||
|
||
jump_frame.grid(row=1, column=0) | ||
jump_lbl.grid(row=0, column=0, sticky=NE) | ||
jump_size.grid(row=0, column=1, sticky=SE) | ||
jump_back.grid(row=0, column=2, sticky=SE) | ||
jump_forward.grid(row=0, column=3, sticky=SE) | ||
|
||
return out_frm | ||
|
||
|
||
def v_navigation_pane_targeted_clips_version(self, parent: Frame) -> Frame: | ||
""" Create a vertical navigation pane for opening video and displaying keyboard shortcuts""" | ||
|
||
def bind_shortcut_keys(parent): | ||
parent.bind('<Control-s>', lambda x: self.__targeted_clips_save()) | ||
parent.bind('<Right>', lambda x: self.change_frame_targeted_annotations(int(self.frm_box.entry_get)+1)) | ||
parent.bind('<Left>', lambda x: self.change_frame_targeted_annotations(int(self.frm_box.entry_get)-1)) | ||
parent.bind('<Control-l>', lambda x: self.change_frame_targeted_annotations(self.max_frm_no-1)) | ||
parent.bind('<Control-o>', lambda x: self.change_frame_targeted_annotations(self.min_frm_no)) | ||
|
||
video_player_frm = Frame(parent) | ||
play_video_btn = Button(video_player_frm, text='OPEN VIDEO', command= lambda: self.__play_video()) | ||
play_video_btn.grid(sticky=N, pady=10) | ||
Label(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').grid(sticky=W) | ||
update_img_from_video = Button(video_player_frm, text='Show current video frame', command=None) | ||
update_img_from_video.grid(sticky=N) | ||
|
||
key_presses_lbl = Label(video_player_frm, | ||
text='\n\n Keyboard shortcuts for frame navigation: \n Right Arrow = +1 frame' | ||
'\n Left Arrow = -1 frame' | ||
'\n Ctrl + s = Save annotations file' | ||
'\n Ctrl + l = Last frame' | ||
'\n Ctrl + o = First frame') | ||
key_presses_lbl.grid(sticky=S) | ||
bind_shortcut_keys(parent) | ||
return video_player_frm | ||
|
||
|
||
def targeted_clips_pane(self, parent: Frame) -> Frame: | ||
""" Create a pane for choosing bouts start and end and a radiobutton truth table for targeted clips annotations""" | ||
def set_selection(): | ||
selection_txt.config(text=f"CURRENT SELECTION START: {start_frm_bx.entry_get}, END: {end_frm_bx.entry_get}") | ||
save_button.config(text=f"SAVE ANNOTATION CLIP \n START FRAME: {start_frm_bx.entry_get}, \n END FRAME: {end_frm_bx.entry_get}") | ||
self.set_start, self.set_end = start_frm_bx.entry_get, end_frm_bx.entry_get | ||
|
||
pane, self.set_start, self.set_end = Frame(parent, bd=2), 0, 1 | ||
selection_txt = Label(pane, text=f"CURRENT SELECTION START: {0}, END: {1}", font=("Helvetica", 14, "bold")) | ||
start_frm_bx = Entry_Box(pane, 'SELECT START FRAME:', '20', validation='numeric', entry_box_width=10) | ||
start_frm_bx.entry_set('0') | ||
end_frm_bx = Entry_Box(pane, 'SELECT END FRAME:', '20', validation='numeric', entry_box_width=10) | ||
end_frm_bx.entry_set('1') | ||
confirm_btn = Button(pane, text="SET SELECTION", command= lambda: set_selection()) | ||
selection_txt.grid(row=0, column=0, pady=30, sticky=N) | ||
start_frm_bx.grid(row=1, column=0, pady=30, sticky=S) | ||
end_frm_bx.grid(row=2, column=0, pady=10, sticky=S) | ||
confirm_btn.grid(row=3, column=0, sticky=S) | ||
|
||
Label(pane, text=f"PRESENT", font=("Helvetica", 12, "bold")).grid(row=4, column=1, pady=20, sticky=NW) | ||
Label(pane, text=f"ABSENT", font=("Helvetica", 12, "bold")).grid(row=4, column=2, pady=20, sticky=NW) | ||
self.clf_radio_btns = {} | ||
for target_cnt, target in enumerate(self.target_lst): | ||
self.clf_radio_btns[target] = StringVar(value=0) | ||
Label(pane, text=target, font=("Helvetica", 12, "bold")).grid(row=target_cnt+5, column=0, sticky=NW) | ||
present = Radiobutton(pane, text=None, variable=self.clf_radio_btns[target], value=1, command=None) | ||
absent = Radiobutton(pane, text=None, variable=self.clf_radio_btns[target], value=0, command=None) | ||
present.grid(row=target_cnt+5, column=1, pady=5, sticky=NW) | ||
absent.grid(row=target_cnt+5, column=2, pady=5, sticky=NW) | ||
|
||
save_button = Button(pane, text=f"SAVE ANNOTATION CLIP \n START FRAME: {start_frm_bx.entry_get}, \n END FRAME: {end_frm_bx.entry_get}", fg='green', font=("Helvetica", 14, "bold"), command= lambda: self.__targeted_clips_save()) | ||
save_button.grid(row=5+len(self.target_lst), column=0, pady=40, sticky=S) | ||
|
||
return pane | ||
|
||
def __play_video(self): | ||
"""Helper to play video when ``play video button`` is pressed """ | ||
p = Popen('python {}'.format(Labelling.PLAY_VIDEO_SCRIPT_PATH.value), stdin=PIPE, stdout=PIPE, shell=True) | ||
p.stdin.write(bytes(self.video_path, 'utf-8')) | ||
p.stdin.close() | ||
temp_file = os.path.join(os.path.dirname(self.config_path), 'subprocess.txt') | ||
with open(temp_file, "w") as text_file: text_file.write(str(p.pid)) | ||
|
||
def __targeted_clips_save(self): | ||
"""Helper method called by targeted_clips_pane to save video and dataframe sliced during targeted clips annotations""" | ||
timer = SimbaTimer(start=True) | ||
print('Saving video clip...') | ||
start_idx, end_idx = int(self.set_start), int(self.set_end) | ||
if start_idx >= end_idx: raise FrameRangeError(msg=f'Start frame ({start_idx}) has to be before end frame ({end_idx}).', source=self.__class__.__name__) | ||
if (start_idx > self.max_frm_no) or (start_idx < self.min_frm_no) or (end_idx > self.max_frm_no) or (end_idx < self.min_frm_no): | ||
raise FrameRangeError(msg=f'Start frame ({start_idx}) or end frame ({end_idx}) cannot be smaller or larger than the minimum ({self.min_frm_no}) and maximum ({self.max_frm_no}) frame of the video', source=self.__class__.__name__) | ||
timestamps = find_time_stamp_from_frame_numbers(start_frame=start_idx, end_frame=end_idx, fps=self.video_meta_data['fps']) | ||
clip_video_in_range(file_path=self.video_path, start_time=timestamps[0], end_time=timestamps[1], out_dir=self.video_dir, include_clip_time_in_filename=True, overwrite=True) | ||
df_filename = os.path.join(self.video_name + f'_{timestamps[0].replace(":", "-")}_{timestamps[1].replace(":", "-")}.{self.file_type}') | ||
video_name = os.path.join(self.video_name + f'_{timestamps[0].replace(":", "-")}_{timestamps[1].replace(":", "-")}') | ||
print(f'Saving targets data {df_filename}...') | ||
df = self.data_df.iloc[start_idx:end_idx, :] | ||
for clf, btn in self.clf_radio_btns.items(): | ||
df[clf] = btn.get() | ||
write_df(df=df, file_type=self.file_type, save_path=os.path.join(self.targets_folder, df_filename)) | ||
self.add_video_to_video_info(video_name=video_name, fps=self.fps, width=self.video_meta_data['width'], height=self.video_meta_data['height'], pixels_per_mm=self.pixels_per_mm) | ||
timer.stop_timer() | ||
stdout_success(msg=f'Annotated clip {df_filename} saved into SimBA project', elapsed_time=timer.elapsed_time_str) | ||
|
||
|
||
def change_frame_targeted_annotations(self, | ||
new_frm_id: int): | ||
print(new_frm_id) | ||
check_int(name='FRAME NUMBER', value=new_frm_id) | ||
if new_frm_id > (self.max_frm_no -1): | ||
self.frm_box.entry_set(val=self.max_frm_no) | ||
raise FrameRangeError(msg=f"FRAME {new_frm_id} CANNOT BE SHOWN - YOU ARE VIEWING THE FINAL FRAME OF THE VIDEO (FRAME NUMBER {self.max_frm_no})", source=self.__class__.__name__) | ||
elif new_frm_id < 0: | ||
self.frm_box.entry_set(val=self.min_frm_no) | ||
raise FrameRangeError(msg=f"FRAME {new_frm_id} CANNOT BE SHOWN - YOU ARE VIEWING THE FIRST FRAME OF THE VIDEO (FRAME NUMBER {self.min_frm_no})", source=self.__class__.__name__) | ||
else: | ||
self.frm_box.entry_set(val=new_frm_id) | ||
self.video_frm_label(frm_number=int(new_frm_id)) |
Oops, something went wrong.