|
| 1 | +from moviepy.editor import VideoFileClip, ImageSequenceClip |
| 2 | +import numpy as np |
| 3 | +import os |
| 4 | +from datetime import timedelta, datetime |
| 5 | +from glob import glob |
| 6 | +from tqdm import tqdm |
| 7 | +import shutil |
| 8 | + |
| 9 | +# i.e if video of duration 30 seconds, saves 10 frame per second = 300 frames saved in total |
| 10 | +SAVING_FRAMES_PER_SECOND = 30 |
| 11 | + |
| 12 | +def format_timedelta(td): |
| 13 | + """Utility function to format timedelta objects in a cool way (e.g 00:00:20.05) |
| 14 | + omitting microseconds and retaining milliseconds""" |
| 15 | + result = str(td) |
| 16 | + try: |
| 17 | + result, ms = result.split(".") |
| 18 | + except ValueError: |
| 19 | + return result + ".00".replace(":", "-") |
| 20 | + ms = int(ms) |
| 21 | + ms = round(ms / 1e4) |
| 22 | + return f"{result}.{ms:02}".replace(":", "-") |
| 23 | + |
| 24 | + |
| 25 | +def extract_frames(video_file, verbose=1): |
| 26 | + # load the video clip |
| 27 | + video_clip = VideoFileClip(video_file) |
| 28 | + # make a folder by the name of the video file |
| 29 | + filename, _ = os.path.splitext(video_file) |
| 30 | + if not os.path.isdir(filename): |
| 31 | + os.mkdir(filename) |
| 32 | + # if the SAVING_FRAMES_PER_SECOND is above video FPS, then set it to FPS (as maximum) |
| 33 | + saving_frames_per_second = min(video_clip.fps, SAVING_FRAMES_PER_SECOND) |
| 34 | + # if SAVING_FRAMES_PER_SECOND is set to 0, step is 1/fps, else 1/SAVING_FRAMES_PER_SECOND |
| 35 | + step = 1 / video_clip.fps if saving_frames_per_second == 0 else 1 / saving_frames_per_second |
| 36 | + iteration = np.arange(0, video_clip.duration, step) |
| 37 | + if verbose: |
| 38 | + iteration = tqdm(iteration, desc="Extracting video frames") |
| 39 | + # iterate over each possible frame |
| 40 | + for current_duration in iteration: |
| 41 | + # format the file name and save it |
| 42 | + frame_duration_formatted = format_timedelta(timedelta(seconds=current_duration)).replace(":", "-") |
| 43 | + frame_filename = os.path.join(filename, f"frame{frame_duration_formatted}.jpg") |
| 44 | + # save the frame with the current duration |
| 45 | + video_clip.save_frame(frame_filename, current_duration) |
| 46 | + return filename, video_clip.fps |
| 47 | + |
| 48 | + |
| 49 | + |
| 50 | +def reverse_video(frames_path, video_fps, remove_extracted_frames=True): |
| 51 | + frame_files = glob(os.path.join(frames_path, "*")) |
| 52 | + # sort by duration in descending order |
| 53 | + frame_files = sorted(frame_files, key=lambda d: datetime.strptime(d.split("frame")[1], "%H-%M-%S.%f.jpg"), reverse=True) |
| 54 | + # calculate the FPS, getting the minimum between the original FPS and the parameter we set |
| 55 | + saving_frames_per_second = min(video_fps, SAVING_FRAMES_PER_SECOND) |
| 56 | + if saving_frames_per_second == 0: |
| 57 | + # if the parameter is set to 0, automatically set it to the original video fps |
| 58 | + saving_frames_per_second = video_fps |
| 59 | + print("Saving the video with FPS:", saving_frames_per_second) |
| 60 | + # load the frames into a image sequence clip (MoviePy) |
| 61 | + image_sequence_clip = ImageSequenceClip(frame_files, fps=saving_frames_per_second) |
| 62 | + # write the video file to disk |
| 63 | + output_filename = f"{frames_path}-inverted.mp4" |
| 64 | + image_sequence_clip.write_videofile(output_filename) |
| 65 | + if remove_extracted_frames: |
| 66 | + # if set to True, then remove the folder that contain the extracted frames |
| 67 | + shutil.rmtree(frames_path) |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +if __name__ == "__main__": |
| 72 | + import sys |
| 73 | + video_file = sys.argv[1] |
| 74 | + frames_folder_path, video_fps = extract_frames(video_file) |
| 75 | + reverse_video(frames_folder_path, video_fps=video_fps) |
| 76 | + |
0 commit comments