Initial ZoomedScene

This commit is contained in:
Grant Sanderson 2016-02-27 18:50:33 -08:00
parent a06848a219
commit b6985b4321
9 changed files with 109 additions and 222 deletions

View file

@ -162,7 +162,7 @@ class DelayByOrder(Animation):
self.num_mobject_points = animation.mobject.get_num_points()
kwargs.update(dict([
(attr, getattr(animation, attr))
for attr in Animation.DEFAULT_CONFIG
for attr in Animation.CONFIG
]))
Animation.__init__(self, animation.mobject, **kwargs)
self.name = str(self) + str(self.animation)

View file

@ -65,12 +65,10 @@ class Camera(object):
def capture_mobjects(self, mobjects, include_sub_mobjects = True):
if include_sub_mobjects:
all_families = [
mobjects = it.chain(*[
mob.submobject_family()
for mob in mobjects
]
mobjects = reduce(op.add, all_families, [])
])
for mobject in mobjects:
if mobject.display_mode == "region":
self.display_region(mobject)
@ -153,9 +151,10 @@ class Camera(object):
])
def adjusted_thickness(self, thickness):
big_shape = PRODUCTION_QUALITY_DISPLAY_CONFIG["pixel_shape"]
factor = sum(big_shape)/sum(self.pixel_shape)
return 1 + (thickness-1)/factor
# big_shape = PRODUCTION_QUALITY_DISPLAY_CONFIG["pixel_shape"]
# factor = sum(big_shape)/sum(self.pixel_shape)
# return 1 + (thickness-1)/factor
return thickness
def get_thickening_nudges(self, thickness):
_range = range(-thickness/2+1, thickness/2+1)

View file

@ -15,14 +15,14 @@ MEDIUM_QUALITY_DISPLAY_CONFIG = {
}
LOW_QUALITY_DISPLAY_CONFIG = {
"pixel_shape" : (480, 640),
"pixel_shape" : (576, 1024),
}
DEFAULT_POINT_DENSITY_2D = 25
DEFAULT_POINT_DENSITY_1D = 200
DEFAULT_POINT_THICKNESS = 6
DEFAULT_POINT_THICKNESS = 3
#TODO, Make sure these are not needed
SPACE_HEIGHT = 4.0

View file

@ -1,185 +0,0 @@
import numpy as np
import itertools as it
import os
import sys
from PIL import Image
import cv2
from colour import Color
import progressbar
from helpers import *
class Camera(object):
CONFIG = {
"pixel_width" : DEFAULT_WIDTH,
"pixel_height" : DEFAULT_HEIGHT,
"space_height" : SPACE_HEIGHT,
"space_center" : ORIGIN,
"background_color" : BLACK,
}
def __init__(self, background = None, **kwargs):
digest_config(self, kwargs, locals())
self.init_background()
self.reset()
width_to_height = float(self.pixel_width) / self.pixel_height
self.space_width = self.space_height * width_to_height
def init_background(self):
if self.background:
shape = self.background.shape[:2]
self.pixel_height, self.pixel_width = shape
else:
background_color = Color(self.background_color)
background_rgb = np.array(background_color.get_rgb())
ones = np.ones(
(self.pixel_height, self.pixel_width, 1),
dtype = 'uint8'
)
self.background = np.dot(
ones, background_rgb.reshape((1, 3))
)
def get_image(self):
return self.pixel_array
def reset(self):
self.pixel_array = np.array(self.background)
# def get_pixels(image_array): #TODO, FIX WIDTH/HEIGHT PROBLEM HERE
# if image_array is None:
# return np.zeros(
# (DEFAULT_HEIGHT, DEFAULT_WIDTH, 3),
# dtype = 'uint8'
# )
# else:
# pixels = np.array(image_array).astype('uint8')
# assert len(pixels.shape) == 3 and pixels.shape[2] == 3
# return pixels
# def paint_region(region, image_array = None, color = None):
# pixels = get_pixels(image_array)
# assert region.shape == pixels.shape[:2]
# if color is None:
# #Random dark color
# rgb = 0.5 * np.random.random(3)
# else:
# rgb = np.array(Color(color).get_rgb())
# pixels[region.bool_grid] = (255*rgb).astype('uint8')
# return pixels
def capture_mobject(self, mobject):
return self.capture_mobjects([mobject])
def capture_mobjects(self, mobjects, include_sub_mobjects = True):
# pixels = get_pixels(image_array)
# height = pixels.shape[0]
# width = pixels.shape[1]
# space_height = SPACE_HEIGHT
# space_width = SPACE_HEIGHT * width / height
# pixels = pixels.reshape((pixels.size/3, 3)).astype('uint8')
if include_sub_mobjects:
all_families = [
mob.submobject_family()
for mob in mobjects
]
mobjects = reduce(op.add, all_families, [])
for mobject in mobjects:
self.display_points(
mobject.points, mobject.rgbs,
mobject.point_thickness
)
def display_points(self, points, rgbs, thickness):
points = self.project_onto_screen(points)
pixel_coordinates = self.pixel_coordinates_of_points(points)
rgbs = (255*rgbs).astype('uint8')
on_screen_indices = self.on_screen_pixels(pixel_coordinates)
pixel_coordinates = pixel_coordinates[on_screen_indices]
rgbs = rgbs[on_screen_indices]
flattener = np.array([1, self.width], dtype = 'int')
flattener = flattener.reshape((2, 1))
indices = np.dot(pixel_coordinates, flattener)[:,0]
pw, ph = self.pixel_width, self.pixel_height
self.pixel_array.reshape((pw*ph, 3))
self.pixel_array[indices] = rgbs.astype('uint8')
self.pixel_array.reshape((ph, pw, 3))
def project_onto_screen(points):
## TODO
points[:,2] = 0
def pixel_coordinates_of_points(self, points):
result = np.zeros((len(points), 2))
width_mult = self.pixel_width/self.space_width/2
width_add = self.pixel_width/2
height_mult = self.pixel_height/self.space_height/2
height_add = self.pixel_height/2
#Flip on y-axis as you go
height_mult *= -1
result[:,0] = points[:,0]*width_mult + width_add
result[:,1] = points[:,1]*height_mult + height_add
return result
def on_screen_pixels(self, pixel_coordinates):
return (pixel_coordinates[:,0] < 0) | \
(pixel_coordinates[:,0] >= width) | \
(pixel_coordinates[:,1] < 0) | \
(pixel_coordinates[:,1] >= height)
# def add_thickness(pixel_indices_and_rgbs, thickness, width, height):
# """
# Imagine dragging each pixel around like a paintbrush in
# a plus-sign-shaped pixel arrangement surrounding it.
# Pass rgb = None to do nothing to them
# """
# thickness = adjusted_thickness(thickness, width, height)
# original = np.array(pixel_indices_and_rgbs)
# n_extra_columns = pixel_indices_and_rgbs.shape[1] - 2
# for nudge in range(-thickness/2+1, thickness/2+1):
# if nudge == 0:
# continue
# for x, y in [[nudge, 0], [0, nudge]]:
# pixel_indices_and_rgbs = np.append(
# pixel_indices_and_rgbs,
# original+([x, y] + [0]*n_extra_columns),
# axis = 0
# )
# admissibles = (pixel_indices_and_rgbs[:,0] >= 0) & \
# (pixel_indices_and_rgbs[:,0] < width) & \
# (pixel_indices_and_rgbs[:,1] >= 0) & \
# (pixel_indices_and_rgbs[:,1] < height)
# return pixel_indices_and_rgbs[admissibles]
def adjusted_thickness(thickness, width, height):
big_width = PRODUCTION_QUALITY_DISPLAY_CONFIG["width"]
big_height = PRODUCTION_QUALITY_DISPLAY_CONFIG["height"]
factor = (big_width + big_height) / (width + height)
return 1 + (thickness-1)/facto

View file

@ -10,6 +10,7 @@ import imp
from helpers import *
from scene import Scene
from camera import Camera
HELP_MESSAGE = """
Usage:
@ -194,7 +195,9 @@ def main():
inspect.getmembers(module, is_scene)
)
config["movie_prefix"] = config["file"].replace(".py", "")
scene_kwargs = config["display_config"]
scene_kwargs = {
"camera" : Camera(**config["display_config"])
}
for SceneClass in get_scene_classes(scene_names_to_classes, config):
for args in get_scene_args(SceneClass, config):
scene_kwargs["construct_args"] = tuplify(args)

View file

@ -28,7 +28,7 @@ class LogoGeneration(Scene):
def construct(self):
digest_config(self, {})
## Usually shouldn't need this...
self.frame_duration = self.DEFAULT_CONFIG["frame_duration"]
self.frame_duration = self.CONFIG["frame_duration"]
##
digest_config(self, {})
circle = Circle(

View file

@ -86,20 +86,25 @@ def filtered_locals(local_args):
def digest_config(obj, kwargs, local_args = {}):
"""
Sets init args and DEFAULT_CONFIG values as local variables
Sets init args and CONFIG values as local variables
The purpose of this function is to ensure that all
configuration of any object is inheritable, able to
be easily passed into instantiation, and is attached
as an attribute of the object.
"""
### Assemble list of DEFAULT_CONFIGs from all super classes
### Assemble list of CONFIGs from all super classes
classes_in_heirarchy = [obj.__class__]
default_configs = []
configs = []
while len(classes_in_heirarchy) > 0:
Class = classes_in_heirarchy.pop()
classes_in_heirarchy += Class.__bases__
if hasattr(Class, "DEFAULT_CONFIG"):
default_configs.append(Class.DEFAULT_CONFIG)
if hasattr(Class, "CONFIG"):
configs.append(Class.CONFIG)
#Order matters a lot here, first dicts have higher priority
all_dicts = [kwargs, filtered_locals(local_args), obj.__dict__]
all_dicts += default_configs
all_dicts += configs
item_lists = reversed([d.items() for d in all_dicts])
obj.__dict__ = dict(reduce(op.add, item_lists))

View file

@ -109,12 +109,11 @@ class Scene(object):
animation.set_run_time(run_time)
return animations
def separate_static_and_moving_mobjects(self, *animations):
moving_mobjects = [
mobject
def separate_moving_and_static_mobjects(self, *animations):
moving_mobjects = list(it.chain(*[
anim.mobject.submobject_family()
for anim in animations
for mobject in anim.mobject.submobject_family()
]
]))
bundle = Mobject(*self.mobjects)
static_mobjects = filter(
lambda m : m not in moving_mobjects,
@ -126,21 +125,30 @@ class Scene(object):
run_time = animations[0].run_time
times = np.arange(0, run_time, self.frame_duration)
time_progression = ProgressDisplay(times)
time_progression.set_description(
"Animation %d: "%self.num_animations + \
str(animations[0]) + \
(", etc." if len(animations) > 1 else "")
)
time_progression.set_description("".join([
"Animation %d: "%self.num_animations,
str(animations[0]),
(", etc." if len(animations) > 1 else ""),
]))
return time_progression
def update_frame(self, moving_mobjects, static_image = None):
if static_image is not None:
self.camera.set_image(static_image)
else:
self.camera.reset()
self.camera.capture_mobjects(moving_mobjects)
def play(self, *animations, **kwargs):
if len(animations) == 0:
raise Warning("Called Scene.play with no animations")
return
self.num_animations += 1
self.add(*[anim.mobject for anim in animations])
animations = self.align_run_times(*animations, **kwargs)
moving_mobjects, static_mobjects = \
self.separate_static_and_moving_mobjects(*animations)
self.separate_moving_and_static_mobjects(*animations)
self.camera.reset()
self.camera.capture_mobjects(
static_mobjects,
@ -151,14 +159,10 @@ class Scene(object):
for t in self.get_time_progression(animations):
for animation in animations:
animation.update(t / animation.run_time)
self.camera.capture_mobjects(moving_mobjects)
frame = self.camera.get_image()
self.frames.append(frame)
self.camera.set_image(static_image)
self.update_frame(moving_mobjects, static_image)
self.frames.append(self.get_frame())
for animation in animations:
animation.clean_up()
self.add(*[anim.mobject for anim in animations])
return self
def play_over_time_range(self, t0, t1, *animations):
@ -243,14 +247,14 @@ class Scene(object):
print "Writing to %s"%file_path
fps = int(1/self.frame_duration)
dim = tuple(reversed(self.shape))
height, width = self.camera.pixel_shape
command = [
FFMPEG_BIN,
'-y', # overwrite output file if it exists
'-f', 'rawvideo',
'-vcodec','rawvideo',
'-s', '%dx%d'%dim, # size of one frame
'-s', '%dx%d'%(width, height), # size of one frame
'-pix_fmt', 'rgb24',
'-r', str(fps), # frames per second
'-i', '-', # The imput comes from a pipe

61
scene/zoomed_scene.py Normal file
View file

@ -0,0 +1,61 @@
import numpy as np
from scene import Scene
from mobject import Mobject
from camera import MovingCamera, Camera
from helpers import *
class ZoomedScene(Scene):
CONFIG = {
"zoomed_canvas_space_shape" : (3, 3),
"zoomed_canvas_center" : None,
"zoomed_canvas_corner" : UP+RIGHT,
"zoomed_camera_background" : None
}
def init_zooming(self, moving_camera_mobject):
self.setup_zoomed_canvas()
self.zoomed_camera = MovingCamera(
moving_camera_mobject,
pixel_shape = self.zoomed_canvas_pixel_shape,
background = self.zoomed_camera_background
)
self.add(moving_camera_mobject)
def setup_zoomed_canvas(self):
height, width = self.zoomed_canvas_space_shape
canvas_corners = Mobject().add_points([
ORIGIN, DOWN+RIGHT
])
canvas_corners.stretch_to_fit_height(height)
canvas_corners.stretch_to_fit_width(width)
canvas_corners.center()
if self.zoomed_canvas_center is not None:
canvas_corners.shift(self.zoomed_canvas_center)
elif self.zoomed_canvas_corner is not None:
canvas_corners.to_corner(self.zoomed_canvas_corner)
pixel_coords = self.camera.points_to_pixel_coords(canvas_corners.points)
upper_left, lower_right = pixel_coords
self.zoomed_canvas_pixel_indices = pixel_coords
self.zoomed_canvas_pixel_shape = (
lower_right[0] - upper_left[0],
lower_right[1] - upper_left[1]
)
def get_frame(self):
frame = Scene.get_frame(self)
(up, left), (down, right) = self.zoomed_canvas_pixel_indices
frame[left:right, up:down, :] = self.zoomed_camera.get_image()
return frame
def update_frame(self, *args, **kwargs):
Scene.update_frame(self, *args, **kwargs)
self.zoomed_camera.reset()
self.zoomed_camera.capture_mobjects(self.mobjects)