diff --git a/camera.py b/camera.py index eabc7d60..7e7a2b93 100644 --- a/camera.py +++ b/camera.py @@ -20,6 +20,8 @@ class Camera(object): #Points in vectorized mobjects with norm greater #than this value will be rescaled. "max_allowable_norm" : 2*SPACE_WIDTH, + "image_mode" : "RGBA", + "n_rgb_coords" : 4, } def __init__(self, background = None, **kwargs): @@ -46,18 +48,20 @@ class Camera(object): def init_background(self): if self.background_image is not None: path = get_full_image_path(self.background_image) - image = Image.open(path).convert('RGB') + image = Image.open(path).convert(self.image_mode) height, width = self.pixel_shape #TODO, how to gracefully handle backgrounds #with different sizes? self.background = np.array(image)[:height, :width] else: - background_rgb = color_to_int_rgb(self.background_color) + background_rgba = color_to_int_rgba( + self.background_color, alpha = 0 + ) self.background = np.zeros( - list(self.pixel_shape)+[3], + list(self.pixel_shape)+[self.n_rgb_coords], dtype = 'uint8' ) - self.background[:,:] = background_rgb + self.background[:,:] = background_rgba def get_image(self): return np.array(self.pixel_array) @@ -88,7 +92,7 @@ class Camera(object): self.display_multiple_vectorized_mobjects(vmobjects) vmobjects = [] self.display_point_cloud( - mobject.points, mobject.rgbs, + mobject.points, mobject.rgbas, self.adjusted_thickness(mobject.stroke_width) ) elif isinstance(mobject, ImageMobject): @@ -102,11 +106,12 @@ class Camera(object): if len(vmobjects) == 0: return #More efficient to bundle together in one "canvas" - image = Image.fromarray(self.pixel_array, mode = "RGB") + image = Image.fromarray(self.pixel_array, mode = self.image_mode) canvas = aggdraw.Draw(image) for vmobject in vmobjects: self.display_vectorized(vmobject, canvas) canvas.flush() + self.pixel_array[:,:] = image def display_vectorized(self, vmobject, canvas): @@ -159,7 +164,7 @@ class Camera(object): result += " ".join([start] + cubics + [end]) return result - def display_point_cloud(self, points, rgbs, thickness): + def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return points = self.align_points_to_camera(points) @@ -167,15 +172,16 @@ class Camera(object): pixel_coords = self.thickened_coordinates( pixel_coords, thickness ) + rgb_len = self.pixel_array.shape[2] - rgbs = (255*rgbs).astype('uint8') + rgbas = (255*rgbas).astype('uint8') target_len = len(pixel_coords) - factor = target_len/len(rgbs) - rgbs = np.array([rgbs]*factor).reshape((target_len, 3)) + factor = target_len/len(rgbas) + rgbas = np.array([rgbas]*factor).reshape((target_len, rgb_len)) on_screen_indices = self.on_screen_pixels(pixel_coords) pixel_coords = pixel_coords[on_screen_indices] - rgbs = rgbs[on_screen_indices] + rgbas = rgbas[on_screen_indices] ph, pw = self.pixel_shape @@ -184,9 +190,9 @@ class Camera(object): indices = np.dot(pixel_coords, flattener)[:,0] indices = indices.astype('int') - new_pa = self.pixel_array.reshape((ph*pw, 3)) - new_pa[indices] = rgbs - self.pixel_array = new_pa.reshape((ph, pw, 3)) + new_pa = self.pixel_array.reshape((ph*pw, rgb_len)) + new_pa[indices] = rgbas + self.pixel_array = new_pa.reshape((ph, pw, rgb_len)) def display_image_mobject(self, image_mobject): corner_coords = self.points_to_pixel_coords(image_mobject.points) @@ -211,7 +217,7 @@ class Camera(object): recentered_coords = all_pixel_coords - ul_coords coord_norms = np.linalg.norm(recentered_coords, axis = 1) - with np.errstate(divide='ignore'): + with np.errstate(divide = 'ignore'): ix_coords, iy_coords = [ np.divide( dim*np.dot(recentered_coords, vect), @@ -226,11 +232,26 @@ class Camera(object): n_to_change = np.sum(to_change) inner_flat_coords = iw*iy_coords[to_change] + ix_coords[to_change] flat_impa = impa.reshape((iw*ih, rgb_len)) - target_rgbs = flat_impa[inner_flat_coords, :] + target_rgbas = flat_impa[inner_flat_coords, :] - flat_pa = self.pixel_array.reshape((ow*oh, rgb_len)) - flat_pa[to_change] = target_rgbs + image = np.zeros((ow*oh, rgb_len), dtype = 'uint8') + image[to_change] = target_rgbas + image = image.reshape((oh, ow, rgb_len)) + self.overlay_rgba_array(image) + def overlay_rgba_array(self, arr): + """ Overlays arr onto self.pixel_array with relevant alphas""" + bg, fg = self.pixel_array/255.0, arr/255.0 + A = 1 - (1 - bg[:,:,3])*(1 - fg[:,:,3]) + alpha_sum = bg[:,:,3] + fg[:,:,3] + for i in range(3): + with np.errstate(divide = 'ignore', invalid='ignore'): + bg[:,:,i] = reduce(op.add, [ + np.divide(arr[:,:,i]*arr[:,:,3], alpha_sum) + for arr in fg, bg + ]) + bg[:,:,3] = A + self.pixel_array = (255*bg).astype('uint8') def align_points_to_camera(self, points): ## This is where projection should live diff --git a/helpers.py b/helpers.py index 3f505704..b3fcf244 100644 --- a/helpers.py +++ b/helpers.py @@ -109,21 +109,32 @@ def diag_to_matrix(l_and_u, diag): def is_closed(points): return np.linalg.norm(points[0] - points[-1]) < CLOSED_THRESHOLD +## Color + def color_to_rgb(color): return np.array(Color(color).get_rgb()) +def color_to_rgba(color, alpha = 1): + return np.append(color_to_rgb(color), [alpha]) + def rgb_to_color(rgb): try: return Color(rgb = rgb) except: return Color(WHITE) +def rgba_to_color(rgba): + return rgb_to_color(rgba[:3]) + def invert_color(color): return rgb_to_color(1.0 - color_to_rgb(color)) def color_to_int_rgb(color): return (255*color_to_rgb(color)).astype('uint8') +def color_to_int_rgba(color, alpha = 255): + return np.append(color_to_int_rgb(color), alpha) + def color_gradient(reference_colors, length_of_output): if length_of_output == 0: return reference_colors[0] @@ -148,6 +159,8 @@ def average_color(*colors): mean_rgb = np.apply_along_axis(np.mean, 0, rgbs) return rgb_to_color(mean_rgb) +### + def compass_directions(n = 4, start_vect = RIGHT): angle = 2*np.pi/n return np.array([ diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index e2957725..5fb59e09 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -17,11 +17,13 @@ class ImageMobject(Mobject): "invert" : False, # "use_cache" : True, "height": 2.0, + "image_mode" : "RGBA" } def __init__(self, filename_or_array, **kwargs): + digest_config(self, kwargs) if isinstance(filename_or_array, str): path = get_full_image_path(filename_or_array) - image = Image.open(path).convert("RGB") + image = Image.open(path).convert(self.image_mode) self.pixel_array = np.array(image) else: self.pixel_array = np.array(filename_or_array) @@ -41,9 +43,13 @@ class ImageMobject(Mobject): h, w = self.pixel_array.shape[:2] self.stretch_to_fit_width(self.height*w/h) - - - + def set_opacity(self, alpha): + self.pixel_array[:,:,3] = int(255*alpha) + return self + + def fade(self, darkness = 0.5): + self.set_opacity(1 - darkness) + return self diff --git a/mobject/point_cloud_mobject.py b/mobject/point_cloud_mobject.py index 4f7dbb73..1bd855ca 100644 --- a/mobject/point_cloud_mobject.py +++ b/mobject/point_cloud_mobject.py @@ -3,49 +3,50 @@ from helpers import * class PMobject(Mobject): def init_points(self): - self.rgbs = np.zeros((0, 3)) + self.rgbas = np.zeros((0, 4)) self.points = np.zeros((0, 3)) return self def get_array_attrs(self): - return Mobject.get_array_attrs(self) + ["rgbs"] + return Mobject.get_array_attrs(self) + ["rgbas"] - def add_points(self, points, rgbs = None, color = None): + def add_points(self, points, rgbas = None, color = None, alpha = 1): """ - points must be a Nx3 numpy array, as must rgbs if it is not None + points must be a Nx3 numpy array, as must rgbas if it is not None """ if not isinstance(points, np.ndarray): points = np.array(points) - num_new_points = points.shape[0] + num_new_points = len(points) self.points = np.append(self.points, points, axis = 0) - if rgbs is None: + if rgbas is None: color = Color(color) if color else self.color - rgbs = np.array([color.get_rgb()] * num_new_points) - elif rgbs.shape != points.shape: - raise Exception("points and rgbs must have same shape") - self.rgbs = np.append(self.rgbs, rgbs, axis = 0) + rgbas = np.repeat( + [color_to_rgba(color, alpha)], + num_new_points, + axis = 0 + ) + elif len(rgbas) != len(points): + raise Exception("points and rgbas must have same shape") + self.rgbas = np.append(self.rgbas, rgbas, axis = 0) return self def highlight(self, color = YELLOW_C, family = True, condition = None): - rgb = Color(color).get_rgb() + rgba = color_to_rgba(color) mobs = self.family_members_with_points() if family else [self] for mob in mobs: if condition: to_change = np.apply_along_axis(condition, 1, mob.points) - mob.rgbs[to_change, :] = rgb + mob.rgbas[to_change, :] = rgba else: - mob.rgbs[:,:] = rgb + mob.rgbas[:,:] = rgba return self def gradient_highlight(self, start_color, end_color): - start_rgb, end_rgb = [ - np.array(Color(color).get_rgb()) - for color in start_color, end_color - ] + start_rgba, end_rgba = map(color_to_rgba, [start_color, end_color]) for mob in self.family_members_with_points(): num_points = mob.get_num_points() - mob.rgbs = np.array([ - interpolate(start_rgb, end_rgb, alpha) + mob.rgbas = np.array([ + interpolate(start_rgba, end_rgba, alpha) for alpha in np.arange(num_points)/float(num_points) ]) return self @@ -53,14 +54,14 @@ class PMobject(Mobject): def match_colors(self, mobject): Mobject.align_data(self, mobject) - self.rgbs = np.array(mobject.rgbs) + self.rgbas = np.array(mobject.rgbas) return self def filter_out(self, condition): for mob in self.family_members_with_points(): to_eliminate = ~np.apply_along_axis(condition, 1, mob.points) mob.points = mob.points[to_eliminate] - mob.rgbs = mob.rgbs[to_eliminate] + mob.rgbas = mob.rgbas[to_eliminate] return self def thin_out(self, factor = 5): @@ -88,13 +89,13 @@ class PMobject(Mobject): return self def fade_to(self, color, alpha): - self.rgbs = interpolate(self.rgbs, np.array(Color(color).rgb), alpha) + self.rgbas = interpolate(self.rgbas, color_to_rgba(color), alpha) for mob in self.submobjects: mob.fade_to(color, alpha) return self - def get_all_rgbs(self): - return self.get_merged_array("rgbs") + def get_all_rgbas(self): + return self.get_merged_array("rgbas") def ingest_submobjects(self): attrs = self.get_array_attrs() @@ -105,7 +106,7 @@ class PMobject(Mobject): return self def get_color(self): - return Color(rgb = self.rgbs[0, :]) + return rgba_to_color(self.rgbas[0, :]) def point_from_proportion(self, alpha): index = alpha*(self.get_num_points()-1) @@ -126,8 +127,8 @@ class PMobject(Mobject): return Point(center) def interpolate_color(self, mobject1, mobject2, alpha): - self.rgbs = interpolate( - mobject1.rgbs, mobject2.rgbs, alpha + self.rgbas = interpolate( + mobject1.rgbas, mobject2.rgbas, alpha ) def pointwise_become_partial(self, mobject, a, b): diff --git a/old_projects/bell.py b/old_projects/bell.py index afb9b416..283dc593 100644 --- a/old_projects/bell.py +++ b/old_projects/bell.py @@ -318,8 +318,8 @@ class MoreFiltersMoreLight(FilterScene): phi, theta = self.camera.get_phi(), self.camera.get_theta() self.set_camera_position(np.pi/2, -np.pi) - self.original_rgbs = [(255, 255, 255)] - self.new_rgbs = [self.arrow_rgb] + self.original_rgbas = [(255, 255, 255)] + self.new_rgbas = [self.arrow_rgb] for bool_array in it.product(*5*[[True, False]]): pfs_to_use = VGroup(*[ pf @@ -330,7 +330,7 @@ class MoreFiltersMoreLight(FilterScene): frame = self.camera.get_image() h, w, three = frame.shape rgb = frame[3*h/8, 7*w/12] - self.original_rgbs.append(rgb) + self.original_rgbas.append(rgb) angles = [pf.filter_angle for pf in pfs_to_use] p = 0.5 @@ -340,7 +340,7 @@ class MoreFiltersMoreLight(FilterScene): if not any(bool_array): new_rgb = self.background_rgb - self.new_rgbs.append(new_rgb) + self.new_rgbas.append(new_rgb) self.camera.reset() self.set_camera_position(phi, theta) @@ -351,9 +351,9 @@ class MoreFiltersMoreLight(FilterScene): frame = FilterScene.get_frame(self) bool_arrays = [ (frame[:,:,0] == r) & (frame[:,:,1] == g) & (frame[:,:,2] == b) - for (r, g, b) in self.original_rgbs + for (r, g, b) in self.original_rgbas ] - for ba, new_rgb in zip(bool_arrays, self.new_rgbs): + for ba, new_rgb in zip(bool_arrays, self.new_rgbas): frame[ba] = new_rgb covered = reduce( lambda b1, b2 : b1 | b2, diff --git a/old_projects/brachistochrone/curves.py b/old_projects/brachistochrone/curves.py index c69d84d0..6eb00db7 100644 --- a/old_projects/brachistochrone/curves.py +++ b/old_projects/brachistochrone/curves.py @@ -92,7 +92,7 @@ class SlideWordDownCycloid(Animation): time = min(time, 1) if time < cut_offs[0]: brightness = time/cut_offs[0] - letter.rgbs = brightness*np.ones(letter.rgbs.shape) + letter.rgbas = brightness*np.ones(letter.rgbas.shape) position = self.path.points[0] angle = 0 elif time < cut_offs[1]: diff --git a/old_projects/brachistochrone/drawing_images.py b/old_projects/brachistochrone/drawing_images.py index 3d66edd9..f30b61f6 100644 --- a/old_projects/brachistochrone/drawing_images.py +++ b/old_projects/brachistochrone/drawing_images.py @@ -63,9 +63,9 @@ def sort_by_color(mob): indices = np.argsort(np.apply_along_axis( lambda p : -np.linalg.norm(p), 1, - mob.rgbs + mob.rgbas )) - mob.rgbs = mob.rgbs[indices] + mob.rgbas = mob.rgbas[indices] mob.points = mob.points[indices] @@ -108,7 +108,7 @@ def nearest_neighbor_align(mobject1, mobject2): ) new_mob2.add_points( mobject2.points[indices], - rgbs = mobject2.rgbs[indices] + rgbas = mobject2.rgbas[indices] ) return new_mob1, new_mob2 diff --git a/old_projects/brachistochrone/multilayered.py b/old_projects/brachistochrone/multilayered.py index ab91696d..f25dbfb7 100644 --- a/old_projects/brachistochrone/multilayered.py +++ b/old_projects/brachistochrone/multilayered.py @@ -334,7 +334,7 @@ class ShowLightAndSlidingObject(MultilayeredScene, TryManyPaths, PhotonScene): if path.get_height() > self.total_glass_height: path.stretch(0.7, 1) path.shift(self.top - path.get_top()) - path.rgbs[:,2] = 0 + path.rgbas[:,2] = 0 loop = paths.pop(1) ##Bad! randy = Randolph() randy.scale(RANDY_SCALE_FACTOR) diff --git a/old_projects/brachistochrone/wordplay.py b/old_projects/brachistochrone/wordplay.py index 45d6f593..821330fa 100644 --- a/old_projects/brachistochrone/wordplay.py +++ b/old_projects/brachistochrone/wordplay.py @@ -330,7 +330,7 @@ class FermatsPrincipleStatement(Scene): norms -= np.min(norms) norms /= np.max(norms) alphas = 0.25 + 0.75 * norms * (1 + np.sin(12*angles))/2 - everything.rgbs = alphas.repeat(3).reshape((len(alphas), 3)) + everything.rgbas = alphas.repeat(3).reshape((len(alphas), 3)) Mobject(everything, words).show() diff --git a/old_projects/hilbert/section2.py b/old_projects/hilbert/section2.py index a4549f60..f67e9471 100644 --- a/old_projects/hilbert/section2.py +++ b/old_projects/hilbert/section2.py @@ -80,10 +80,10 @@ class HilbertCurveIsPerfect(Scene): closest_point_indices = np.apply_along_axis( np.argmin, 1, distance_matrix ) - colored_curve.rgbs = sparce_lion.rgbs[closest_point_indices] + colored_curve.rgbas = sparce_lion.rgbas[closest_point_indices] line = Line(5*LEFT, 5*RIGHT) Mobject.align_data(line, colored_curve) - line.rgbs = colored_curve.rgbs + line.rgbas = colored_curve.rgbas self.add(lion) self.play(ShowCreation(curve, run_time = 3)) @@ -332,7 +332,7 @@ class PseudoHilbertCurvesDontFillSpace(Scene): square.digest_mobject_attrs() square.scale(2**(-5)) square.corner.highlight( - Color(rgb = curve.rgbs[curve.get_num_points()/3]) + Color(rgb = curve.rgbas[curve.get_num_points()/3]) ) square.shift( grid.get_corner(UP+LEFT)-\ diff --git a/old_projects/tau_poem.py b/old_projects/tau_poem.py index 8fa13697..72f22e01 100644 --- a/old_projects/tau_poem.py +++ b/old_projects/tau_poem.py @@ -474,12 +474,12 @@ class TauPoem(Scene): ) blue_rgb = np.array(Color("blue").get_rgb()) white_rgb = np.ones(3) - circle.rgbs = np.array([ + circle.rgbas = np.array([ alpha * blue_rgb + (1 - alpha) * white_rgb - for alpha in np.arange(0, 1, 1.0/len(circle.rgbs)) + for alpha in np.arange(0, 1, 1.0/len(circle.rgbas)) ]) for index in range(circle.points.shape[0]): - circle.rgbs + circle.rgbas def trianglify((x, y, z)): norm = np.linalg.norm((x, y, z)) comp = complex(x, y)*complex(0, 1) diff --git a/scene/scene.py b/scene/scene.py index debc3aa8..ce7d798a 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -477,7 +477,7 @@ class Scene(object): '-f', 'rawvideo', '-vcodec','rawvideo', '-s', '%dx%d'%(width, height), # size of one frame - '-pix_fmt', 'rgb24', + '-pix_fmt', 'rgba', '-r', str(fps), # frames per second '-i', '-', # The imput comes from a pipe '-an', # Tells FFMPEG not to expect any audio