diff --git a/scenedetect/cli/__init__.py b/scenedetect/cli/__init__.py index 2c267f94..9a1bc8fd 100644 --- a/scenedetect/cli/__init__.py +++ b/scenedetect/cli/__init__.py @@ -710,6 +710,54 @@ def detect_threshold_command( ) +@click.command('load-scenes') +@click.option( + '--input', + '-i', + multiple=False, + metavar='CSV', + type=click.Path(exists=True, file_okay=True, readable=True, resolve_path=True), + help='Input csv file that contains csv information.') +@click.option( + '--start-col', + '-s', + metavar='START-HEADER', + type=click.STRING, + default=None, + help='Header for column used to mark scene start points') +@click.option( + '--end-col', + '-e', + metavar='END-HEADER', + type=click.STRING, + default=None, + help='Header for column used to mark scene end points') +@click.option( + '--framerate', + '-f', + metavar='FPS', + type=click.FLOAT, + default=None, + help='Framerate in frames/sec for input video used for timecode to frame number conversions.') +@click.pass_context +def load_scenes_command(ctx: click.Context, input: Optional[str], start_col: Optional[str], + end_col: Optional[str], framerate: Optional[float]): + """A detector that is used to read an input csv file and only detect the scenes from the given + input file. Useful for instances in which a csv file is manually edited and then used to split + the video based on the manually edited csv file. + + Examples: + + load-scenes -i scenes.csv + + load-scenes -i scenes.csv -s 'Start Timecode' -f 30 + """ + assert isinstance(ctx.obj, CliContext) + + ctx.obj.handle_load_scenes( + input=input, start_col=start_col, end_col=end_col, framerate=framerate) + + @click.command('export-html') @click.option( '--filename', @@ -1125,3 +1173,4 @@ def _add_cli_command(cli: click.Group, command: click.Command): _add_cli_command(scenedetect_cli, detect_content_command) _add_cli_command(scenedetect_cli, detect_threshold_command) _add_cli_command(scenedetect_cli, detect_adaptive_command) +_add_cli_command(scenedetect_cli, load_scenes_command) diff --git a/scenedetect/cli/config.py b/scenedetect/cli/config.py index ced0cd1d..8ad602a0 100644 --- a/scenedetect/cli/config.py +++ b/scenedetect/cli/config.py @@ -255,6 +255,12 @@ def from_config(config_value: str, default: 'KernelSizeValue') -> 'KernelSizeVal 'min-scene-len': TimecodeValue(0), 'threshold': RangeValue(12.0, min_val=0.0, max_val=255.0), }, + 'load-scenes': { + 'input': '', + 'start_col': 'Start Frame', + 'end_col': 'End Frame', + 'framerate': False + }, 'export-html': { 'filename': '$VIDEO_NAME-Scenes.html', 'image-height': 0, diff --git a/scenedetect/cli/context.py b/scenedetect/cli/context.py index 2d685020..a3013b83 100644 --- a/scenedetect/cli/context.py +++ b/scenedetect/cli/context.py @@ -442,6 +442,24 @@ def handle_detect_threshold( self.options_processed = options_processed_orig + def handle_load_scenes(self, input: AnyStr, start_col: Optional[str], end_col: Optional[str], + framerate: Optional[float]): + """Handle `load-scenes` command options.""" + self._check_input_open() + options_processed_orig = self.options_processed + self.options_processed = False + + input = self.config.get_value("load-scenes", "input", input) + start_col = self.config.get_value("load-scenes", "start_col", start_col) + end_col = self.config.get_value("load-scenes", "end_col", end_col) + framerate = self.config.get_value("load-scenes", "framerate", framerate) + + self._add_detector( + scenedetect.detectors.SceneLoader( + csv_file=input, start_col=start_col, end_col=end_col, framerate=framerate)) + + self.options_processed = options_processed_orig + def handle_export_html( self, filename: Optional[AnyStr], diff --git a/scenedetect/detectors/__init__.py b/scenedetect/detectors/__init__.py index d622f5c3..5873c803 100644 --- a/scenedetect/detectors/__init__.py +++ b/scenedetect/detectors/__init__.py @@ -76,6 +76,7 @@ from scenedetect.detectors.content_detector import ContentDetector from scenedetect.detectors.threshold_detector import ThresholdDetector from scenedetect.detectors.adaptive_detector import AdaptiveDetector +from scenedetect.detectors.scene_loader import SceneLoader # Algorithms being ported: #from scenedetect.detectors.motion_detector import MotionDetector diff --git a/scenedetect/detectors/scene_loader.py b/scenedetect/detectors/scene_loader.py new file mode 100644 index 00000000..2fdf53b5 --- /dev/null +++ b/scenedetect/detectors/scene_loader.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# +# PySceneDetect: Python-Based Video Scene Detector +# --------------------------------------------------------------- +# [ Site: http://www.scenedetect.scenedetect.com/ ] +# [ Docs: http://manual.scenedetect.scenedetect.com/ ] +# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] +# +# Copyright (C) 2014-2022 Brandon Castellano . +# PySceneDetect is licensed under the BSD 3-Clause License; see the +# included LICENSE file, or visit one of the above pages for details. +# +""":py:class:`SceneLoader` is a class designed for use cases in which a list of +scenes is read from a csv file and actual detection of scene boundaries does not +need to occur. + +This is available from the command-line as the `load-scenes` command. +""" + +import os +import csv + +from typing import List + +import numpy + +from scenedetect.scene_detector import SceneDetector +from scenedetect.frame_timecode import FrameTimecode + + +class SceneLoader(SceneDetector): + """Used for cases in which scenes are read from a csv file. Does not + actually detect scene boundaries. + + Incompatible with other detectors. + """ + + def __init__(self, csv_file=None, start_col="Start Frame", end_col="End Frame", framerate=None): + """ + Arguments: + csv_file: Path to csv file containing scene data for video + start_col: Header for the column containing the frame or timecode for the beginning of + each scene + end_col: Optional header for the column containing the frame or timecode for the end + of each scene. + framerate: Framerate for the input video, used for handling timecode <-> frame number + conversions if timecodes are used as input for marking cut points + """ + super().__init__() + self.framerate = framerate + + # Check to make specified csv file exists + if not csv_file: + raise ValueError('file path to csv file must be specified') + if not os.path.exists(csv_file): + raise ValueError('specified csv file does not exist') + + self.csv_file = csv_file + + # Open csv and check and read first row for column headers + self.file_reader = self._open_csv(self.csv_file) + csv_headers = next(self.file_reader) + + # Check to make sure column headers are present + if start_col not in csv_headers: + raise ValueError('specified column header for scene start is not present') + if end_col not in csv_headers: + raise ValueError('specified column header for scene end is not present') + + self.start_col = start_col + self.start_col_idx = csv_headers.index(start_col) + self.end_col = end_col + self.end_col_idx = csv_headers.index(end_col) + + self._last_scene_cut = None + self._last_scene_row = None + self._scene_start = None + self._scene_end = None + + def _open_csv(self, csv_file): + """Opens the specified csv file for reading. + + Arguments: + csv_file: Path to csv file containing scene data for video + + Returns: + file_reader: csv.reader object + """ + input_file = open(csv_file, 'r') + file_reader = csv.reader(input_file) + + return file_reader + + def _get_next_scene(self, file_reader, framerate=None): + """Reads the next scene information from the input csv file. + + Arguments: + file_reader: The csv.reader object for the detector + framerate: If timecodes are used as an input, a framerate is required for + timecode <-> frame number conversions + """ + try: + self._last_scene_row = next(file_reader) + except StopIteration: + # We have reached the end of the csv file, do not modify scene list + pass + + if framerate: + self._scene_start = FrameTimecode( + self._last_scene_row[self.start_col_idx], fps=self.framerate).frame_num + self._scene_end = FrameTimecode( + self._last_scene_row[self.end_col_idx], fps=self.framerate).frame_num + else: + self._scene_start = int(self._last_scene_row[self.start_col_idx]) - 1 + self._scene_end = int(self._last_scene_row[self.end_col_idx]) - 1 + + def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> List[int]: + """Simply reads cut data from a given csv file. Video is not analyzed. Therefore this + detector is incompatible with other detectors or a StatsManager. + + Arguments: + frame_num: Frame number of frame that is being passed. + frame_img: Decoded frame image (numpy.ndarray) to perform scene detection on. This is + unused for this detector as the video is not analyzed, but is allowed for + compatiblity. + + Returns: + cut_list: List of cuts (as provided by input csv file) + """ + cut_list = [] + + # First time through, read first row of input csv and get beginning/end frames + if not self._last_scene_row: + self._get_next_scene(self.file_reader, self.framerate) + # return cut_list + + # If frame_num is earlier than the first input scene, just return empty cut + if frame_num < self._scene_start: + return cut_list + + # If frame_num is at the beginning of the input scene, mark a cut and get next scene info + if frame_num == self._scene_start: + # To avoid duplication of beginning scene, check if next scene begins at frame 1 + # If so, just ignore this scene and get the next since it is added automatically + if frame_num == 0: + self._get_next_scene(self.file_reader, self.framerate) + return cut_list + + # We have hit a cut point, add it to the cut_list and get the next scene + cut_list.append(frame_num) + self._get_next_scene(self.file_reader, self.framerate) + + return cut_list + + def is_processing_required(self, frame_num): + return True