From 404a8b1b3f02c9b8a9eedaffc6376e9d7b02e897 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 9 Feb 2018 12:55:24 -0800 Subject: [PATCH 01/14] Added floobits files to gitignore --- .gitignore | 2 + old_projects/nn/mnist_loader.py | 170 ++++++++++++++++---------------- 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 961fd7cb..fda3b94b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ prettiness_hall_of_fame.py files/ ben_playground.py ben_cairo_test.py +.floo +.flooignore *.xml *.iml diff --git a/old_projects/nn/mnist_loader.py b/old_projects/nn/mnist_loader.py index 611bbf70..7687f413 100644 --- a/old_projects/nn/mnist_loader.py +++ b/old_projects/nn/mnist_loader.py @@ -1,85 +1,85 @@ -""" -mnist_loader -~~~~~~~~~~~~ - -A library to load the MNIST image data. For details of the data -structures that are returned, see the doc strings for ``load_data`` -and ``load_data_wrapper``. In practice, ``load_data_wrapper`` is the -function usually called by our neural network code. -""" - -#### Libraries -# Standard library -import cPickle -import gzip - -# Third-party libraries -import numpy as np - -def load_data(): - """Return the MNIST data as a tuple containing the training data, - the validation data, and the test data. - - The ``training_data`` is returned as a tuple with two entries. - The first entry contains the actual training images. This is a - numpy ndarray with 50,000 entries. Each entry is, in turn, a - numpy ndarray with 784 values, representing the 28 * 28 = 784 - pixels in a single MNIST image. - - The second entry in the ``training_data`` tuple is a numpy ndarray - containing 50,000 entries. Those entries are just the digit - values (0...9) for the corresponding images contained in the first - entry of the tuple. - - The ``validation_data`` and ``test_data`` are similar, except - each contains only 10,000 images. - - This is a nice data format, but for use in neural networks it's - helpful to modify the format of the ``training_data`` a little. - That's done in the wrapper function ``load_data_wrapper()``, see - below. - """ - f = gzip.open('/Users/grant/cs/neural-networks-and-deep-learning/data/mnist.pkl.gz', 'rb') - training_data, validation_data, test_data = cPickle.load(f) - f.close() - return (training_data, validation_data, test_data) - -def load_data_wrapper(): - """Return a tuple containing ``(training_data, validation_data, - test_data)``. Based on ``load_data``, but the format is more - convenient for use in our implementation of neural networks. - - In particular, ``training_data`` is a list containing 50,000 - 2-tuples ``(x, y)``. ``x`` is a 784-dimensional numpy.ndarray - containing the input image. ``y`` is a 10-dimensional - numpy.ndarray representing the unit vector corresponding to the - correct digit for ``x``. - - ``validation_data`` and ``test_data`` are lists containing 10,000 - 2-tuples ``(x, y)``. In each case, ``x`` is a 784-dimensional - numpy.ndarry containing the input image, and ``y`` is the - corresponding classification, i.e., the digit values (integers) - corresponding to ``x``. - - Obviously, this means we're using slightly different formats for - the training data and the validation / test data. These formats - turn out to be the most convenient for use in our neural network - code.""" - tr_d, va_d, te_d = load_data() - training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]] - training_results = [vectorized_result(y) for y in tr_d[1]] - training_data = zip(training_inputs, training_results) - validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]] - validation_data = zip(validation_inputs, va_d[1]) - test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]] - test_data = zip(test_inputs, te_d[1]) - return (training_data, validation_data, test_data) - -def vectorized_result(j): - """Return a 10-dimensional unit vector with a 1.0 in the jth - position and zeroes elsewhere. This is used to convert a digit - (0...9) into a corresponding desired output from the neural - network.""" - e = np.zeros((10, 1)) - e[j] = 1.0 - return e +""" +mnist_loader +~~~~~~~~~~~~ + +A library to load the MNIST image data. For details of the data +structures that are returned, see the doc strings for ``load_data`` +and ``load_data_wrapper``. In practice, ``load_data_wrapper`` is the +function usually called by our neural network code. +""" + +#### Libraries +# Standard library +import cPickle +import gzip + +# Third-party libraries +import numpy as np + +def load_data(): + """Return the MNIST data as a tuple containing the training data, + the validation data, and the test data. + + The ``training_data`` is returned as a tuple with two entries. + The first entry contains the actual training images. This is a + numpy ndarray with 50,000 entries. Each entry is, in turn, a + numpy ndarray with 784 values, representing the 28 * 28 = 784 + pixels in a single MNIST image. + + The second entry in the ``training_data`` tuple is a numpy ndarray + containing 50,000 entries. Those entries are just the digit + values (0...9) for the corresponding images contained in the first + entry of the tuple. + + The ``validation_data`` and ``test_data`` are similar, except + each contains only 10,000 images. + + This is a nice data format, but for use in neural networks it's + helpful to modify the format of the ``training_data`` a little. + That's done in the wrapper function ``load_data_wrapper()``, see + below. + """ + f = gzip.open('/Users/grant/cs/neural-networks-and-deep-learning/data/mnist.pkl.gz', 'rb') + training_data, validation_data, test_data = cPickle.load(f) + f.close() + return (training_data, validation_data, test_data) + +def load_data_wrapper(): + """Return a tuple containing ``(training_data, validation_data, + test_data)``. Based on ``load_data``, but the format is more + convenient for use in our implementation of neural networks. + + In particular, ``training_data`` is a list containing 50,000 + 2-tuples ``(x, y)``. ``x`` is a 784-dimensional numpy.ndarray + containing the input image. ``y`` is a 10-dimensional + numpy.ndarray representing the unit vector corresponding to the + correct digit for ``x``. + + ``validation_data`` and ``test_data`` are lists containing 10,000 + 2-tuples ``(x, y)``. In each case, ``x`` is a 784-dimensional + numpy.ndarry containing the input image, and ``y`` is the + corresponding classification, i.e., the digit values (integers) + corresponding to ``x``. + + Obviously, this means we're using slightly different formats for + the training data and the validation / test data. These formats + turn out to be the most convenient for use in our neural network + code.""" + tr_d, va_d, te_d = load_data() + training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]] + training_results = [vectorized_result(y) for y in tr_d[1]] + training_data = zip(training_inputs, training_results) + validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]] + validation_data = zip(validation_inputs, va_d[1]) + test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]] + test_data = zip(test_inputs, te_d[1]) + return (training_data, validation_data, test_data) + +def vectorized_result(j): + """Return a 10-dimensional unit vector with a 1.0 in the jth + position and zeroes elsewhere. This is used to convert a digit + (0...9) into a corresponding desired output from the neural + network.""" + e = np.zeros((10, 1)) + e[j] = 1.0 + return e From f8653f2946af3370f38af2f3b87bb7abdb3593e3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 9 Feb 2018 15:26:12 -0800 Subject: [PATCH 02/14] Opening scene of Uncertainty project --- active_projects/uncertainty.py | 239 +++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 active_projects/uncertainty.py diff --git a/active_projects/uncertainty.py b/active_projects/uncertainty.py new file mode 100644 index 00000000..83109534 --- /dev/null +++ b/active_projects/uncertainty.py @@ -0,0 +1,239 @@ +from helpers import * +import scipy + +from animation.animation import Animation +from animation.transform import * +from animation.simple_animations import * +from animation.playground import * +from animation.continual_animation import * +from topics.geometry import * +from topics.characters import * +from topics.functions import * +from topics.fractals import * +from topics.number_line import * +from topics.combinatorics import * +from topics.numerals import * +from topics.three_dimensions import * +from topics.objects import * +from topics.probability import * +from topics.complex_numbers import * +from topics.common_scenes import * +from scene import Scene +from scene.reconfigurable_scene import ReconfigurableScene +from scene.zoomed_scene import * +from camera import Camera +from mobject import * +from mobject.image_mobject import * +from mobject.vectorized_mobject import * +from mobject.svg_mobject import * +from mobject.tex_mobject import * +from topics.graph_scene import * + +from active_projects.fourier import * + + +class GaussianDistributionWrapper(Line): + """ + This is meant to encode a 2d normal distribution as + a mobject (so as to be able to have it be interpolated + during animations). It is a line whose start_point coordinates + encode the coordinates of mu, and whose end_point - start_point + encodes the coordinates of sigma. + """ + CONFIG = { + "stroke_width" : 0, + "mu_x" : 0, + "sigma_x" : 1, + "mu_y" : 0, + "sigma_y" : 0, + } + def __init__(self, **kwargs): + Line.__init__(self, ORIGIN, RIGHT, **kwargs) + self.change_parameters(self.mu_x, self.mu_y, self.sigma_x, self.sigma_y) + + def change_parameters(self, mu_x = None, mu_y = None, sigma_x = None, sigma_y = None): + curr_parameters = self.get_parameteters() + args = [mu_x, mu_y, sigma_x, sigma_y] + new_parameters = [ + arg or curr + for curr, arg in zip(curr_parameters, args) + ] + mu_x, mu_y, sigma_x, sigma_y = new_parameters + mu_point = mu_x*RIGHT + mu_y*UP + sigma_vect = sigma_x*RIGHT + sigma_y*UP + self.put_start_and_end_on(mu_point, mu_point + sigma_vect) + return self + + def get_parameteters(self): + """ Return mu_x, mu_y, sigma_x, sigma_y""" + start, end = self.get_start_and_end() + return tuple(it.chain(start[:2], (end - start)[:2])) + + def get_random_points(self, size = 1): + mu_x, mu_y, sigma_x, sigma_y = self.get_parameteters() + x_vals = np.random.normal(mu_x, sigma_x, size) + y_vals = np.random.normal(mu_y, sigma_y, size) + return np.array([ + x*RIGHT + y*UP + for x, y in zip(x_vals, y_vals) + ]) + +class ProbabalisticMobjectCloud(ContinualAnimation): + CONFIG = { + "fill_opacity" : 0.25, + "n_copies" : 100, + "gaussian_distribution_wrapper_config" : { + "sigma_x" : 1, + } + } + def __init__(self, prototype, **kwargs): + digest_config(self, kwargs) + fill_opacity = self.fill_opacity or prototype.get_fill_opacity() + self.gaussian_distribution_wrapper = GaussianDistributionWrapper( + **self.gaussian_distribution_wrapper_config + ) + group = VGroup(*[ + prototype.copy().set_fill(opacity = fill_opacity) + for x in range(self.n_copies) + ]) + ContinualAnimation.__init__(self, group, **kwargs) + + def update_mobject(self, dt): + group = self.mobject + points = self.gaussian_distribution_wrapper.get_random_points(len(group)) + for mob, point in zip(group, points): + self.update_mobject_by_point(mob, point) + return self + + def update_mobject_by_point(self, mobject, point): + mobject.move_to(point) + return self + +class ProbabalisticDotCloud(ProbabalisticMobjectCloud): + CONFIG = { + "color" : BLUE, + } + def __init__(self, **kwargs): + digest_config(self, kwargs) + dot = Dot(color = self.color) + ProbabalisticMobjectCloud.__init__(self, dot) + +class ProbabalisticVectorCloud(ProbabalisticMobjectCloud): + CONFIG = { + "color" : RED, + "n_copies" : 20, + "fill_opacity" : 0.5, + "center_func" : lambda : ORIGIN, + } + def __init__(self, **kwargs): + digest_config(self, kwargs) + vector = Vector( + RIGHT, color = self.color, + max_tip_length_to_length_ratio = 1, + ) + ProbabalisticMobjectCloud.__init__(self, vector) + + def update_mobject_by_point(self, vector, point): + vector.put_start_and_end_on( + self.center_func(), + point + ) + +################### + +class MentionUncertaintyPrinciple(TeacherStudentsScene): + def construct(self): + title = TextMobject("Heisenberg Uncertainty Principle") + title.to_edge(UP) + + dot_cloud = ProbabalisticDotCloud() + vector_cloud = ProbabalisticVectorCloud( + gaussian_distribution_wrapper_config = {"sigma_x" : 0.2}, + center_func = dot_cloud.gaussian_distribution_wrapper.get_start, + ) + for cloud in dot_cloud, vector_cloud: + gdw = cloud.gaussian_distribution_wrapper + gdw.move_to(title.get_center(), LEFT) + gdw.shift(2*DOWN) + vector_cloud.gaussian_distribution_wrapper.shift(3*RIGHT) + + def get_brace_text_group_update(gdw, vect, text): + brace = Brace(gdw, vect) + text = brace.get_tex("\\sigma_{\\text{%s}}"%text, buff = SMALL_BUFF) + group = VGroup(brace, text) + def update_group(group): + brace, text = group + brace.match_width(gdw, stretch = True) + brace.next_to(gdw, vect) + text.next_to(brace, vect, buff = SMALL_BUFF) + return ContinualUpdateFromFunc(group, update_group) + + dot_brace_anim = get_brace_text_group_update( + dot_cloud.gaussian_distribution_wrapper, + DOWN, "position", + ) + vector_brace_anim = get_brace_text_group_update( + vector_cloud.gaussian_distribution_wrapper, + UP, "momentum", + ) + + self.add(title) + self.add(dot_cloud) + self.play( + Write(title), + self.teacher.change, "raise_right_hand", + self.get_student_changes(*["pondering"]*3) + ) + self.play( + Write(dot_brace_anim.mobject, run_time = 1) + ) + self.add(dot_brace_anim) + self.wait() + # self.wait(2) + self.play( + dot_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 0.1}, + run_time = 2, + ) + self.wait() + self.add(vector_cloud) + self.play( + FadeIn(vector_brace_anim.mobject) + ) + self.add(vector_brace_anim) + self.play( + vector_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 1}, + self.get_student_changes(*3*["confused"]), + run_time = 3, + ) + self.wait() + #Back and forth + self.play( + dot_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 2}, + vector_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 0.1}, + run_time = 3, + ) + self.change_student_modes("thinking", "erm", "sassy") + self.play( + dot_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 0.1}, + vector_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 1}, + run_time = 3, + ) + self.wait(2) + + + + + + + + + + + + From 90e4951ef5bba0d1a2698bd9c4165adae6293840 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 18:32:17 -0800 Subject: [PATCH 03/14] Implemented FourierTradeoff in uncertainty.py --- active_projects/fourier.py | 40 +++++++- active_projects/uncertainty.py | 182 ++++++++++++++++++++++++++++++--- 2 files changed, 202 insertions(+), 20 deletions(-) diff --git a/active_projects/fourier.py b/active_projects/fourier.py index 429b5041..83857a08 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -30,18 +30,48 @@ from mobject.svg_mobject import * from mobject.tex_mobject import * from topics.graph_scene import * +USE_ALMOST_FOURIER_BY_DEFAULT = True +NUM_SAMPLES_FOR_FFT = 1000 + + +def get_fourier_graph( + axes, time_func, t_min, t_max, + n_samples = NUM_SAMPLES_FOR_FFT, + complex_to_real_func = lambda z : z.real, + color = RED, + ): + # N = n_samples + # T = time_range/n_samples + time_range = float(t_max - t_min) + time_step_size = time_range/n_samples + time_samples = time_func(np.linspace(t_min, t_max, n_samples)) + fft_output = np.fft.fft(time_samples) + frequencies = np.linspace(0.0, n_samples/(2.0*time_range), n_samples//2) + # #Cycles per second of fouier_samples[1] + # (1/time_range)*n_samples + # freq_step_size = 1./time_range + graph = VMobject() + graph.set_points_smoothly([ + axes.coords_to_point( + x, 200.0*complex_to_real_func(y)/n_samples, + ) + for x, y in zip(frequencies, fft_output[:n_samples//2]) + ]) + graph.highlight(color) + return graph def get_fourier_transform( func, t_min, t_max, - real_part = True, - use_almost_fourier = True, + complex_to_real_func = lambda z : z.real, + use_almost_fourier = USE_ALMOST_FOURIER_BY_DEFAULT, ): - # part = "real" if real_part else "imag" - trig = np.cos if real_part else np.sin scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0 def fourier_transform(f): return scalar*scipy.integrate.quad( - lambda t : func(t)*trig(-TAU*f*t), + lambda t : complex_to_real_func( + # f(t) e^{-TAU*i*f*t} + func(t)*np.exp(complex(0, -TAU*f*t)) + ), t_min, t_max )[0] return fourier_transform diff --git a/active_projects/uncertainty.py b/active_projects/uncertainty.py index 83109534..31da08af 100644 --- a/active_projects/uncertainty.py +++ b/active_projects/uncertainty.py @@ -32,6 +32,9 @@ from topics.graph_scene import * from active_projects.fourier import * +FREQUENCY_COLOR = RED +USE_ALMOST_FOURIER_BY_DEFAULT = False + class GaussianDistributionWrapper(Line): """ This is meant to encode a 2d normal distribution as @@ -207,24 +210,173 @@ class MentionUncertaintyPrinciple(TeacherStudentsScene): self.get_student_changes(*3*["confused"]), run_time = 3, ) - self.wait() #Back and forth - self.play( - dot_cloud.gaussian_distribution_wrapper.change_parameters, - {"sigma_x" : 2}, - vector_cloud.gaussian_distribution_wrapper.change_parameters, - {"sigma_x" : 0.1}, - run_time = 3, + for x in range(2): + self.play( + dot_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 2}, + vector_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 0.1}, + run_time = 3, + ) + self.change_student_modes("thinking", "erm", "sassy") + self.play( + dot_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 0.1}, + vector_cloud.gaussian_distribution_wrapper.change_parameters, + {"sigma_x" : 1}, + run_time = 3, + ) + self.wait() + +class FourierTradeoff(Scene): + def construct(self): + #Setup axes + time_mean = 4 + time_axes = Axes( + x_min = 0, + x_max = 2*time_mean, + x_axis_config = {"unit_size" : 1.5}, + y_min = -2, + y_max = 2, + y_axis_config = {"unit_size" : 0.5} ) - self.change_student_modes("thinking", "erm", "sassy") - self.play( - dot_cloud.gaussian_distribution_wrapper.change_parameters, - {"sigma_x" : 0.1}, - vector_cloud.gaussian_distribution_wrapper.change_parameters, - {"sigma_x" : 1}, - run_time = 3, + time_label = TextMobject("Time") + time_label.next_to( + time_axes.x_axis.get_right(), UP, + buff = MED_SMALL_BUFF, ) - self.wait(2) + time_axes.add(time_label) + time_axes.center().to_edge(UP) + time_axes.x_axis.add_numbers(*range(1, 2*time_mean)) + + frequency_axes = Axes( + x_min = 0, + x_max = 8, + x_axis_config = {"unit_size" : 1.5}, + y_min = 0, + y_max = 15, + y_axis_config = { + "unit_size" : 0.15, + "tick_frequency" : 5, + }, + color = TEAL, + ) + frequency_label = TextMobject("Frequency") + frequency_label.next_to( + frequency_axes.x_axis.get_right(), UP, + buff = MED_SMALL_BUFF, + ) + frequency_label.highlight(FREQUENCY_COLOR) + frequency_axes.add(frequency_label) + frequency_axes.move_to(time_axes, LEFT) + frequency_axes.to_edge(DOWN, buff = LARGE_BUFF) + frequency_axes.x_axis.add_numbers() + + # Graph information + + #x-coordinate of this point determines width of wave_packet graph + width_tracker = VectorizedPoint(0.5*RIGHT) + def get_width(): + return width_tracker.get_center()[0] + + def get_wave_packet_function(): + factor = 1./get_width() + return lambda t : np.sqrt(factor)*np.cos(4*TAU*t)*np.exp(-factor*(t-time_mean)**2) + + def get_wave_packet(): + graph = time_axes.get_graph( + get_wave_packet_function(), + num_graph_points = 200, + ) + graph.highlight(YELLOW) + return graph + + time_radius = 10 + def get_wave_packet_fourier_transform(): + return get_fourier_graph( + frequency_axes, get_wave_packet_function(), + t_min = time_mean - time_radius, + t_max = time_mean + time_radius, + n_samples = 2*time_radius*17, + complex_to_real_func = abs, + color = FREQUENCY_COLOR, + ) + + wave_packet = get_wave_packet() + wave_packet_update = UpdateFromFunc( + wave_packet, + lambda g : Transform(g, get_wave_packet()).update(1) + ) + fourier_graph = get_wave_packet_fourier_transform() + fourier_graph_update = UpdateFromFunc( + fourier_graph, + lambda g : Transform(g, get_wave_packet_fourier_transform()).update(1) + ) + + arrow = Arrow( + wave_packet, frequency_axes.coords_to_point(4, 10), + color = FREQUENCY_COLOR, + ) + fourier_words = TextMobject("Fourier Transform") + fourier_words.next_to(arrow, RIGHT, buff = MED_LARGE_BUFF) + sub_words = TextMobject("(To be explained shortly)") + sub_words.highlight(BLUE) + sub_words.scale(0.75) + sub_words.next_to(fourier_words, DOWN) + + #Draw items + self.add(time_axes, frequency_axes) + self.play(ShowCreation(wave_packet)) + self.play( + ReplacementTransform( + wave_packet.copy(), + fourier_graph, + ), + GrowArrow(arrow), + Write(fourier_words, run_time = 1) + ) + # self.play(FadeOut(arrow)) + self.wait() + for width in 6, 0.1, 1: + self.play( + width_tracker.move_to, width*RIGHT, + wave_packet_update, + fourier_graph_update, + run_time = 3 + ) + if sub_words not in self.mobjects: + self.play(FadeIn(sub_words)) + else: + self.wait() + self.wait() + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5af31c1b8bedbb3bb62b140990efef52c78cd08d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 18:37:34 -0800 Subject: [PATCH 04/14] Minor refactoring to the camera class, with an additional change to z-buffering default behavior --- camera/camera.py | 54 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 75a71089..6c3c5a99 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -10,6 +10,10 @@ from helpers import * from mobject import Mobject, PMobject, VMobject, \ ImageMobject, Group, BackgroundColoredVMobject +# Set a @profile decorator over any method whose +# performance you'd like to analyze +from profilehooks import profile + class Camera(object): CONFIG = { "background_image" : None, @@ -31,7 +35,11 @@ class Camera(object): "image_mode" : "RGBA", "n_rgb_coords" : 4, "background_alpha" : 0, #Out of color_max_val - "pixel_array_dtype" : 'uint8' + "pixel_array_dtype" : 'uint8', + "use_z_coordinate_for_display_order" : False, + # z_buff_func is only used if the flag above is set to True. + # round z coordinate to nearest hundredth when comparring + "z_buff_func" : lambda m : np.round(m.get_center()[2], 2), } def __init__(self, background = None, **kwargs): @@ -94,7 +102,12 @@ class Camera(object): return retval def set_pixel_array(self, pixel_array, convert_from_floats = False): - self.pixel_array = self.convert_pixel_array(pixel_array, convert_from_floats) + converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) + if not hasattr(self, "pixel_array"): #TODO: And the shapes match? + self.pixel_array = converted_array + else: + #Set in place + self.pixel_array[:,:,:] = converted_array[:,:,:] def set_background(self, pixel_array, convert_from_floats = False): self.background = self.convert_pixel_array(pixel_array, convert_from_floats) @@ -141,8 +154,6 @@ class Camera(object): self, mobjects, include_submobjects = True, excluded_mobjects = None, - #Round z coordinate to nearest hundredth when comparring - z_buff_func = lambda m : np.round(m.get_center()[2], 2) ): if include_submobjects: mobjects = self.extract_mobject_family_members( @@ -154,15 +165,22 @@ class Camera(object): ) mobjects = list_difference_update(mobjects, all_excluded) - # Should perhaps think about what happens here when include_submobjects is False, - # (for now, the onus is then on the caller to ensure this is handled correctly by - # passing us an appropriately pre-flattened list of mobjects if need be) - return sorted(mobjects, lambda a, b: cmp(z_buff_func(a), z_buff_func(b))) + if self.use_z_coordinate_for_display_order: + # Should perhaps think about what happens here when include_submobjects is False, + # (for now, the onus is then on the caller to ensure this is handled correctly by + # passing us an appropriately pre-flattened list of mobjects if need be) + return sorted( + mobjects, + lambda a, b: cmp(self.z_buff_func(a), self.z_buff_func(b)) + ) + else: + return mobjects def capture_mobject(self, mobject, **kwargs): 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: @@ -190,23 +208,32 @@ class Camera(object): #TODO, more? Call out if it's unknown? self.display_multiple_vectorized_mobjects(vmobjects) + ## Methods associated with svg rendering + + def get_aggdraw_canvas(self): + if not hasattr(self, "canvas"): + self.reset_aggdraw_canvas() + return self.canvas + + def reset_aggdraw_canvas(self): + image = Image.fromarray(self.pixel_array, mode = self.image_mode) + self.canvas = aggdraw.Draw(image) + def display_multiple_vectorized_mobjects(self, vmobjects): if len(vmobjects) == 0: return #More efficient to bundle together in one "canvas" - image = Image.fromarray(self.pixel_array, mode = self.image_mode) - canvas = aggdraw.Draw(image) + canvas = self.get_aggdraw_canvas() for vmobject in vmobjects: self.display_vectorized(vmobject, canvas) canvas.flush() - self.pixel_array[:,:] = image - - def display_vectorized(self, vmobject, canvas): + def display_vectorized(self, vmobject, canvas = None): if vmobject.is_subpath: #Subpath vectorized mobjects are taken care #of by their parent return + canvas = canvas or self.get_aggdraw_canvas() pen, fill = self.get_pen_and_fill(vmobject) pathstring = self.get_pathstring(vmobject) symbol = aggdraw.Symbol(pathstring) @@ -280,7 +307,6 @@ class Camera(object): self.pixel_array, array ) - def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return From d35ba7b196fa5210e5e091d7b86ef9dd84740a3e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 21:19:26 -0800 Subject: [PATCH 05/14] Sped up get_pathstring of camera --- camera/camera.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 6c3c5a99..111eae03 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -179,6 +179,7 @@ class Camera(object): def capture_mobject(self, mobject, **kwargs): return self.capture_mobjects([mobject], **kwargs) + @profile def capture_mobjects(self, mobjects, **kwargs): self.reset_aggdraw_canvas() mobjects = self.get_mobjects_to_display(mobjects, **kwargs) @@ -269,20 +270,18 @@ class Camera(object): # points = self.adjust_out_of_range_points(points) if len(points) == 0: continue - points = self.align_points_to_camera(points) - coords = self.points_to_pixel_coords(points) - start = "M%d %d"%tuple(coords[0]) - #(handle1, handle2, anchor) tripletes - triplets = zip(*[ - coords[i+1::3] - for i in range(3) - ]) - cubics = [ - "C" + " ".join(map(str, it.chain(*triplet))) - for triplet in triplets - ] - end = "Z" if vmobject.mark_paths_closed else "" - result += " ".join([start] + cubics + [end]) + aligned_points = self.align_points_to_camera(points) + coords = self.points_to_pixel_coords(aligned_points) + coord_strings = coords.flatten().astype("string") + #Start new path string with M + coord_strings[0] = "M" + coord_strings[0] + #The C at the start of every 6th number communicates + #that the following 6 define a cubic Bezier + coord_strings[2::6] = map(lambda s : "C" + str(s), coord_strings[2::6]) + #Possibly finish with "Z" + if vmobject.mark_paths_closed: + coord_strings[-1] = coord_strings[-1] + " Z" + result += " ".join(coord_strings) return result def display_background_colored_vmobject(self, cvmobject): @@ -307,6 +306,8 @@ class Camera(object): self.pixel_array, array ) + ## Methods for other rendering + def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return From 983a3e0357d82818795baf44a6c1fb4e2571c65e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 21:38:12 -0800 Subject: [PATCH 06/14] Direct computation of color hex to speed things up --- camera/camera.py | 34 +++++++++++++++++++++------------- helpers.py | 2 +- mobject/vectorized_mobject.py | 6 ++++++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 111eae03..6c7c369f 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -241,14 +241,22 @@ class Camera(object): canvas.symbol((0, 0), symbol, pen, fill) def get_pen_and_fill(self, vmobject): - pen = aggdraw.Pen( - self.color_to_hex_l(self.get_stroke_color(vmobject)), - max(vmobject.stroke_width, 0) - ) - fill = aggdraw.Brush( - self.color_to_hex_l(self.get_fill_color(vmobject)), - opacity = int(self.color_max_val*vmobject.get_fill_opacity()) - ) + stroke_width = max(vmobject.get_stroke_width(), 0) + if stroke_width == 0: + pen = None + else: + stroke_rgb = self.get_stroke_rgb(vmobject) + stroke_hex = rgb_to_hex(stroke_rgb) + pen = aggdraw.Pen(stroke_hex, stroke_width) + + fill_opacity = int(self.color_max_val*vmobject.get_fill_opacity()) + if fill_opacity == 0: + fill = None + else: + fill_rgb = self.get_fill_rgb(vmobject) + fill_hex = rgb_to_hex(fill_rgb) + fill = aggdraw.Brush(fill_hex, fill_opacity) + return (pen, fill) def color_to_hex_l(self, color): @@ -257,14 +265,14 @@ class Camera(object): except: return Color(BLACK).get_hex_l() - def get_stroke_color(self, vmobject): - return vmobject.get_stroke_color() + def get_stroke_rgb(self, vmobject): + return vmobject.get_stroke_rgb() - def get_fill_color(self, vmobject): - return vmobject.get_fill_color() + def get_fill_rgb(self, vmobject): + return vmobject.get_fill_rgb() def get_pathstring(self, vmobject): - result = "" + result = "" for mob in [vmobject]+vmobject.get_subpath_mobjects(): points = mob.points # points = self.adjust_out_of_range_points(points) diff --git a/helpers.py b/helpers.py index 8e1af238..d0e3c962 100644 --- a/helpers.py +++ b/helpers.py @@ -126,7 +126,7 @@ def rgba_to_color(rgba): return rgb_to_color(rgba[:3]) def rgb_to_hex(rgb): - return Color(rgb = rgb).get_hex_l() + return "#" + "".join('%02x'%int(255*x) for x in rgb) def invert_color(color): return rgb_to_color(1.0 - color_to_rgb(color)) diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index fbf055b0..7bb8bdf3 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -120,6 +120,9 @@ class VMobject(Mobject): ) return self + def get_fill_rgb(self): + return self.fill_rgb + def get_fill_color(self): try: self.fill_rgb = np.clip(self.fill_rgb, 0.0, 1.0) @@ -130,6 +133,9 @@ class VMobject(Mobject): def get_fill_opacity(self): return np.clip(self.fill_opacity, 0, 1) + def get_stroke_rgb(self): + return self.stroke_rgb + def get_stroke_color(self): try: self.stroke_rgb = np.clip(self.stroke_rgb, 0, 1) From c9ff83920cfb4d2604b92ba23d2c71b4f07dc5e0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 21:43:55 -0800 Subject: [PATCH 07/14] Updated ThreeDCamera methods for new color scheme --- topics/three_dimensions.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/topics/three_dimensions.py b/topics/three_dimensions.py index a7314d2e..55de2925 100644 --- a/topics/three_dimensions.py +++ b/topics/three_dimensions.py @@ -40,22 +40,17 @@ class ThreeDCamera(CameraWithPerspective): self.rotation_mobject = VectorizedPoint() self.set_position(self.phi, self.theta, self.distance) - def get_color(self, method): - color = method() - vmobject = method.im_self + def modified_rgb(self, vmobject, rgb): if should_shade_in_3d(vmobject): - return Color(rgb = self.get_shaded_rgb( - color_to_rgb(color), - normal_vect = self.get_unit_normal_vect(vmobject) - )) + return self.get_shaded_rgb(rgb, self.get_unit_normal_vect(vmobject)) else: return color - def get_stroke_color(self, vmobject): - return self.get_color(vmobject.get_stroke_color) + def get_stroke_rgb(self, vmobject): + return self.modified_rgb(vmobject, vmobject.get_stroke_rgb()) - def get_fill_color(self, vmobject): - return self.get_color(vmobject.get_fill_color) + def get_fill_rgb(self, vmobject): + return self.modified_rgb(vmobject, vmobject.get_fill_rgb()) def get_shaded_rgb(self, rgb, normal_vect): brightness = np.dot(normal_vect, self.unit_sun_vect)**2 From 5e25ecd33ca8651a9a142e9e04b44d70a947cd30 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 22:19:00 -0800 Subject: [PATCH 08/14] Tiny changes --- animation/transform.py | 2 +- camera/camera.py | 9 ++------- topics/number_line.py | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/animation/transform.py b/animation/transform.py index 30d7c7d2..4ee132a5 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -48,7 +48,7 @@ class Transform(Animation): self.path_arc, self.path_arc_axis, ) - + def get_all_mobjects(self): return self.mobject, self.starting_mobject, self.target_mobject diff --git a/camera/camera.py b/camera/camera.py index 6c7c369f..958787f9 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -10,10 +10,6 @@ from helpers import * from mobject import Mobject, PMobject, VMobject, \ ImageMobject, Group, BackgroundColoredVMobject -# Set a @profile decorator over any method whose -# performance you'd like to analyze -from profilehooks import profile - class Camera(object): CONFIG = { "background_image" : None, @@ -179,7 +175,6 @@ class Camera(object): def capture_mobject(self, mobject, **kwargs): return self.capture_mobjects([mobject], **kwargs) - @profile def capture_mobjects(self, mobjects, **kwargs): self.reset_aggdraw_canvas() mobjects = self.get_mobjects_to_display(mobjects, **kwargs) @@ -256,7 +251,7 @@ class Camera(object): fill_rgb = self.get_fill_rgb(vmobject) fill_hex = rgb_to_hex(fill_rgb) fill = aggdraw.Brush(fill_hex, fill_opacity) - + return (pen, fill) def color_to_hex_l(self, color): @@ -280,7 +275,7 @@ class Camera(object): continue aligned_points = self.align_points_to_camera(points) coords = self.points_to_pixel_coords(aligned_points) - coord_strings = coords.flatten().astype("string") + coord_strings = coords.flatten().astype(str) #Start new path string with M coord_strings[0] = "M" + coord_strings[0] #The C at the start of every 6th number communicates diff --git a/topics/number_line.py b/topics/number_line.py index 4dab9f07..30ad693a 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -136,8 +136,6 @@ class NumberLine(VMobject): self.tip = tip self.add(tip) - - class UnitInterval(NumberLine): CONFIG = { "x_min" : 0, From b9ef9f6fc0128a1a4b38c456b2a5d5a712c73a72 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 22:34:39 -0800 Subject: [PATCH 09/14] Enabled adding end animation number with -n flag --- extract_scene.py | 9 ++++++++- scene/scene.py | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/extract_scene.py b/extract_scene.py index 825f0ea1..ea34fa06 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -89,6 +89,7 @@ def get_configuration(): "write_all" : args.write_all, "output_name" : args.output_name, "skip_to_animation_number" : args.skip_to_animation_number, + "end_after_animation_number" : None, } if args.low_quality: config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG @@ -102,7 +103,12 @@ def get_configuration(): stan = config["skip_to_animation_number"] if stan is not None: - config["skip_to_animation_number"] = int(stan) + if "," in stan: + start, end = stan.split(",") + config["skip_to_animation_number"] = int(start) + config["end_after_animation_number"] = int(end) + else: + config["skip_to_animation_number"] = int(stan) config["skip_animations"] = any([ config["show_last_frame"] and not config["write_to_movie"], @@ -221,6 +227,7 @@ def main(): "output_directory", "save_pngs", "skip_to_animation_number", + "end_after_animation_number", ] ]) diff --git a/scene/scene.py b/scene/scene.py index 4c72897c..625145f4 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -40,6 +40,7 @@ class Scene(Container): "always_continually_update" : False, "random_seed" : 0, "skip_to_animation_number" : None, + "end_after_animation_number" : None, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) # Perhaps allow passing in a non-empty *mobjects parameter? @@ -409,6 +410,10 @@ class Scene(Container): if self.skip_to_animation_number: if self.num_plays + 1 == self.skip_to_animation_number: self.skip_animations = False + if self.end_after_animation_number: + if self.num_plays >= self.end_after_animation_number: + self.skip_animations = True + return self #Don't even both with the rest... if self.skip_animations: kwargs["run_time"] = 0 From 9cfab5c2ed3ca5370d221f5fffe561f54d6fc386 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 10 Feb 2018 22:45:46 -0800 Subject: [PATCH 10/14] Better convention for -n flag --- extract_scene.py | 20 ++++++++++---------- scene/scene.py | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extract_scene.py b/extract_scene.py index ea34fa06..605b4513 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -68,7 +68,7 @@ def get_configuration(): for short_arg, long_arg in optional_args: parser.add_argument(short_arg, long_arg, action = "store_true") parser.add_argument("-o", "--output_name") - parser.add_argument("-n", "--skip_to_animation_number") + parser.add_argument("-n", "--start_at_animation_number") args = parser.parse_args() except argparse.ArgumentError as err: print(str(err)) @@ -88,8 +88,8 @@ def get_configuration(): "ignore_waits" : args.preview, "write_all" : args.write_all, "output_name" : args.output_name, - "skip_to_animation_number" : args.skip_to_animation_number, - "end_after_animation_number" : None, + "start_at_animation_number" : args.start_at_animation_number, + "end_at_animation_number" : None, } if args.low_quality: config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG @@ -101,18 +101,18 @@ def get_configuration(): config["camera_config"] = PRODUCTION_QUALITY_CAMERA_CONFIG config["frame_duration"] = PRODUCTION_QUALITY_FRAME_DURATION - stan = config["skip_to_animation_number"] + stan = config["start_at_animation_number"] if stan is not None: if "," in stan: start, end = stan.split(",") - config["skip_to_animation_number"] = int(start) - config["end_after_animation_number"] = int(end) + config["start_at_animation_number"] = int(start) + config["end_at_animation_number"] = int(end) else: - config["skip_to_animation_number"] = int(stan) + config["start_at_animation_number"] = int(stan) config["skip_animations"] = any([ config["show_last_frame"] and not config["write_to_movie"], - config["skip_to_animation_number"], + config["start_at_animation_number"], ]) return config @@ -226,8 +226,8 @@ def main(): "write_to_movie", "output_directory", "save_pngs", - "skip_to_animation_number", - "end_after_animation_number", + "start_at_animation_number", + "end_at_animation_number", ] ]) diff --git a/scene/scene.py b/scene/scene.py index 625145f4..1cb81bff 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -39,8 +39,8 @@ class Scene(Container): "name" : None, "always_continually_update" : False, "random_seed" : 0, - "skip_to_animation_number" : None, - "end_after_animation_number" : None, + "start_at_animation_number" : None, + "end_at_animation_number" : None, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) # Perhaps allow passing in a non-empty *mobjects parameter? @@ -407,18 +407,17 @@ class Scene(Container): if len(args) == 0: warnings.warn("Called Scene.play with no animations") return - if self.skip_to_animation_number: - if self.num_plays + 1 == self.skip_to_animation_number: + if self.start_at_animation_number: + if self.num_plays == self.start_at_animation_number: self.skip_animations = False - if self.end_after_animation_number: - if self.num_plays >= self.end_after_animation_number: + if self.end_at_animation_number: + if self.num_plays >= self.end_at_animation_number: self.skip_animations = True return self #Don't even both with the rest... if self.skip_animations: kwargs["run_time"] = 0 animations = self.compile_play_args_to_animation_list(*args) - self.num_plays += 1 sync_animation_run_times_and_rate_funcs(*animations, **kwargs) moving_mobjects = self.get_moving_mobjects(*animations) @@ -434,6 +433,7 @@ class Scene(Container): self.mobjects_from_last_animation = moving_mobjects self.clean_up_animations(*animations) self.continual_update(0) + self.num_plays += 1 return self def clean_up_animations(self, *animations): From 6e296ae6df2adba18df961e4aa406846185f53cf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 11 Feb 2018 18:21:31 -0800 Subject: [PATCH 11/14] 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 12/14] 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 13/14] 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 14/14] 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)