From 99c3147aeeae488cf19c7563047042596083ad1f Mon Sep 17 00:00:00 2001 From: sronilsson Date: Tue, 28 May 2024 12:45:59 -0400 Subject: [PATCH] video tools --- simba/ui/pop_ups/video_processing_pop_up.py | 109 ++++++++++---------- simba/video_processors/video_processing.py | 49 ++++----- 2 files changed, 76 insertions(+), 82 deletions(-) diff --git a/simba/ui/pop_ups/video_processing_pop_up.py b/simba/ui/pop_ups/video_processing_pop_up.py index a271eff1e..3654eb171 100644 --- a/simba/ui/pop_ups/video_processing_pop_up.py +++ b/simba/ui/pop_ups/video_processing_pop_up.py @@ -37,6 +37,7 @@ NotDirectoryError) from simba.utils.lookups import get_color_dict, get_fonts from simba.utils.printing import SimbaTimer, stdout_success +from simba.utils.warnings import FrameRangeWarning from simba.utils.read_write import ( check_if_hhmmss_timestamp_is_valid_part_of_video, concatenate_videos_in_folder, find_all_videos_in_directory, @@ -960,61 +961,63 @@ def run(self): class CreateGIFPopUP(PopUpMixin): def __init__(self): - PopUpMixin.__init__(self, title="CREATE GIF FROM VIDEO", size=(250, 250)) - settings_frm = CreateLabelFrameWithIcon( - parent=self.main_frm, - header="SETTINGS", - icon_name=Keys.DOCUMENTATION.value, - icon_link=Links.VIDEO_TOOLS.value, - ) - selected_video = FileSelect( - settings_frm, - "Video path: ", - title="Select a video file", - file_types=[("VIDEO FILE", Options.ALL_VIDEO_FORMAT_STR_OPTIONS.value)], - ) - start_time_entry_box = Entry_Box( - settings_frm, "Start time (s): ", "16", validation="numeric" - ) - duration_entry_box = Entry_Box( - settings_frm, "Duration (s): ", "16", validation="numeric" - ) - width_entry_box = Entry_Box(settings_frm, "Width: ", "16", validation="numeric") - gpu_var = BooleanVar() - gpu_cb = Checkbutton( - settings_frm, text="Use GPU (decreased runtime)", variable=gpu_var - ) - width_instructions_1 = Label( - settings_frm, - text="Example Width: 240, 360, 480, 720, 1080", - font=("Times", 12, "italic"), - ) - width_instructions_2 = Label( - settings_frm, - text="Aspect ratio is kept (i.e., height is automatically computed)", - font=("Times", 12, "italic"), - ) - run_btn = Button( - settings_frm, - text="CREATE GIF", - command=lambda: gif_creator( - file_path=selected_video.file_path, - start_time=start_time_entry_box.entry_get, - duration=duration_entry_box.entry_get, - width=width_entry_box.entry_get, - gpu=gpu_var.get(), - ), - ) + PopUpMixin.__init__(self, title="CREATE GIF FROM VIDEO", size=(600, 400)) + settings_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="SETTINGS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_TOOLS.value) + self.selected_video = FileSelect(settings_frm, "VIDEO PATH: ", title="Select a video file", file_types=[("VIDEO FILE", Options.ALL_VIDEO_FORMAT_STR_OPTIONS.value)], lblwidth=40) + self.start_time_entry_box = Entry_Box(settings_frm, "START TIME (s):", "40", validation="numeric") + self.duration_entry_box = Entry_Box(settings_frm, "DURATION (s): ", "40", validation="numeric") + resolution_widths = Options.RESOLUTION_OPTIONS_2.value + self.resolution_dropdown = DropDownMenu(settings_frm, "GIF WIDTH (ASPECT RATIO RETAINED):", resolution_widths, "40") + self.quality_dropdown = DropDownMenu(settings_frm, "GIF QUALITY (%):", list(range(1, 101, 1)), "40") + fps_lst = list(range(1, 101, 1)) + fps_lst.insert(0, 'AUTO') + self.fps_dropdown = DropDownMenu(settings_frm, "GIF FPS:", fps_lst, "40") + self.gpu_var = BooleanVar() + gpu_cb = Checkbutton(settings_frm, text="USE GPU (decreased runtime)", variable=self.gpu_var) + self.quality_dropdown.setChoices(100) + self.resolution_dropdown.setChoices('AUTO') + self.fps_dropdown.setChoices('AUTO') settings_frm.grid(row=0, sticky=NW) - selected_video.grid(row=0, sticky=NW, pady=5) - start_time_entry_box.grid(row=1, sticky=NW) - duration_entry_box.grid(row=2, sticky=NW) - width_entry_box.grid(row=3, sticky=NW) - gpu_cb.grid(row=4, column=0, sticky=NW) - width_instructions_1.grid(row=5, sticky=NW) - width_instructions_2.grid(row=6, sticky=NW) - run_btn.grid(row=7, sticky=NW, pady=10) + self.selected_video.grid(row=0, sticky=NW, pady=5) + self.start_time_entry_box.grid(row=1, sticky=NW) + self.duration_entry_box.grid(row=2, sticky=NW) + self.resolution_dropdown.grid(row=3, sticky=NW) + self.quality_dropdown.grid(row=4, sticky=NW) + self.fps_dropdown.grid(row=5, sticky=NW) + gpu_cb.grid(row=6, column=0, sticky=NW) + self.create_run_frm(run_function=self.run) + self.main_frm.mainloop() + def run(self): + video_path = self.selected_video.file_path + width = self.resolution_dropdown.getChoices() + start_time = self.start_time_entry_box.entry_get + duration = self.duration_entry_box.entry_get + fps = self.fps_dropdown.getChoices() + quality = int(self.quality_dropdown.getChoices()) + gpu = self.gpu_var.get() + check_ffmpeg_available() + if gpu: check_nvidea_gpu_available() + check_file_exist_and_readable(file_path=video_path) + video_meta_data = get_video_meta_data(video_path=video_path) + if width == 'AUTO': width = video_meta_data['width'] + else: width = int(width) + if fps == 'AUTO': fps = int(video_meta_data['fps']) + else: fps = int(fps) + if fps > int(video_meta_data['fps']): + FrameRangeWarning(msg=f'The chosen FPS ({fps}) is higher than the video FPS ({video_meta_data["fps"]}). The video FPS will be used', source=self.__class__.__name__) + fps = int(video_meta_data['fps']) + max_duration = video_meta_data['video_length_s'] - int(start_time) + check_int(name='start_time', value=start_time, max_value=video_meta_data['video_length_s'], min_value=0) + check_int(name='duration', value=duration, max_value=max_duration, min_value=1) + + threading.Thread(target=gif_creator(file_path=video_path, + start_time=int(start_time), + duration=int(duration), + width=width, + gpu=gpu, + fps=fps, + quality=int(quality))).start() class CalculatePixelsPerMMInVideoPopUp(PopUpMixin): def __init__(self): diff --git a/simba/video_processors/video_processing.py b/simba/video_processors/video_processing.py index 447553238..088d78fae 100644 --- a/simba/video_processors/video_processing.py +++ b/simba/video_processors/video_processing.py @@ -940,13 +940,13 @@ def downsample_video( ) -def gif_creator( - file_path: Union[str, os.PathLike], - start_time: int, - duration: int, - width: int, - gpu: Optional[bool] = False, -) -> None: +def gif_creator(file_path: Union[str, os.PathLike], + start_time: int, + duration: int, + width: Optional[int] = None, + quality: Optional[int] = 100, + fps: Optional[int] = 15, + gpu: Optional[bool] = False) -> None: """ Create a sample gif from a video file. The result is stored in the same directory as the input file with the ``.gif`` file-ending. @@ -966,41 +966,32 @@ def gif_creator( check_ffmpeg_available(raise_error=True) if gpu and not check_nvidea_gpu_available(): - raise FFMPEGCodecGPUError( - msg="NVIDEA GPU not available (as evaluated by nvidea-smi returning None", - source=gif_creator.__name__, - ) + raise FFMPEGCodecGPUError(msg="NVIDEA GPU not available (as evaluated by nvidea-smi returning None", source=gif_creator.__name__) timer = SimbaTimer(start=True) check_file_exist_and_readable(file_path=file_path) check_int(name="Start time", value=start_time, min_value=0) check_int(name="Duration", value=duration, min_value=1) - check_int(name="Width", value=width) video_meta_data = get_video_meta_data(file_path) + if width is None: + width = video_meta_data['width'] + check_int(name="Width", value=width, min_value=2) + check_int(name="FPS", value=fps, min_value=1) + check_int(name="QUALITY", value=quality, min_value=1, max_value=100) + quality = int((quality - 1) / (100 - 1) * (256 - 1) + 1) + if width % 2 != 0: width -= 1 + if quality == 1: quality += 1 if (int(start_time) + int(duration)) > video_meta_data["video_length_s"]: - raise FrameRangeError( - msg=f'The end of the gif (start time: {start_time} + duration: {duration}) is longer than the {file_path} video: {video_meta_data["video_length_s"]}s', - source=gif_creator.__name__, - ) - + raise FrameRangeError(msg=f'The end of the gif (start time: {start_time} + duration: {duration}) is longer than the {file_path} video: {video_meta_data["video_length_s"]}s', source=gif_creator.__name__) dir, file_name, ext = get_fn_ext(filepath=file_path) - save_name = os.path.join(dir, file_name + ".gif") - if os.path.isfile(save_name): - raise FileExistError( - "SIMBA ERROR: The outfile file already exist: {}.".format(save_name), - source=gif_creator.__name__, - ) + save_name = os.path.join(dir, f"{file_name}.gif") if gpu: command = f'ffmpeg -hwaccel auto -c:v h264_cuvid -ss {start_time} -i "{file_path}" -to {duration} -vf "fps=10,scale={width}:-1" -c:v gif -pix_fmt rgb24 -y "{save_name}" -y' else: - command = f'ffmpeg -ss {start_time} -t {duration} -i "{file_path}" -filter_complex "[0:v] fps=15,scale=w={width}:h=-1,split [a][b];[a] palettegen=stats_mode=single [p];[b][p] paletteuse=new=1" "{save_name}"' + command = f'ffmpeg -ss {start_time} -t {duration} -i "{file_path}" -filter_complex "[0:v] fps={fps},scale=w={width}:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=single:max_colors={quality} [p];[b][p] paletteuse=dither=bayer:bayer_scale=3" "{save_name}" -loglevel error -stats -hide_banner -y' print("Creating gif sample... ") subprocess.call(command, shell=True, stdout=subprocess.PIPE) timer.stop_timer() - stdout_success( - msg=f"SIMBA COMPLETE: Video converted! {save_name} generated!", - elapsed_time=timer.elapsed_time_str, - source=gif_creator.__name__, - ) + stdout_success(msg=f"SIMBA COMPLETE: Video converted! {save_name} generated!", elapsed_time=timer.elapsed_time_str, source=gif_creator.__name__) # gif_creator(file_path=r'/Users/simon/Desktop/envs/simba/troubleshooting/mouse_open_field/project_folder/videos/SI_DAY3_308_CD1_PRESENT.mp4', start_time=5, duration=15, width=600, gpu=False)