From 35dd93fb8d16c011a5b0e3f1a94f7948c6611f19 Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Sat, 9 Nov 2019 11:13:31 -0500 Subject: [PATCH 1/3] Support merging/dropping of short scenes. * Add new `-m`/`--min-duration` option for specifying a minimum number of seconds for each scene. * Scenes shorter than this value will be merged with subsequent scenes, or if `-M`/`--min-duration-action` is set to `drop`, dropped from the scene list entirely. --- scenedetect/cli/__init__.py | 17 ++++++++++++++--- scenedetect/cli/context.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/scenedetect/cli/__init__.py b/scenedetect/cli/__init__.py index 664c57cf..ef0d58e4 100644 --- a/scenedetect/cli/__init__.py +++ b/scenedetect/cli/__init__.py @@ -206,6 +206,17 @@ def duplicate_command(ctx, param_hint): 'Skips N frames during processing (-fs 1 skips every other frame, processing 50% of the video,' ' -fs 2 processes 33% of the frames, -fs 3 processes 25%, etc...).' ' Reduces processing speed at expense of accuracy.') +@click.option( + '--min-duration', '-m', metavar='SECONDS', + type=click.FLOAT, default=None, help= + 'Ensure that scenes are at least SECONDS long, either by merging shorter scenes together' + ' (with `--min-duration-action=merge` or by dropping them (with `--min-duration-action=drop`') +@click.option( + '--min-duration-action', '-M', metavar='ACTION', + type=click.Choice(['merge', 'drop']), default='merge', help= + 'Select how short scenes are merged when using `--min-duration`. Valid values are' + ' `merge` to have short scenes merged with their neighbors, or `drop` to drop short' + ' scenes from the output') @click.option( '--stats', '-s', metavar='CSV', type=click.Path(exists=False, file_okay=True, writable=True, resolve_path=False), help= @@ -231,7 +242,8 @@ def duplicate_command(ctx, param_hint): ' commands. Equivalent to setting `--verbosity none`. Overrides the current verbosity' ' level, even if `-v`/`--verbosity` is set.') @click.pass_context -def scenedetect_cli(ctx, input, output, framerate, downscale, frame_skip, stats, +def scenedetect_cli(ctx, input, output, framerate,downscale, frame_skip, + min_duration, min_duration_action, stats, verbosity, logfile, quiet): """ For example: @@ -287,7 +299,7 @@ def scenedetect_cli(ctx, input, output, framerate, downscale, frame_skip, stats, logging.info('Output directory set:\n %s', ctx.obj.output_directory) ctx.obj.parse_options( input_list=input, framerate=framerate, stats_file=stats, downscale=downscale, - frame_skip=frame_skip) + frame_skip=frame_skip, min_duration=min_duration, min_duration_action=min_duration_action) except: logging.error('Could not parse CLI options.') raise @@ -745,4 +757,3 @@ def colors_command(ctx): add_cli_command(scenedetect_cli, split_video_command) add_cli_command(scenedetect_cli, export_html_command) - diff --git a/scenedetect/cli/context.py b/scenedetect/cli/context.py index 305909da..8cb3a5f7 100644 --- a/scenedetect/cli/context.py +++ b/scenedetect/cli/context.py @@ -329,6 +329,33 @@ def process_input(self): # files with based on the given commands (list-scenes, split-video, save-images, etc...). cut_list = self.scene_manager.get_cut_list(base_timecode) scene_list = self.scene_manager.get_scene_list(base_timecode) + + + def merge_scenes(scenes, min_duration): + scenes = scenes[:] + i = 0 + while i < len(scenes) - 1: + if i >= len(scenes) - 1: + break + if (scenes[i][1] - scenes[i][0]) < min_duration: + scenes[i] = (scenes[i][0], scenes[i+1][1]) + del scenes[i+1] + i = 0 + else: + i += 1 + return scenes + + if self.min_duration: + if self.min_duration_action == "merge": + scene_list = merge_scenes(scene_list, self.min_duration) + elif self.min_duration_action == "drop": + scene_list = [ + s for s in scene_list + if ( s[1].get_seconds() - s[0].get_seconds() ) >= self.min_duration + ] + else: + raise click.BadParameter("unknown value for --min-duration-action: %s" %(self.min_duration_action)) + video_paths = self.video_manager.get_video_paths() video_name = os.path.basename(video_paths[0]) if video_name.rfind('.') >= 0: @@ -536,7 +563,8 @@ def _init_video_manager(self, input_list, framerate, downscale): return video_manager_initialized - def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip): + def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip, + min_duration, min_duration_action): # type: (List[str], float, str, int, int) -> None """ Parse Options: Parses all global options/arguments passed to the main scenedetect command, before other sub-commands (e.g. this function processes @@ -574,6 +602,8 @@ def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip self.options_processed = True + self.min_duration = min_duration + self.min_duration_action = min_duration_action def time_command(self, start=None, duration=None, end=None): # type: (Optional[str], Optional[str], Optional[str]) -> None @@ -691,4 +721,3 @@ def save_images_command(self, num_images, output, name_format, jpeg, webp, quali logging.error('Multiple image type flags set for save-images command.') raise click.BadParameter( 'Only one image type (JPG/PNG/WEBP) can be specified.', param_hint='save-images') - From 50a5c63a74295b6f61104c3c1c0467ede8b8849b Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Sun, 10 Nov 2019 01:43:31 -0500 Subject: [PATCH 2/3] Revert "Support merging/dropping of short scenes." This reverts commit 35dd93fb8d16c011a5b0e3f1a94f7948c6611f19. --- scenedetect/cli/__init__.py | 16 ++-------------- scenedetect/cli/context.py | 33 ++------------------------------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/scenedetect/cli/__init__.py b/scenedetect/cli/__init__.py index ef0d58e4..4c4ffb54 100644 --- a/scenedetect/cli/__init__.py +++ b/scenedetect/cli/__init__.py @@ -206,17 +206,6 @@ def duplicate_command(ctx, param_hint): 'Skips N frames during processing (-fs 1 skips every other frame, processing 50% of the video,' ' -fs 2 processes 33% of the frames, -fs 3 processes 25%, etc...).' ' Reduces processing speed at expense of accuracy.') -@click.option( - '--min-duration', '-m', metavar='SECONDS', - type=click.FLOAT, default=None, help= - 'Ensure that scenes are at least SECONDS long, either by merging shorter scenes together' - ' (with `--min-duration-action=merge` or by dropping them (with `--min-duration-action=drop`') -@click.option( - '--min-duration-action', '-M', metavar='ACTION', - type=click.Choice(['merge', 'drop']), default='merge', help= - 'Select how short scenes are merged when using `--min-duration`. Valid values are' - ' `merge` to have short scenes merged with their neighbors, or `drop` to drop short' - ' scenes from the output') @click.option( '--stats', '-s', metavar='CSV', type=click.Path(exists=False, file_okay=True, writable=True, resolve_path=False), help= @@ -242,8 +231,7 @@ def duplicate_command(ctx, param_hint): ' commands. Equivalent to setting `--verbosity none`. Overrides the current verbosity' ' level, even if `-v`/`--verbosity` is set.') @click.pass_context -def scenedetect_cli(ctx, input, output, framerate,downscale, frame_skip, - min_duration, min_duration_action, stats, +def scenedetect_cli(ctx, input, output, framerate, downscale, frame_skip, stats, verbosity, logfile, quiet): """ For example: @@ -299,7 +287,7 @@ def scenedetect_cli(ctx, input, output, framerate,downscale, frame_skip, logging.info('Output directory set:\n %s', ctx.obj.output_directory) ctx.obj.parse_options( input_list=input, framerate=framerate, stats_file=stats, downscale=downscale, - frame_skip=frame_skip, min_duration=min_duration, min_duration_action=min_duration_action) + frame_skip=frame_skip) except: logging.error('Could not parse CLI options.') raise diff --git a/scenedetect/cli/context.py b/scenedetect/cli/context.py index 8cb3a5f7..305909da 100644 --- a/scenedetect/cli/context.py +++ b/scenedetect/cli/context.py @@ -329,33 +329,6 @@ def process_input(self): # files with based on the given commands (list-scenes, split-video, save-images, etc...). cut_list = self.scene_manager.get_cut_list(base_timecode) scene_list = self.scene_manager.get_scene_list(base_timecode) - - - def merge_scenes(scenes, min_duration): - scenes = scenes[:] - i = 0 - while i < len(scenes) - 1: - if i >= len(scenes) - 1: - break - if (scenes[i][1] - scenes[i][0]) < min_duration: - scenes[i] = (scenes[i][0], scenes[i+1][1]) - del scenes[i+1] - i = 0 - else: - i += 1 - return scenes - - if self.min_duration: - if self.min_duration_action == "merge": - scene_list = merge_scenes(scene_list, self.min_duration) - elif self.min_duration_action == "drop": - scene_list = [ - s for s in scene_list - if ( s[1].get_seconds() - s[0].get_seconds() ) >= self.min_duration - ] - else: - raise click.BadParameter("unknown value for --min-duration-action: %s" %(self.min_duration_action)) - video_paths = self.video_manager.get_video_paths() video_name = os.path.basename(video_paths[0]) if video_name.rfind('.') >= 0: @@ -563,8 +536,7 @@ def _init_video_manager(self, input_list, framerate, downscale): return video_manager_initialized - def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip, - min_duration, min_duration_action): + def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip): # type: (List[str], float, str, int, int) -> None """ Parse Options: Parses all global options/arguments passed to the main scenedetect command, before other sub-commands (e.g. this function processes @@ -602,8 +574,6 @@ def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip self.options_processed = True - self.min_duration = min_duration - self.min_duration_action = min_duration_action def time_command(self, start=None, duration=None, end=None): # type: (Optional[str], Optional[str], Optional[str]) -> None @@ -721,3 +691,4 @@ def save_images_command(self, num_images, output, name_format, jpeg, webp, quali logging.error('Multiple image type flags set for save-images command.') raise click.BadParameter( 'Only one image type (JPG/PNG/WEBP) can be specified.', param_hint='save-images') + From 0e63b72127679e4b3aa36332cd33266ffccc2bb1 Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Sun, 10 Nov 2019 01:44:13 -0500 Subject: [PATCH 3/3] Interpret --min-scene-len argument as a FrameTimecode. --- scenedetect/cli/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scenedetect/cli/__init__.py b/scenedetect/cli/__init__.py index 4c4ffb54..cec686f2 100644 --- a/scenedetect/cli/__init__.py +++ b/scenedetect/cli/__init__.py @@ -413,9 +413,11 @@ def time_command(ctx, start, duration, end): # '[Optional] Intensity cutoff threshold to disable scene cut detection. Useful for avoiding.' # ' scene changes triggered by flashes. Refers to frame metric delta_lum in stats file.') @click.option( - '--min-scene-len', '-m', metavar='FRAMES', - type=click.INT, default=15, show_default=True, help= - 'Minimum size/length of any scene, in number of frames.') + '--min-scene-len', '-m', metavar='TIMECODE', + type=click.STRING, default="0", help= + 'Minimum size/length of any scene. TIMECODE can be specified as exact' + ' number of frames, a time in seconds followed by s, or a timecode in the' + ' format HH:MM:SS or HH:MM:SS.nnn') @click.pass_context def detect_content_command(ctx, threshold, min_scene_len): #, intensity_cutoff): """ Perform content detection algorithm on input video(s). @@ -428,6 +430,8 @@ def detect_content_command(ctx, threshold, min_scene_len): #, intensity_cutoff): #if intensity_cutoff is not None: # raise NotImplementedError() + min_scene_len = parse_timecode(ctx.obj, min_scene_len) + logging.debug('Detecting content, parameters:\n' ' threshold: %d, min-scene-len: %d', threshold, min_scene_len) @@ -447,9 +451,11 @@ def detect_content_command(ctx, threshold, min_scene_len): #, intensity_cutoff): 'Threshold value (integer) that the delta_rgb frame metric must exceed to trigger a new scene.' ' Refers to frame metric delta_rgb in stats file.') @click.option( - '--min-scene-len', '-m', metavar='FRAMES', - type=click.INT, default=15, show_default=True, help= - 'Minimum size/length of any scene, in number of frames.') + '--min-scene-len', '-m', metavar='TIMECODE', + type=click.STRING, default="0", help= + 'Minimum size/length of any scene. TIMECODE can be specified as exact' + ' number of frames, a time in seconds followed by s, or a timecode in the' + ' format HH:MM:SS or HH:MM:SS.nnn') @click.option( '--fade-bias', '-f', metavar='PERCENT', type=click.IntRange(-100, 100), default=0, show_default=True, help= @@ -488,6 +494,8 @@ def detect_threshold_command(ctx, threshold, min_scene_len, fade_bias, add_last_ # Handle case where add_last_scene is not set and is None. add_last_scene = True if add_last_scene else False + min_scene_len = parse_timecode(ctx.obj, min_scene_len) + # Convert min_percent and fade_bias from integer to floats (0.0-1.0 and -1.0-+1.0 respectively). min_percent /= 100.0 fade_bias /= 100.0