def write_frames_of_scenes(video_path, output_dir, vid_name, l_scene_end): vid_read = get_reader(video_path) curr_scene = 1 frame_num = 0 for i, frame in tqdm(enumerate(vid_read)): if i in l_scene_end: curr_scene += 1 frame_num = 0 frame_num += 1 file_name = '{}-scene{}-frame{}.png'.format(vid_name, curr_scene, frame_num) cv2.imwrite( get_and_create_path(file_name, output_dir + '/scene' + str(curr_scene)), cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) print('Completed writing files')
def parse_options(self, input_list, framerate, stats_file, downscale, frame_skip, min_scene_len, drop_short_scenes): # 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 the [options] when calling scenedetect [options] [commands [command options]]. This method calls the _init_video_manager(), _open_stats_file(), and check_input_open() methods, which may raise a click.BadParameter exception. Raises: click.BadParameter """ if not input_list: return self.logger.debug('Parsing program options.') self.frame_skip = frame_skip video_manager_initialized = self._init_video_manager( input_list=input_list, framerate=framerate, downscale=downscale) # Ensure VideoManager is initialized, and open StatsManager if --stats is specified. if not video_manager_initialized: self.video_manager = None self.logger.info('VideoManager not initialized.') else: self.logger.debug('VideoManager initialized.') self.stats_file_path = get_and_create_path(stats_file, self.output_directory) if self.stats_file_path is not None: self.check_input_open() self._open_stats_file() # Init SceneManager. self.scene_manager = SceneManager(self.stats_manager) self.drop_short_scenes = drop_short_scenes self.min_scene_len = parse_timecode(self, min_scene_len) self.options_processed = True
def process_input(self): # type: () -> None """ Process Input: Processes input video(s) and generates output as per CLI commands. Run after all command line options/sub-commands have been parsed. """ logging.debug('Processing input...') if not self.options_processed: logging.debug( 'Skipping processing, CLI options were not parsed successfully.' ) return self.check_input_open() if not self.scene_manager.get_num_detectors() > 0: logging.error( 'No scene detectors specified (detect-content, detect-threshold, etc...),\n' ' or failed to process all command line arguments.') return # Handle scene detection commands (detect-content, detect-threshold, etc...). self.video_manager.start() base_timecode = self.video_manager.get_base_timecode() start_time = time.time() logging.info('Detecting scenes...') num_frames = self.scene_manager.detect_scenes( frame_source=self.video_manager, frame_skip=self.frame_skip, show_progress=not self.quiet_mode) duration = time.time() - start_time logging.info('Processed %d frames in %.1f seconds (average %.2f FPS).', num_frames, duration, float(num_frames) / duration) # Handle -s/--statsfile option. if self.stats_file_path is not None: if self.stats_manager.is_save_required(): with open(self.stats_file_path, 'wt') as stats_file: logging.info('Saving frame metrics to stats file: %s', os.path.basename(self.stats_file_path)) self.stats_manager.save_to_csv(stats_file, base_timecode) else: logging.debug( 'No frame metrics updated, skipping update of the stats file.' ) # Get list of detected cuts and scenes from the SceneManager to generate the required output # 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) video_paths = self.video_manager.get_video_paths() video_name = os.path.basename(video_paths[0]) if video_name.rfind('.') >= 0: video_name = video_name[:video_name.rfind('.')] # Ensure we don't divide by zero. if scene_list: logging.info( 'Detected %d scenes, average shot length %.1f seconds.', len(scene_list), sum([(end_time - start_time).get_seconds() for start_time, end_time in scene_list]) / float(len(scene_list))) else: logging.info('No scenes detected.') # Handle list-scenes command. if self.scene_list_output: scene_list_filename = Template( self.scene_list_name_format).safe_substitute( VIDEO_NAME=video_name) if not scene_list_filename.lower().endswith('.csv'): scene_list_filename += '.csv' scene_list_path = get_and_create_path( scene_list_filename, self.scene_list_directory if self.scene_list_directory is not None else self.output_directory) logging.info('Writing scene list to CSV file:\n %s', scene_list_path) with open(scene_list_path, 'wt') as scene_list_file: write_scene_list(scene_list_file, scene_list, cut_list) # Handle `list-scenes`. if self.print_scene_list: logging.info( """Scene List: ----------------------------------------------------------------------- | Scene # | Start Frame | Start Time | End Frame | End Time | ----------------------------------------------------------------------- %s ----------------------------------------------------------------------- """, '\n'.join([ ' | %5d | %11d | %s | %11d | %s |' % (i + 1, start_time.get_frames(), start_time.get_timecode(), end_time.get_frames(), end_time.get_timecode()) for i, (start_time, end_time) in enumerate(scene_list) ])) if cut_list: logging.info('Comma-separated timecode list:\n %s', ','.join([cut.get_timecode() for cut in cut_list])) # Handle save-images command. if self.save_images: self._generate_images( scene_list=scene_list, video_name=video_name, image_name_template=self.image_name_format, output_dir=self.image_directory, downscale_factor=self.video_manager.get_downscale_factor()) # Handle export-html command. if self.export_html: html_filename = Template( self.html_name_format).safe_substitute(VIDEO_NAME=video_name) if not html_filename.lower().endswith('.html'): html_filename += '.html' html_path = get_and_create_path( html_filename, self.image_directory if self.image_directory is not None else self.output_directory) logging.info('Exporting to html file:\n %s:', html_path) if not self.html_include_images: self.image_filenames = None write_scene_list_html(html_path, scene_list, cut_list, image_filenames=self.image_filenames, image_width=self.image_width, image_height=self.image_height) # Handle split-video command. if self.split_video: # Add proper extension to filename template if required. dot_pos = self.split_name_format.rfind('.') if self.split_mkvmerge and not self.split_name_format.endswith( '.mkv'): self.split_name_format += '.mkv' # Don't add if we find an extension between 2 and 4 characters elif not (dot_pos >= 0) or (dot_pos >= 0 and not ((len(self.split_name_format) - (dot_pos + 1) <= 4 >= 2))): self.split_name_format += '.mp4' output_file_prefix = get_and_create_path( self.split_name_format, self.split_directory if self.split_directory is not None else self.output_directory) mkvmerge_available = is_mkvmerge_available() ffmpeg_available = is_ffmpeg_available() if mkvmerge_available and (self.split_mkvmerge or not ffmpeg_available): if not self.split_mkvmerge: logging.warning( 'ffmpeg not found, falling back to fast copy mode (split-video -c/--copy).' ) split_video_mkvmerge(video_paths, scene_list, output_file_prefix, video_name, suppress_output=self.quiet_mode or self.split_quiet) elif ffmpeg_available: if self.split_mkvmerge: logging.warning( 'mkvmerge not found, falling back to normal splitting' ' mode (split-video).') split_video_ffmpeg(video_paths, scene_list, output_file_prefix, video_name, arg_override=self.split_args, hide_progress=self.quiet_mode, suppress_output=self.quiet_mode or self.split_quiet) else: if not (mkvmerge_available or ffmpeg_available): error_strs = [ "ffmpeg/mkvmerge is required for split-video [-c/--copy]." ] else: error_strs = [ "{EXTERN_TOOL} is required for split-video{EXTRA_ARGS}." .format(EXTERN_TOOL='mkvmerge' if self.split_mkvmerge else 'ffmpeg', EXTRA_ARGS=' -c/--copy' if self.split_mkvmerge else '') ] error_strs += [ "Install one of the above tools to enable the split-video command." ] error_str = '\n'.join(error_strs) logging.debug(error_str) raise click.BadParameter(error_str, param_hint='split-video') if scene_list: logging.info( 'Video splitting completed, individual scenes written to disk.' )
def _generate_images( self, scene_list, video_name, image_name_template='$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER', output_dir=None, downscale_factor=1): # type: (List[Tuple[FrameTimecode, FrameTimecode]) -> None if not scene_list: return if not self.options_processed: return if self.num_images <= 0: raise ValueError() self.check_input_open() imwrite_param = [] if self.image_param is not None: imwrite_param = [ self.imwrite_params[self.image_extension], self.image_param ] # Reset video manager and downscale factor. self.video_manager.release() self.video_manager.reset() self.video_manager.set_downscale_factor(1) self.video_manager.start() # Setup flags and init progress bar if available. completed = True logging.info('Generating output images (%d per scene)...', self.num_images) progress_bar = None if tqdm and not self.quiet_mode: progress_bar = tqdm(total=len(scene_list) * self.num_images, unit='images') filename_template = Template(image_name_template) scene_num_format = '%0' scene_num_format += str( max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + 'd' image_num_format = '%0' image_num_format += str(math.floor(math.log(self.num_images, 10)) + 2) + 'd' timecode_list = dict() fps = scene_list[0][0].framerate timecode_list = [ [ FrameTimecode(int(f), fps=fps) for f in [ # middle frames a[len(a) // 2] if ( 0 < j < self.num_images - 1) or self.num_images == 1 # first frame else min(a[0] + self.image_frame_margin, a[-1]) if j == 0 # last frame else max(a[-1] - self.image_frame_margin, a[0]) # for each evenly-split array of frames in the scene list for j, a in enumerate(np.array_split(r, self.num_images)) ] ] for i, r in enumerate([ # pad ranges to number of images r if r.stop - r.start >= self.num_images else list(r) + [r.stop - 1] * (self.num_images - len(r)) # create range of frames in scene for r in ( range(start.get_frames(), end.get_frames()) # for each scene in scene list for start, end in scene_list) ]) ] self.image_filenames = {i: [] for i in range(len(timecode_list))} for i, tl in enumerate(timecode_list): for j, image_timecode in enumerate(tl): self.video_manager.seek(image_timecode) self.video_manager.grab() ret_val, frame_im = self.video_manager.retrieve() if downscale_factor != 1: logging.info("resizing thumb") scale_percent = 1 / downscale_factor width = int(frame_im.shape[1] * scale_percent) height = int(frame_im.shape[0] * scale_percent) resized = cv2.resize(frame_im, (width, height), interpolation=cv2.INTER_AREA) frame_im = resized if ret_val: file_path = '%s.%s' % (filename_template.safe_substitute( VIDEO_NAME=video_name, SCENE_NUMBER=scene_num_format % (i + 1), IMAGE_NUMBER=image_num_format % (j + 1), FRAME_NUMBER=image_timecode.get_frames()), self.image_extension) self.image_filenames[i].append(file_path) cv2.imwrite( get_and_create_path( file_path, output_dir if output_dir is not None else self.output_directory), frame_im, imwrite_param) else: completed = False break if progress_bar: progress_bar.update(1) if not completed: logging.error('Could not generate all output images.')
def process_input(self): # type: () -> None """ Process Input: Processes input video(s) and generates output as per CLI commands. Run after all command line options/sub-commands have been parsed. """ self.logger.debug('Processing input...') if not self.options_processed: self.logger.debug( 'Skipping processing, CLI options were not parsed successfully.' ) return self.check_input_open() assert self.scene_manager.get_num_detectors() >= 0 if self.scene_manager.get_num_detectors() == 0: self.logger.error( 'No scene detectors specified (detect-content, detect-threshold, etc...),\n' ' or failed to process all command line arguments.') return # Display a warning if the video codec type seems unsupported (#86). if int(abs(self.video_manager.get(cv2.CAP_PROP_FOURCC))) == 0: self.logger.error( 'Video codec detection failed, output may be incorrect.\nThis could be caused' ' by using an outdated version of OpenCV, or using codecs that currently are' ' not well supported (e.g. VP9).\n' 'As a workaround, consider re-encoding the source material before processing.\n' 'For details, see https://github.com/Breakthrough/PySceneDetect/issues/86' ) # Handle scene detection commands (detect-content, detect-threshold, etc...). self.video_manager.start() start_time = time.time() self.logger.info('Detecting scenes...') num_frames = self.scene_manager.detect_scenes( frame_source=self.video_manager, frame_skip=self.frame_skip, show_progress=not self.quiet_mode) # Handle case where video fails with multiple audio tracks (#179). # TODO: Using a different video backend as per #213 may also resolve this issue, # as well as numerous other timing related issues. if num_frames <= 0: self.logger.critical( 'Failed to read any frames from video file. This could be caused' ' by the video having multiple audio tracks. If so, please try' ' removing the audio tracks or muxing to mkv via:\n' ' ffmpeg -i input.mp4 -c copy -an output.mp4\n' 'or:\n' ' mkvmerge -o output.mkv input.mp4\n' 'For details, see https://pyscenedetect.readthedocs.io/en/latest/faq/' ) return duration = time.time() - start_time self.logger.info( 'Processed %d frames in %.1f seconds (average %.2f FPS).', num_frames, duration, float(num_frames) / duration) # Handle -s/--statsfile option. if self.stats_file_path is not None: if self.stats_manager.is_save_required(): with open(self.stats_file_path, 'wt') as stats_file: self.logger.info('Saving frame metrics to stats file: %s', os.path.basename(self.stats_file_path)) base_timecode = self.video_manager.get_base_timecode() self.stats_manager.save_to_csv(stats_file, base_timecode) else: self.logger.debug( 'No frame metrics updated, skipping update of the stats file.' ) # Get list of detected cuts and scenes from the SceneManager to generate the required output # files with based on the given commands (list-scenes, split-video, save-images, etc...). cut_list = self.scene_manager.get_cut_list() scene_list = self.scene_manager.get_scene_list() # Handle --drop-short-scenes. if self.drop_short_scenes and self.min_scene_len > 0: scene_list = [ s for s in scene_list if (s[1] - s[0]) >= self.min_scene_len ] video_paths = self.video_manager.get_video_paths() video_name = self.video_manager.get_video_name() if scene_list: # Ensure we don't divide by zero. self.logger.info( 'Detected %d scenes, average shot length %.1f seconds.', len(scene_list), sum([(end_time - start_time).get_seconds() for start_time, end_time in scene_list]) / float(len(scene_list))) else: self.logger.info('No scenes detected.') # Handle list-scenes command. if self.scene_list_output: scene_list_filename = Template( self.scene_list_name_format).safe_substitute( VIDEO_NAME=video_name) if not scene_list_filename.lower().endswith('.csv'): scene_list_filename += '.csv' scene_list_path = get_and_create_path( scene_list_filename, self.scene_list_directory if self.scene_list_directory is not None else self.output_directory) self.logger.info('Writing scene list to CSV file:\n %s', scene_list_path) with open(scene_list_path, 'wt') as scene_list_file: write_scene_list(output_csv_file=scene_list_file, scene_list=scene_list, include_cut_list=not self.skip_cuts, cut_list=cut_list) if self.print_scene_list: self.logger.info( """Scene List: ----------------------------------------------------------------------- | Scene # | Start Frame | Start Time | End Frame | End Time | ----------------------------------------------------------------------- %s ----------------------------------------------------------------------- """, '\n'.join([ ' | %5d | %11d | %s | %11d | %s |' % (i + 1, start_time.get_frames(), start_time.get_timecode(), end_time.get_frames(), end_time.get_timecode()) for i, (start_time, end_time) in enumerate(scene_list) ])) if cut_list: self.logger.info( 'Comma-separated timecode list:\n %s', ','.join([cut.get_timecode() for cut in cut_list])) # Handle save-images command. if self.save_images: image_output_dir = self.output_directory if self.image_directory is not None: image_output_dir = self.image_directory image_filenames = save_images( scene_list=scene_list, video_manager=self.video_manager, num_images=self.num_images, frame_margin=self.frame_margin, image_extension=self.image_extension, encoder_param=self.image_param, image_name_template=self.image_name_format, output_dir=image_output_dir, show_progress=not self.quiet_mode, scale=self.scale, height=self.height, width=self.width) # Handle export-html command. if self.export_html: html_filename = Template( self.html_name_format).safe_substitute(VIDEO_NAME=video_name) if not html_filename.lower().endswith('.html'): html_filename += '.html' html_path = get_and_create_path( html_filename, self.image_directory if self.image_directory is not None else self.output_directory) self.logger.info('Exporting to html file:\n %s:', html_path) if not self.html_include_images: image_filenames = None write_scene_list_html(html_path, scene_list, cut_list, image_filenames=image_filenames, image_width=self.image_width, image_height=self.image_height) # Handle split-video command. if self.split_video: output_path_template = self.split_name_format # Add proper extension to filename template if required. dot_pos = output_path_template.rfind('.') extension_length = 0 if dot_pos < 0 else len( output_path_template) - (dot_pos + 1) # If using mkvmerge, force extension to .mkv. if self.split_mkvmerge and not output_path_template.endswith( '.mkv'): output_path_template += '.mkv' # Otherwise, if using ffmpeg, only add an extension if one doesn't exist. elif not 2 <= extension_length <= 4: output_path_template += '.mp4' output_path_template = get_and_create_path( output_path_template, self.split_directory if self.split_directory is not None else self.output_directory) # Ensure the appropriate tool is available before handling split-video. check_split_video_requirements(self.split_mkvmerge) if self.split_mkvmerge: split_video_mkvmerge(video_paths, scene_list, output_path_template, video_name, suppress_output=self.quiet_mode or self.split_quiet) else: split_video_ffmpeg(video_paths, scene_list, output_path_template, video_name, arg_override=self.split_args, hide_progress=self.quiet_mode, suppress_output=self.quiet_mode or self.split_quiet) if scene_list: self.logger.info( 'Video splitting completed, individual scenes written to disk.' )
def save_images(scene_list, video_manager, num_images=3, frame_margin=1, image_extension='jpg', encoder_param=95, image_name_template='$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER', output_dir=None, downscale_factor=1, show_progress=False, scale=None, height=None, width=None): # type: (List[Tuple[FrameTimecode, FrameTimecode]], VideoManager, # Optional[int], Optional[int], Optional[str], Optional[int], # Optional[str], Optional[str], Optional[int], Optional[bool], # Optional[float], Optional[int], Optional[int]) # -> Dict[List[str]] """ Saves a set number of images from each scene, given a list of scenes and the associated video/frame source. Arguments: scene_list: A list of scenes (pairs of FrameTimecode objects) returned from calling a SceneManager's detect_scenes() method. video_manager: A VideoManager object corresponding to the scene list. Note that the video will be closed/re-opened and seeked through. num_images: Number of images to generate for each scene. Minimum is 1. frame_margin: Number of frames to pad each scene around the beginning and end (e.g. moves the first/last image into the scene by N frames). Can set to 0, but will result in some video files failing to extract the very last frame. image_extension: Type of image to save (must be one of 'jpg', 'png', or 'webp'). encoder_param: Quality/compression efficiency, based on type of image: 'jpg' / 'webp': Quality 0-100, higher is better quality. 100 is lossless for webp. 'png': Compression from 1-9, where 9 achieves best filesize but is slower to encode. image_name_template: Template to use when creating the images on disk. Can use the macros $VIDEO_NAME, $SCENE_NUMBER, and $IMAGE_NUMBER. The image extension is applied automatically as per the argument image_extension. output_dir: Directory to output the images into. If not set, the output is created in the working directory. downscale_factor: Integer factor to downscale images by. No filtering is currently done, only downsampling (thus requiring an integer). show_progress: If True, shows a progress bar if tqdm is installed. scale: Optional factor by which to rescale saved images.A scaling factor of 1 would not result in rescaling. A value <1 results in a smaller saved image, while a value >1 results in an image larger than the original. This value is ignored if either the height or width values are specified. height: Optional value for the height of the saved images. Specifying both the height and width will resize images to an exact size, regardless of aspect ratio. Specifying only height will rescale the image to that number of pixels in height while preserving the aspect ratio. width: Optional value for the width of the saved images. Specifying both the width and height will resize images to an exact size, regardless of aspect ratio. Specifying only width will rescale the image to that number of pixels wide while preserving the aspect ratio. Returns: Dict[List[str]]: Dictionary of the format { scene_num : [image_paths] }, where scene_num is the number of the scene in scene_list (starting from 1), and image_paths is a list of the paths to the newly saved/created images. Raises: ValueError: Raised if any arguments are invalid or out of range (e.g. if num_images is negative). """ if not scene_list: return {} if num_images <= 0 or frame_margin < 0: raise ValueError() # TODO: Validate that encoder_param is within the proper range. # Should be between 0 and 100 (inclusive) for jpg/webp, and 1-9 for png. imwrite_param = [get_cv2_imwrite_params()[image_extension], encoder_param] if encoder_param is not None else [] video_name = video_manager.get_video_name() # Reset video manager and downscale factor. video_manager.release() video_manager.reset() video_manager.set_downscale_factor(downscale_factor) video_manager.start() # Setup flags and init progress bar if available. completed = True logging.info('Generating output images (%d per scene)...', num_images) progress_bar = None if show_progress and tqdm: progress_bar = tqdm( total=len(scene_list) * num_images, unit='images', dynamic_ncols=True) filename_template = Template(image_name_template) scene_num_format = '%0' scene_num_format += str(max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + 'd' image_num_format = '%0' image_num_format += str(math.floor(math.log(num_images, 10)) + 2) + 'd' timecode_list = dict() fps = scene_list[0][0].framerate timecode_list = [ [ FrameTimecode(int(f), fps=fps) for f in [ # middle frames a[len(a)//2] if (0 < j < num_images-1) or num_images == 1 # first frame else min(a[0] + frame_margin, a[-1]) if j == 0 # last frame else max(a[-1] - frame_margin, a[0]) # for each evenly-split array of frames in the scene list for j, a in enumerate(np.array_split(r, num_images)) ] ] for i, r in enumerate([ # pad ranges to number of images r if 1+r[-1]-r[0] >= num_images else list(r) + [r[-1]] * (num_images - len(r)) # create range of frames in scene for r in ( range(start.get_frames(), end.get_frames()) # for each scene in scene list for start, end in scene_list ) ]) ] image_filenames = {i: [] for i in range(len(timecode_list))} aspect_ratio = get_aspect_ratio(video_manager) if abs(aspect_ratio - 1.0) < 0.01: aspect_ratio = None for i, scene_timecodes in enumerate(timecode_list): for j, image_timecode in enumerate(scene_timecodes): video_manager.seek(image_timecode) ret_val, frame_im = video_manager.read() if ret_val: file_path = '%s.%s' % ( filename_template.safe_substitute( VIDEO_NAME=video_name, SCENE_NUMBER=scene_num_format % (i + 1), IMAGE_NUMBER=image_num_format % (j + 1), FRAME_NUMBER=image_timecode.get_frames()), image_extension) image_filenames[i].append(file_path) if aspect_ratio is not None: frame_im = cv2.resize( frame_im, (0, 0), fx=aspect_ratio, fy=1.0, interpolation=cv2.INTER_CUBIC) # Get frame dimensions prior to resizing or scaling frame_height = frame_im.shape[0] frame_width = frame_im.shape[1] # Figure out what kind of resizing needs to be done if height and width: frame_im = cv2.resize( frame_im, (width, height), interpolation=cv2.INTER_CUBIC) elif height and not width: factor = height / float(frame_height) width = int(factor * frame_width) frame_im = cv2.resize( frame_im, (width, height), interpolation=cv2.INTER_CUBIC) elif width and not height: factor = width / float(frame_width) height = int(factor * frame_height) frame_im = cv2.resize( frame_im, (width, height), interpolation=cv2.INTER_CUBIC) elif scale: frame_im = cv2.resize( frame_im, (0, 0), fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC) cv2.imwrite( get_and_create_path(file_path, output_dir), frame_im, imwrite_param) else: completed = False break if progress_bar: progress_bar.update(1) if not completed: logging.error('Could not generate all output images.') return image_filenames
def generate_images( scene_list, video_manager, video_name, num_images=2, image_extension='jpg', quality_or_compression=95, image_name_template='$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER', output_dir=None, downscale_factor=1, show_progress=False): # type: (...) -> bool """ TODO: Documentation. Arguments: quality_or_compression: For image_extension=jpg or webp, represents encoding quality, from 0-100 (higher indicates better quality). For WebP, 100 indicates lossless. Default value in the CLI is 95 for JPEG, and 100 for WebP. If image_extension=png, represents the compression rate, from 0-9. Higher values produce smaller files but result in longer compression time. This setting does not affect image quality (lossless PNG), only file size. Default value in the CLI is 3. [default: 95] Returns: True if all requested images were generated & saved successfully, False otherwise. """ if not scene_list: return True if num_images <= 0: raise ValueError() imwrite_param = [] available_extensions = get_cv2_imwrite_params() if quality_or_compression is not None: if image_extension in available_extensions: imwrite_param = [ available_extensions[image_extension], quality_or_compression ] else: valid_extensions = str(list(available_extensions.keys())) raise RuntimeError( 'Invalid image extension, must be one of (case-sensitive): %s' % valid_extensions) # Reset video manager and downscale factor. video_manager.release() video_manager.reset() video_manager.set_downscale_factor(downscale_factor) video_manager.start() # Setup flags and init progress bar if available. completed = True progress_bar = None if tqdm and show_progress: progress_bar = tqdm(total=len(scene_list) * num_images, unit='images') filename_template = Template(image_name_template) scene_num_format = '%0' scene_num_format += str( max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + 'd' image_num_format = '%0' image_num_format += str(math.floor(math.log(num_images, 10)) + 2) + 'd' timecode_list = dict() for i in range(len(scene_list)): timecode_list[i] = [] if num_images == 1: for i, (start_time, end_time) in enumerate(scene_list): duration = end_time - start_time timecode_list[i].append(start_time + int(duration.get_frames() / 2)) else: middle_images = num_images - 2 for i, (start_time, end_time) in enumerate(scene_list): timecode_list[i].append(start_time) if middle_images > 0: duration = (end_time.get_frames() - 1) - start_time.get_frames() duration_increment = None duration_increment = int(duration / (middle_images + 1)) for j in range(middle_images): timecode_list[i].append(start_time + ((j + 1) * duration_increment)) # End FrameTimecode is always the same frame as the next scene's start_time # (one frame past the end), so we need to subtract 1 here. timecode_list[i].append(end_time - 1) for i in timecode_list: for j, image_timecode in enumerate(timecode_list[i]): video_manager.seek(image_timecode) video_manager.grab() ret_val, frame_im = video_manager.retrieve() if ret_val: file_path = '%s.%s' % (filename_template.safe_substitute( VIDEO_NAME=video_name, SCENE_NUMBER=scene_num_format % (i + 1), IMAGE_NUMBER=image_num_format % (j + 1)), image_extension) cv2.imwrite(get_and_create_path(file_path, output_dir), frame_im, imwrite_param) else: completed = False break if progress_bar: progress_bar.update(1) return completed
def save_bbox_to_file(start, end, fps, tube_bbox, frame_shape, cur_traj, inp, image_shape, vid_reader, increase_area=0.1, aspect_rat=True, pan_thresh=None, fa=None, rm_low_res=None, model=None, device=None, half=None): left, top, right, bot = tube_bbox width = right - left height = bot - top # Computing aspect preserving bbox width_increase = max(increase_area, ((1 + 2 * increase_area) * height - width) / (2 * width)) height_increase = max(increase_area, ((1 + 2 * increase_area) * width - height) / (2 * height)) left = int(left - width_increase * width) top = int(top - height_increase * height) right = int(right + width_increase * width) bot = int(bot + height_increase * height) top, bot, left, right = max(0, top), min(bot, frame_shape[0]), max( 0, left), min(right, frame_shape[1]) h, w = bot - top, right - left # making sure that h and w are the same if aspect_rat: if h != w: # change right and left boundaries of box if height is greater than width if h < w: dif = w - h if dif % 2 == 0: right -= int(dif / 2) left += int(dif / 2) else: right -= (dif // 2) + 1 left += dif // 2 h, w = bot - top, right - left # change top and bottom boundaries of box if width is greater than height, else: dif = h - w if dif % 2 == 0: bot -= int(dif / 2) top += int(dif / 2) else: bot -= (dif // 2) + 1 top += dif // 2 h, w = bot - top, right - left if rm_low_res: if h < rm_low_res and w < rm_low_res: return if pan_thresh: end_frame = vid_reader.get_data(end) start_frame = vid_reader.get_data(start) end_bbox = extract_bbox(end_frame, fa, model, device, half) start_bbox = extract_bbox(start_frame, fa, model, device, half) # get bboxes closest to tube bbox (that represents the bbox for the current trajetory) if len(start_bbox) == 0: return if len(start_bbox) == 1: start_bbox = start_bbox[0] else: max_iou = 0 bbox_num = 0 for i in range(len(start_bbox)): cur_iou = bb_intersection_over_union(tube_bbox, start_bbox[i]) if cur_iou > max_iou: max_iou = cur_iou bbox_num = i start_bbox = start_bbox[bbox_num] print(' TUBE START', start_bbox) # same thing for ending frame if len(end_bbox) == 0: return if len(end_bbox) == 1: end_bbox = end_bbox[0] else: max_iou = 0 bbox_num = 0 for i in range(len(end_bbox)): cur_iou = bb_intersection_over_union(tube_bbox, end_bbox[i]) if cur_iou < max_iou: max_iou = cur_iou bbox_num = i end_bbox = end_bbox[bbox_num] print(' TUBE END', end_bbox) if bb_intersection_over_union(end_bbox, start_bbox) < pan_thresh: return # end_start_iou = bb_intersection_over_union(end_bbox, start_bbox) # if end_start_iou > pan_thresh: # return for frame in range(start, end): cur_frame = vid_reader.get_data(frame) # cropped_frame = cur_frame[top:bot, left:right, :] cropped_frame = cv2.cvtColor(cur_frame[top:bot, left:right, :], cv2.COLOR_RGB2BGR) if h > image_shape[0] and w > image_shape[1]: resized_frame = cv2.resize(cropped_frame, dsize=(image_shape[0], image_shape[1]), interpolation=cv2.INTER_AREA) else: resized_frame = cv2.resize(cropped_frame, dsize=(image_shape[0], image_shape[1]), interpolation=cv2.INTER_CUBIC) file_name = '{}-traj{}-frame{}.png'.format(args.name, cur_traj, frame - start + 1) cv2.imwrite( get_and_create_path( file_name, args.out + '/' + args.name + '_traj' + str(cur_traj)), resized_frame)
def scenedetect_cli(ctx, input, output, framerate, downscale, frame_skip, min_scene_len, drop_short_scenes, stats, verbosity, logfile, quiet): """ For example: scenedetect -i video.mp4 -s video.stats.csv detect-content list-scenes Note that the following options represent [OPTIONS] above. To list the optional [ARGS] for a particular COMMAND, type `scenedetect help COMMAND`. You can also combine commands (e.g. scenedetect [...] detect-content save-images --png split-video). """ ctx.call_on_close(ctx.obj.process_input) logging.disable(logging.NOTSET) format_str = '[PySceneDetect] %(message)s' if verbosity.lower() == 'none': verbosity = None elif verbosity.lower() == 'debug': format_str = '%(levelname)s: %(module)s.%(funcName)s(): %(message)s' if quiet: verbosity = None ctx.obj.quiet_mode = True if verbosity is None else False ctx.obj.output_directory = output if logfile is not None: logfile = get_and_create_path(logfile) logging.basicConfig(filename=logfile, filemode='a', format=format_str, level=getattr(logging, verbosity.upper()) if verbosity is not None else verbosity) elif verbosity is not None: logging.basicConfig(format=format_str, level=getattr(logging, verbosity.upper())) else: logging.disable(logging.CRITICAL) logging.info('PySceneDetect %s', scenedetect.__version__) if stats is not None and frame_skip != 0: ctx.obj.options_processed = False error_strs = [ 'Unable to detect scenes with stats file if frame skip is not 1.', ' Either remove the -fs/--frame-skip option, or the -s/--stats file.\n' ] logging.error('\n'.join(error_strs)) raise click.BadParameter( '\n Combining the -s/--stats and -fs/--frame-skip options is not supported.', param_hint='frame skip + stats file') try: if ctx.obj.output_directory is not None: 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_scene_len=min_scene_len, drop_short_scenes=drop_short_scenes) except Exception as ex: logging.error('Could not parse CLI options.: %s', ex) raise
def find_scenes(video_path, generate_images=False): """ This method slicing a video to a list of scenes, each scene will have a similar color distributions. This function allows to generate images for each scene. :param video_path: The path to the video for finding scenes :param generate_images: whether to generate images or not :return: a list of scenes """ video_manager = VideoManager([video_path]) stats_manager = StatsManager() scene_manager = SceneManager(stats_manager) scene_manager.add_detector(ContentDetector()) base_timecode = video_manager.get_base_timecode() stats_file_path = '%s.stats.csv' % video_path scene_list = [] output = [] try: if os.path.exists(stats_file_path): with open(stats_file_path, 'r') as stats_file: stats_manager.load_from_csv(stats_file, base_timecode) video_manager.set_downscale_factor() video_manager.start() scene_manager.detect_scenes(frame_source=video_manager) scene_list = scene_manager.get_scene_list(base_timecode) print("Starting to generate images from scenelist") num_images = 2 if not scene_list: return available_extensions = get_cv2_imwrite_params() image_extension = "jpg" imwrite_param = [available_extensions[image_extension], 100] video_manager.release() video_manager.reset() video_manager.set_downscale_factor(1) video_manager.start() completed = True print('Generating output images (%d per scene)...', num_images) filename_template = Template( "$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER") scene_num_format = '%0' scene_num_format += str( max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + 'd' image_num_format = '%0' image_num_format += str(math.floor(math.log(num_images, 10)) + 2) + 'd' timecode_list = dict() fps = scene_list[0][0].framerate timecode_list = [[ FrameTimecode(int(f), fps=fps) for f in [ a[len(a) // 2] if (0 < j < num_images - 1) or num_images == 1 else min(a[0] + 0, a[-1]) if j == 0 else max(a[-1] - 0, a[0]) for j, a in enumerate(np.array_split(r, num_images)) ] ] for i, r in enumerate([ r if r.stop - r.start >= num_images else list(r) + [r.stop - 1] * (num_images - len(r)) for r in (range(start.get_frames(), end.get_frames()) for start, end in scene_list) ])] image_filenames = {i: [] for i in range(len(timecode_list))} for i, tl in enumerate(timecode_list): for j, image_timecode in enumerate(tl): video_manager.seek(image_timecode) video_manager.grab() ret_val, frame_im = video_manager.retrieve() if ret_val: file_path = '%s.%s' % (filename_template.safe_substitute( VIDEO_NAME=video_path, SCENE_NUMBER=scene_num_format % (i + 1), IMAGE_NUMBER=image_num_format % (j + 1), FRAME_NUMBER=image_timecode.get_frames()), image_extension) image_filenames[i].append(file_path) abs_file_path = get_and_create_path(file_path, "output") output.append(frame_im) if generate_images: print(abs_file_path) cv2.imwrite(abs_file_path, frame_im, imwrite_param) else: completed = False break if not completed: print('Could not generate all output images.') finally: video_manager.release() return output
def _generate_images( self, scene_list, video_name, image_name_template='$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER', output_dir=None): # type: (List[Tuple[FrameTimecode, FrameTimecode]) -> None if not scene_list: return if not self.options_processed: return if self.num_images <= 0: raise ValueError() self.check_input_open() imwrite_param = [] if self.image_param is not None: imwrite_param = [ self.imwrite_params[self.image_extension], self.image_param ] # Reset video manager and downscale factor. self.video_manager.release() self.video_manager.reset() self.video_manager.set_downscale_factor(1) self.video_manager.start() # Setup flags and init progress bar if available. completed = True logging.info('Generating output images (%d per scene)...', self.num_images) progress_bar = None if tqdm and not self.quiet_mode: progress_bar = tqdm(total=len(scene_list) * self.num_images, unit='images') filename_template = Template(image_name_template) scene_num_format = '%0' scene_num_format += str( max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + 'd' image_num_format = '%0' image_num_format += str(math.floor(math.log(self.num_images, 10)) + 2) + 'd' timecode_list = dict() self.image_filenames = dict() for i in range(len(scene_list)): timecode_list[i] = [] self.image_filenames[i] = [] if self.num_images == 1: for i, (start_time, end_time) in enumerate(scene_list): duration = end_time - start_time timecode_list[i].append(start_time + int(duration.get_frames() / 2)) else: middle_images = self.num_images - 2 for i, (start_time, end_time) in enumerate(scene_list): timecode_list[i].append(start_time) if middle_images > 0: duration = (end_time.get_frames() - 1) - start_time.get_frames() duration_increment = None duration_increment = int(duration / (middle_images + 1)) for j in range(middle_images): timecode_list[i].append(start_time + ((j + 1) * duration_increment)) # End FrameTimecode is always the same frame as the next scene's start_time # (one frame past the end), so we need to subtract 1 here. timecode_list[i].append(end_time - 1) for i in timecode_list: for j, image_timecode in enumerate(timecode_list[i]): self.video_manager.seek(image_timecode) self.video_manager.grab() ret_val, frame_im = self.video_manager.retrieve() if ret_val: file_path = '%s.%s' % (filename_template.safe_substitute( VIDEO_NAME=video_name, SCENE_NUMBER=scene_num_format % (i + 1), IMAGE_NUMBER=image_num_format % (j + 1)), self.image_extension) self.image_filenames[i].append(file_path) cv2.imwrite( get_and_create_path( file_path, output_dir if output_dir is not None else self.output_directory), frame_im, imwrite_param) else: completed = False break if progress_bar: progress_bar.update(1) if not completed: logging.error('Could not generate all output images.')