def extract_frames(video_path: str, output_frames_dir: str, notification_path: Optional[str]=None, quality: int=0) -> None: """Use ffmpeg to read a video file and extract the frames. Requires 'ffmpeg' to be on the command line. The resulting JPEG frames will be named in the format '%06d.jpg'. Uses notification_path to indicate whether the extraction completed; this will be named video_path'/.finished-extraction' if None. Will warn and write over the output dir if it exists but no file at notification_path exists. Quality must be between 0 and 31, where 0 is the highest. """ if notification_path is None: notification_path = pjoin(dirname(video_path), ".finished-extraction") if pexists(notification_path) and pexists(output_frames_dir): logger.info("Frames directory {} is already complete; skipping.".format(output_frames_dir)) else: if pexists(output_frames_dir): logger.warn("Frames directory {} already exists but is incomplete. Extracting frames...".format(output_frames_dir)) else: logger.info("Extracting frames into {}".format(output_frames_dir)) if not pexists(video_path): raise ValueError('Cannot extract frames: video.avi does not exist') wrap_cmd_call([ 'ffmpeg', '-i', video_path, '-q:v', quality, pjoin(output_frames_dir, '%06d.jpg') ]) with open(notification_path, 'w'): print('') # all done
def gen_video(video_path: str, input_frames_dir: str, video_frame_rate: str, input_image_extension: str = '.jpg', encoding: str = 'libx265', crf: int = 0) -> None: """Use ffmpeg to read generate a video from frames. Requires 'ffmpeg' to be on the command line. The resulting JPEG frames will be named in the format '%06d.jpg'. """ input_format_str = pjoin(input_frames_dir, "%06d{}".format(input_image_extension)) wrap_cmd_call([ 'ffmpeg', '-f', 'concat', # I don't remember why! '-r', video_frame_rate, # we don't know this because we just have frames '-safe', '0', # just allow any filename '-i', input_format_str, '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2', # compatibility with some players, including QuickTime '-c:v', encoding, '-crf', crf, '-pix_fmt', 'yuv420p', '-y', video_path ])
def fix_path(path: str) -> str: # ffmpeg won't recognize './' and will simply not write images! # and Python doesn't recognize ~ if '%' in path: raise ValueError( 'For technical limitations (regarding ffmpeg), local paths cannot contain a percent sign (%), but "{}" does' .format(path)) if path == '~': return os.environ['HOME'] # prevent out of bounds if path.startswith('~'): path = pjoin(os.environ['HOME'], path[2:]) return path.replace('./', '')
def fix_path_platform_dependent(path: str) -> str: """Modifies path strings to work with Python and external tools. Replaces a beginning '~' with the HOME environment variable. Also accepts either / or \ (but not both) as a path separator in windows. """ path = fix_path(path) # if windows, allow either / or \, but not both if platform.system() == 'Windows': bits = re.split('[/\\\\]', path) return pjoin(*bits).replace(":", ":\\") else: return path
def pjoin_sanitized_rel(*pieces: Iterable[any]) -> str: """Builds a path from a hierarchy, sanitizing the path by replacing /, :, <, >, ", ', \, |, ?, *, <DEL>, and control characters 0–32 with a hyphen-minus (-). Each input to pjoin_sanitized must refer only to a single directory or file (cannot contain a path separator). This means that you cannot have an absolute path (it would begin with os.path (probably /); use pjoin_sanitized_abs for this. """ return pjoin(*[_sanitize_bit(str(bit)) for bit in pieces])
def pjoin_sanitized_abs(*pieces: Iterable[any]) -> str: """Same as pjoin_sanitized_rel but starts with os.sep (the root directory).""" return pjoin(os.sep, pjoin_sanitized_rel(*pieces))