From 18b2267c7e8744e74ee3df9c63e5460f027522fc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 9 May 2017 13:14:08 -0700 Subject: [PATCH] Scenes now pass frames directly to ffmpeg pipe when writing movies, rather than saving them all up in bulk then passing in the list --- extract_scene.py | 57 +++++++++++++++++------------- scene/scene.py | 92 ++++++++++++++++++------------------------------ 2 files changed, 68 insertions(+), 81 deletions(-) diff --git a/extract_scene.py b/extract_scene.py index b82b7f65..4e2963e4 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -46,16 +46,17 @@ def get_configuration(sys_argv): print str(err) sys.exit(2) config = { - "file" : None, - "scene_name" : "", - "camera_config" : PRODUCTION_QUALITY_CAMERA_CONFIG, - "frame_duration" : PRODUCTION_QUALITY_FRAME_DURATION, - "preview" : False, - "write" : False, - "save_image" : False, - "quiet" : False, - "write_all" : False, - "output_name" : None, + "file" : None, + "scene_name" : "", + "camera_config" : PRODUCTION_QUALITY_CAMERA_CONFIG, + "frame_duration" : PRODUCTION_QUALITY_FRAME_DURATION, + "preview" : False, + "write_to_movie" : False, + "save_all_frames" : False, + "save_image" : False, + "quiet" : False, + "write_all" : False, + "output_name" : None, } for opt, arg in opts: if opt == '-h': @@ -66,11 +67,12 @@ def get_configuration(sys_argv): config["frame_duration"] = LOW_QUALITY_FRAME_DURATION if opt == '-p': config["preview"] = True + config["save_all_frames"] = True if opt == '-m': config["camera_config"] = MEDIUM_QUALITY_CAMERA_CONFIG config["frame_duration"] = MEDIUM_QUALITY_FRAME_DURATION if opt == '-w': - config["write"] = True + config["write_to_movie"] = True if opt == '-s': config["save_image"] = True if opt in ['-q', '-a']: @@ -80,10 +82,10 @@ def get_configuration(sys_argv): if opt == '-o': config["output_name"] = arg #By default, write to file - actions = ["write", "preview", "save_image"] + actions = ["write_to_movie", "preview", "save_image"] if not any([config[key] for key in actions]): - config["write"] = True - config["skip_animations"] = config["save_image"] and not config["write"] + config["write_to_movie"] = True + config["skip_animations"] = config["save_image"] and not config["write_to_movie"] if len(args) == 0: print HELP_MESSAGE @@ -104,10 +106,7 @@ def handle_scene(scene, **config): if config["save_image"]: if not config["write_all"]: scene.show_frame() - path = os.path.join(MOVIE_DIR, config["movie_prefix"]) - scene.save_image(path, output_name) - if config["write"]: - scene.write_to_movie(os.path.join(config["movie_prefix"], output_name)) + scene.save_image(output_name) if config["quiet"]: sys.stdout.close() @@ -167,12 +166,22 @@ def main(): scene_names_to_classes = dict( inspect.getmembers(module, is_scene) ) - config["movie_prefix"] = config["file"].replace(".py", "") - scene_kwargs = { - "camera_config" : config["camera_config"], - "frame_duration" : config["frame_duration"], - "skip_animations" : config["skip_animations"], - } + config["output_directory"] = os.path.join( + MOVIE_DIR, + config["file"].replace(".py", "") + ) + + scene_kwargs = dict([ + (key, config[key]) + for key in [ + "camera_config", + "frame_duration", + "skip_animations", + "write_to_movie", + "save_all_frames", + "output_directory", + ] + ]) for SceneClass in get_scene_classes(scene_names_to_classes, config): try: handle_scene(SceneClass(**scene_kwargs), **config) diff --git a/scene/scene.py b/scene/scene.py index 917ae5ac..8d8b9cc4 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -21,22 +21,29 @@ from animation.transform import MoveToTarget class Scene(object): CONFIG = { - "camera_class" : Camera, - "camera_config" : {}, - "frame_duration" : LOW_QUALITY_FRAME_DURATION, - "construct_args" : [], - "skip_animations" : False, + "camera_class" : Camera, + "camera_config" : {}, + "frame_duration" : LOW_QUALITY_FRAME_DURATION, + "construct_args" : [], + "skip_animations" : False, + "write_to_movie" : True, + "save_frames" : False, + "output_directory" : MOVIE_DIR, } def __init__(self, **kwargs): digest_config(self, kwargs) self.camera = self.camera_class(**self.camera_config) - self.frames = [] self.mobjects = [] self.foreground_mobjects = [] self.num_plays = 0 + self.saved_frames = [] self.setup() + if self.write_to_movie: + self.open_movie_pipe() self.construct(*self.construct_args) + if self.write_to_movie: + self.close_movie_pipe() def setup(self): """ @@ -313,31 +320,6 @@ class Scene(object): return self.mobjects_from_last_animation return [] - def play_over_time_range(self, t0, t1, *animations): - needed_scene_time = max(abs(t0), abs(t1)) - existing_scene_time = len(self.frames)*self.frame_duration - if existing_scene_time < needed_scene_time: - self.dither(needed_scene_time - existing_scene_time) - existing_scene_time = needed_scene_time - #So negative values may be used - if t0 < 0: - t0 = float(t0)%existing_scene_time - if t1 < 0: - t1 = float(t1)%existing_scene_time - t0, t1 = min(t0, t1), max(t0, t1) - - moving_mobjects, static_mobjects = \ - self.separate_moving_and_static_mobjects(*animations) - for t in np.arange(t0, t1, self.frame_duration): - for animation in animations: - animation.update((t-t0)/(t1 - t0)) - index = int(t/self.frame_duration) - self.update_frame(moving_mobjects, self.frames[index]) - self.frames[index] = self.get_frame() - for animation in animations: - animation.clean_up() - return self - def dither(self, duration = DEFAULT_DITHER_TIME): if self.skip_animations: return self @@ -356,21 +338,22 @@ class Scene(object): return self def add_frames(self, *frames): - self.frames += list(frames) + if self.write_to_movie: + for frame in frames: + self.writing_process.stdin.write(frame.tostring()) + if self.save_frames: + self.saved_frames += list(frames) - def repeat_frames(self, num): - self.frames = self.frames*num - return self - - def reverse_frames(self): - self.frames.reverse() + def repeat_frames(self, num = 1): + assert(self.save_frames) + self.add_frames(*self.saved_frames*num) return self def invert_colors(self): white_frame = 255*np.ones(self.get_frame().shape, dtype = 'uint8') - self.frames = [ + self.saved_frames = [ white_frame-frame - for frame in self.frames + for frame in self.saved_frames ] return self @@ -381,8 +364,8 @@ class Scene(object): def preview(self): TkSceneRoot(self) - def save_image(self, directory = MOVIE_DIR, name = None): - path = os.path.join(directory, "images") + def save_image(self, name = None): + path = os.path.join(self.output_directory, "images") file_name = (name or str(self)) + ".png" full_path = os.path.join(path, file_name) if not os.path.exists(path): @@ -391,20 +374,15 @@ class Scene(object): Image.fromarray(self.get_frame()).save(full_path) def get_movie_file_path(self, name, extension): - file_path = os.path.join(MOVIE_DIR, name) + file_path = os.path.join(self.output_directory, name) if not file_path.endswith(extension): file_path += extension - directory = os.path.split(file_path)[0] - if not os.path.exists(directory): - os.makedirs(directory) + if not os.path.exists(self.output_directory): + os.makedirs(self.output_directory) return file_path - def write_to_movie(self, name = None): - if len(self.frames) == 0: - print "No frames, so I'm not writing anything" - return - if name is None: - name = str(self) + def open_movie_pipe(self): + name = str(self) file_path = self.get_movie_file_path(name, ".mp4") print "Writing to %s"%file_path @@ -427,11 +405,11 @@ class Scene(object): '-loglevel', 'error', file_path, ] - process = sp.Popen(command, stdin=sp.PIPE) - for frame in self.frames: - process.stdin.write(frame.tostring()) - process.stdin.close() - process.wait() + self.writing_process = sp.Popen(command, stdin=sp.PIPE) + + def close_movie_pipe(self): + self.writing_process.stdin.close() + self.writing_process.wait()