From 6e296ae6df2adba18df961e4aa406846185f53cf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 11 Feb 2018 18:21:31 -0800 Subject: [PATCH 1/4] Refactor of background coloring vmobjects, should be faster this way. --- camera/camera.py | 111 ++++++++++++++++++++++++++-------- mobject/__init__.py | 2 +- mobject/vectorized_mobject.py | 53 ++++------------ 3 files changed, 98 insertions(+), 68 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 958787f9..c1b96aed 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -8,7 +8,9 @@ import aggdraw from helpers import * from mobject import Mobject, PMobject, VMobject, \ - ImageMobject, Group, BackgroundColoredVMobject + ImageMobject, Group + +from profilehooks import profile class Camera(object): CONFIG = { @@ -180,15 +182,13 @@ class Camera(object): mobjects = self.get_mobjects_to_display(mobjects, **kwargs) vmobjects = [] for mobject in mobjects: - if isinstance(mobject, VMobject) and not isinstance(mobject, BackgroundColoredVMobject): - vmobjects.append(mobject) + if isinstance(mobject, VMobject): + vmobjects.append(mobject) elif len(vmobjects) > 0: self.display_multiple_vectorized_mobjects(vmobjects) vmobjects = [] - if isinstance(mobject, BackgroundColoredVMobject): - self.display_background_colored_vmobject(mobject) - elif isinstance(mobject, PMobject): + if isinstance(mobject, PMobject): self.display_point_cloud( mobject.points, mobject.rgbas, self.adjusted_thickness(mobject.stroke_width) @@ -221,7 +221,15 @@ class Camera(object): #More efficient to bundle together in one "canvas" canvas = self.get_aggdraw_canvas() for vmobject in vmobjects: - self.display_vectorized(vmobject, canvas) + if vmobject.get_background_image_file(): + canvas.flush() + self.display_background_colored_vmobject(vmobject) + #TODO: Resetting canvas every time here is inefficient + self.reset_aggdraw_canvas() + canvas = self.get_aggdraw_canvas() + else: + self.display_vectorized(vmobject, canvas) + # last_vmobject_had_background = False canvas.flush() def display_vectorized(self, vmobject, canvas = None): @@ -287,27 +295,21 @@ class Camera(object): result += " ".join(coord_strings) return result + def get_background_colored_vmobject_displayer(self): + #Quite wordy to type out a bunch + long_name = "background_colored_vmobject_displayer" + if not hasattr(self, long_name): + setattr(self, long_name, BackgroundColoredVMobjectDisplayer(self)) + return getattr(self, long_name) + + @profile def display_background_colored_vmobject(self, cvmobject): - mob_array = np.zeros( - self.pixel_array.shape, - dtype = self.pixel_array_dtype - ) - image = Image.fromarray(mob_array, mode = self.image_mode) - canvas = aggdraw.Draw(image) - self.display_vectorized(cvmobject, canvas) - canvas.flush() - cv_background = cvmobject.background_array - if not np.all(self.pixel_array.shape == cv_background): - cvmobject.resize_background_array_to_match(self.pixel_array) - cv_background = cvmobject.background_array - array = np.array( - (np.array(mob_array).astype('float')/255.)*\ - np.array(cv_background), - dtype = self.pixel_array_dtype - ) + displayer = self.get_background_colored_vmobject_displayer() + cvmobject_pixel_array = displayer.display(cvmobject) self.pixel_array[:,:] = np.maximum( - self.pixel_array, array + self.pixel_array, cvmobject_pixel_array ) + return self ## Methods for other rendering @@ -505,6 +507,65 @@ class Camera(object): return centered_space_coords +class BackgroundColoredVMobjectDisplayer(object): + def __init__(self, camera): + self.camera = camera + self.file_name_to_pixel_array_map = {} + self.init_canvas() + + def init_canvas(self): + self.pixel_array = np.zeros( + self.camera.pixel_array.shape, + dtype = self.camera.pixel_array_dtype, + ) + self.reset_canvas() + + def reset_canvas(self): + image = Image.fromarray(self.pixel_array, mode = self.camera.image_mode) + self.canvas = aggdraw.Draw(image) + + def resize_background_array( + self, background_array, + new_width, new_height, + mode = "RGBA" + ): + image = Image.fromarray(background_array, mode = mode) + resized_image = image.resize((new_width, new_height)) + return np.array(resized_image) + + def resize_background_array_to_match(self, background_array, pixel_array): + height, width = pixel_array.shape[:2] + mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" + return self.resize_background_array(background_array, width, height, mode) + + def get_background_array(self, cvmobject): + file_name = cvmobject.get_background_image_file() + if file_name in self.file_name_to_pixel_array_map: + return self.file_name_to_pixel_array_map[file_name] + full_path = get_full_raster_image_path(file_name) + image = Image.open(full_path) + array = np.array(image) + + camera = self.camera + if not np.all(camera.pixel_array.shape == array.shape): + array = self.resize_background_array_to_match(array, camera.pixel_array) + + self.file_name_to_pixel_array_map[file_name] = array + return array + + def display(self, cvmobject): + background_array = self.get_background_array(cvmobject) + self.camera.display_vectorized(cvmobject, self.canvas) + self.canvas.flush() + array = np.array( + (background_array*self.pixel_array.astype('float')/255), + dtype = self.camera.pixel_array_dtype + ) + self.pixel_array[:,:] = 0 + self.reset_canvas() + return array + + class MovingCamera(Camera): """ Stays in line with the height, width and position diff --git a/mobject/__init__.py b/mobject/__init__.py index 4dbeb0f9..e87c8285 100644 --- a/mobject/__init__.py +++ b/mobject/__init__.py @@ -6,5 +6,5 @@ __all__ = [ from mobject import Mobject, Group from point_cloud_mobject import Point, Mobject1D, Mobject2D, PMobject -from vectorized_mobject import VMobject, VGroup, BackgroundColoredVMobject +from vectorized_mobject import VMobject, VGroup from image_mobject import ImageMobject \ No newline at end of file diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 7bb8bdf3..e732cc5b 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -17,6 +17,7 @@ class VMobject(Mobject): "propagate_style_to_family" : False, "pre_function_handle_to_anchor_scale_factor" : 0.01, "make_smooth_after_applying_functions" : False, + "background_image_file" : None, } def get_group_class(self): @@ -151,6 +152,16 @@ class VMobject(Mobject): return self.get_stroke_color() return self.get_fill_color() + def color_using_background_image(self, background_image_file): + self.background_image_file = background_image_file + self.highlight(WHITE) + for submob in self.submobjects: + submob.color_using_background_image(background_image_file) + return self + + def get_background_image_file(self): + return self.background_image_file + ## Drawing def start_at(self, point): if len(self.points) == 0: @@ -470,46 +481,4 @@ class VectorizedPoint(VMobject): def set_location(self,new_loc): self.set_points(np.array([new_loc])) -class BackgroundColoredVMobject(VMobject): - CONFIG = { - # Can be set to None, using set_background_array to initialize instead - "background_image_file" : "color_background", - "stroke_color" : WHITE, - "fill_color" : WHITE, - } - def __init__(self, vmobject, **kwargs): - # Note: At the moment, this does nothing to mimic - # the full family of the vmobject passed in. - VMobject.__init__(self, **kwargs) - - #Match properties of vmobject - self.points = np.array(vmobject.points) - self.set_stroke(WHITE, vmobject.get_stroke_width()) - self.set_fill(WHITE, vmobject.get_fill_opacity()) - for submob in vmobject.submobjects: - self.add(BackgroundColoredVMobject(submob, **kwargs)) - - if self.background_image_file != None: - #Initialize background array - path = get_full_raster_image_path(self.background_image_file) - image = Image.open(path) - self.set_background_array(np.array(image)) - - def set_background_array(self, background_array): - self.background_array = background_array - - def resize_background_array(self, new_width, new_height, mode = "RGBA"): - image = Image.fromarray(self.background_array, mode = mode) - resized_image = image.resize((new_width, new_height)) - self.background_array = np.array(resized_image) - - def resize_background_array_to_match(self, pixel_array): - height, width = pixel_array.shape[:2] - mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" - self.resize_background_array(width, height, mode) - - - - - From ae10d26696c0dbbd2d67d9065e3fa628f1e852b9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 11 Feb 2018 18:59:54 -0800 Subject: [PATCH 2/4] Added batch_by_property --- helpers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/helpers.py b/helpers.py index d0e3c962..80ee9e33 100644 --- a/helpers.py +++ b/helpers.py @@ -226,6 +226,24 @@ def all_elements_are_instances(iterable, Class): def adjacent_pairs(objects): return zip(objects, list(objects[1:])+[objects[0]]) +def batch_by_property(items, property_func): + batches = [] + def add_batch(batch): + if len(batch) > 0: + batches.append(batch) + curr_batch = [] + curr_prop = None + for item in items: + prop = property_func(item) + if prop != curr_prop: + add_batch(curr_batch) + curr_prop = prop + curr_batch = [item] + else: + curr_batch.append(item) + add_batch(curr_batch) + return batches + def complex_to_R3(complex_num): return np.array((complex_num.real, complex_num.imag, 0)) From 2ff4a7ba079632b989d5a1b3366425fdc135cb92 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 11 Feb 2018 19:00:09 -0800 Subject: [PATCH 3/4] Refactored for meaningful speedup in background colored vmobjects --- camera/camera.py | 59 +++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index c1b96aed..8b08be11 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -178,7 +178,7 @@ class Camera(object): return self.capture_mobjects([mobject], **kwargs) def capture_mobjects(self, mobjects, **kwargs): - self.reset_aggdraw_canvas() + # self.reset_aggdraw_canvas() mobjects = self.get_mobjects_to_display(mobjects, **kwargs) vmobjects = [] for mobject in mobjects: @@ -218,18 +218,21 @@ class Camera(object): def display_multiple_vectorized_mobjects(self, vmobjects): if len(vmobjects) == 0: return - #More efficient to bundle together in one "canvas" + batches = batch_by_property( + vmobjects, + lambda vm : vm.get_background_image_file() + ) + for batch in batches: + if batch[0].get_background_image_file(): + self.display_multiple_background_colored_vmobject(batch) + else: + self.display_multiple_non_background_colored_vmobjects(batch) + + def display_multiple_non_background_colored_vmobjects(self, vmobjects): + self.reset_aggdraw_canvas() canvas = self.get_aggdraw_canvas() for vmobject in vmobjects: - if vmobject.get_background_image_file(): - canvas.flush() - self.display_background_colored_vmobject(vmobject) - #TODO: Resetting canvas every time here is inefficient - self.reset_aggdraw_canvas() - canvas = self.get_aggdraw_canvas() - else: - self.display_vectorized(vmobject, canvas) - # last_vmobject_had_background = False + self.display_vectorized(vmobject, canvas) canvas.flush() def display_vectorized(self, vmobject, canvas = None): @@ -303,9 +306,9 @@ class Camera(object): return getattr(self, long_name) @profile - def display_background_colored_vmobject(self, cvmobject): + def display_multiple_background_colored_vmobject(self, cvmobjects): displayer = self.get_background_colored_vmobject_displayer() - cvmobject_pixel_array = displayer.display(cvmobject) + cvmobject_pixel_array = displayer.display(*cvmobjects) self.pixel_array[:,:] = np.maximum( self.pixel_array, cvmobject_pixel_array ) @@ -553,17 +556,27 @@ class BackgroundColoredVMobjectDisplayer(object): self.file_name_to_pixel_array_map[file_name] = array return array - def display(self, cvmobject): - background_array = self.get_background_array(cvmobject) - self.camera.display_vectorized(cvmobject, self.canvas) - self.canvas.flush() - array = np.array( - (background_array*self.pixel_array.astype('float')/255), - dtype = self.camera.pixel_array_dtype + def display(self, *cvmobjects): + batches = batch_by_property( + cvmobjects, lambda cv : cv.get_background_image_file() ) - self.pixel_array[:,:] = 0 - self.reset_canvas() - return array + curr_array = None + for batch in batches: + background_array = self.get_background_array(batch[0]) + for cvmobject in batch: + self.camera.display_vectorized(cvmobject, self.canvas) + self.canvas.flush() + new_array = np.array( + (background_array*self.pixel_array.astype('float')/255), + dtype = self.camera.pixel_array_dtype + ) + if curr_array is None: + curr_array = new_array + else: + curr_array = np.maximum(curr_array, new_array) + self.pixel_array[:,:] = 0 + self.reset_canvas() + return curr_array class MovingCamera(Camera): From 0d05c5834237ecdf0bd6bf0deb0226b17cb24047 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 11 Feb 2018 19:05:24 -0800 Subject: [PATCH 4/4] Cleanup from last changes --- camera/camera.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 8b08be11..7c6d5477 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -10,8 +10,6 @@ from helpers import * from mobject import Mobject, PMobject, VMobject, \ ImageMobject, Group -from profilehooks import profile - class Camera(object): CONFIG = { "background_image" : None, @@ -178,7 +176,6 @@ class Camera(object): return self.capture_mobjects([mobject], **kwargs) def capture_mobjects(self, mobjects, **kwargs): - # self.reset_aggdraw_canvas() mobjects = self.get_mobjects_to_display(mobjects, **kwargs) vmobjects = [] for mobject in mobjects: @@ -305,7 +302,6 @@ class Camera(object): setattr(self, long_name, BackgroundColoredVMobjectDisplayer(self)) return getattr(self, long_name) - @profile def display_multiple_background_colored_vmobject(self, cvmobjects): displayer = self.get_background_colored_vmobject_displayer() cvmobject_pixel_array = displayer.display(*cvmobjects)