Reimplemented ZoomedScene, using a new MultiCamera

This commit is contained in:
Grant Sanderson
2018-05-10 15:55:31 -07:00
parent 1f394ca2eb
commit 7ee85faadd
6 changed files with 249 additions and 38 deletions

View File

@ -11,7 +11,7 @@ from colour import Color
from scipy.spatial.distance import pdist from scipy.spatial.distance import pdist
from constants import * from constants import *
from mobject.types.image_mobject import ImageMobject from mobject.types.image_mobject import AbstractImageMobject
from mobject.mobject import Mobject from mobject.mobject import Mobject
from mobject.types.point_cloud_mobject import PMobject from mobject.types.point_cloud_mobject import PMobject
from mobject.types.vectorized_mobject import VMobject from mobject.types.vectorized_mobject import VMobject
@ -61,6 +61,12 @@ class Camera(object):
self.canvas = None self.canvas = None
return copy.copy(self) return copy.copy(self)
def reset_pixel_shape(self, new_shape):
self.pixel_shape = tuple(new_shape)
self.init_background()
self.resize_frame_shape()
self.reset()
def resize_frame_shape(self, fixed_dimension=0): def resize_frame_shape(self, fixed_dimension=0):
""" """
Changes frame_shape to match the aspect ratio Changes frame_shape to match the aspect ratio
@ -68,13 +74,14 @@ class Camera(object):
whether frame_shape[0] (height) or frame_shape[1] (width) whether frame_shape[0] (height) or frame_shape[1] (width)
remains fixed while the other changes accordingly. remains fixed while the other changes accordingly.
""" """
aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0] frame_height, frame_width = self.frame_shape
frame_width, frame_height = self.frame_shape pixel_height, pixel_width = self.pixel_shape
aspect_ratio = fdiv(pixel_width, pixel_height)
if fixed_dimension == 0: if fixed_dimension == 0:
frame_height = aspect_ratio * frame_width frame_height = frame_width / aspect_ratio
else: else:
frame_width = frame_height / aspect_ratio frame_width = aspect_ratio * frame_height
self.frame_shape = (frame_width, frame_height) self.frame_shape = (frame_height, frame_width)
def init_background(self): def init_background(self):
if self.background_image is not None: if self.background_image is not None:
@ -151,6 +158,7 @@ class Camera(object):
def reset(self): def reset(self):
self.set_pixel_array(self.background) self.set_pixel_array(self.background)
return self
#### ####
@ -204,7 +212,7 @@ class Camera(object):
type_func_pairs = [ type_func_pairs = [
(VMobject, self.display_multiple_vectorized_mobjects), (VMobject, self.display_multiple_vectorized_mobjects),
(PMobject, self.display_multiple_point_cloud_mobjects), (PMobject, self.display_multiple_point_cloud_mobjects),
(ImageMobject, self.display_multiple_image_mobjects), (AbstractImageMobject, self.display_multiple_image_mobjects),
(Mobject, lambda batch: batch), # Do nothing (Mobject, lambda batch: batch), # Do nothing
] ]

View File

@ -44,7 +44,7 @@ class MappingCamera(Camera):
# CameraPlusOverlay class) # CameraPlusOverlay class)
class MultiCamera(Camera): class OldMultiCamera(Camera):
def __init__(self, *cameras_with_start_positions, **kwargs): def __init__(self, *cameras_with_start_positions, **kwargs):
self.shifted_cameras = [ self.shifted_cameras = [
DictAsObject( DictAsObject(
@ -92,11 +92,11 @@ class MultiCamera(Camera):
for shifted_camera in self.shifted_cameras: for shifted_camera in self.shifted_cameras:
shifted_camera.camera.init_background() shifted_camera.camera.init_background()
# A MultiCamera which, when called with two full-size cameras, initializes itself # A OldMultiCamera which, when called with two full-size cameras, initializes itself
# as a splitscreen, also taking care to resize each individual camera within it # as a splitscreen, also taking care to resize each individual camera within it
class SplitScreenCamera(MultiCamera): class SplitScreenCamera(OldMultiCamera):
def __init__(self, left_camera, right_camera, **kwargs): def __init__(self, left_camera, right_camera, **kwargs):
digest_config(self, kwargs) digest_config(self, kwargs)
self.left_camera = left_camera self.left_camera = left_camera
@ -110,5 +110,8 @@ class SplitScreenCamera(MultiCamera):
camera.resize_frame_shape() camera.resize_frame_shape()
camera.reset() camera.reset()
MultiCamera.__init__(self, (left_camera, (0, 0)), OldMultiCamera.__init__(
(right_camera, (0, half_width))) self,
(left_camera, (0, 0)),
(right_camera, (0, half_width)),
)

View File

@ -1,9 +1,11 @@
from __future__ import absolute_import from __future__ import absolute_import
from constants import FRAME_HEIGHT from constants import FRAME_HEIGHT
from constants import WHITE
from camera.camera import Camera from camera.camera import Camera
from mobject.frame import ScreenRectangle from mobject.frame import ScreenRectangle
from utils.config_ops import digest_config
class MovingCamera(Camera): class MovingCamera(Camera):
@ -12,7 +14,9 @@ class MovingCamera(Camera):
of a given mobject of a given mobject
""" """
CONFIG = { CONFIG = {
"aligned_dimension": "width" # or height "fixed_dimension": 0, # width
"default_frame_stroke_color": WHITE,
"default_frame_stroke_width": 0,
} }
def __init__(self, frame=None, **kwargs): def __init__(self, frame=None, **kwargs):
@ -20,21 +24,28 @@ class MovingCamera(Camera):
frame is a Mobject, (should be a rectangle) determining frame is a Mobject, (should be a rectangle) determining
which region of space the camera displys which region of space the camera displys
""" """
digest_config(self, kwargs)
if frame is None: if frame is None:
frame = ScreenRectangle(height=FRAME_HEIGHT) frame = ScreenRectangle(height=FRAME_HEIGHT)
frame.fade(1) frame.set_stroke(
self.default_frame_stroke_color,
self.default_frame_stroke_width,
)
self.frame = frame self.frame = frame
Camera.__init__(self, **kwargs) Camera.__init__(self, **kwargs)
def capture_mobjects(self, *args, **kwargs): def capture_mobjects(self, mobjects, **kwargs):
self.space_center = self.frame.get_center() self.reset_space_center()
self.realign_frame_shape() self.realign_frame_shape()
Camera.capture_mobjects(self, *args, **kwargs) Camera.capture_mobjects(self, mobjects, **kwargs)
def reset_space_center(self):
self.space_center = self.frame.get_center()
def realign_frame_shape(self): def realign_frame_shape(self):
height, width = self.frame_shape height, width = self.frame_shape
if self.aligned_dimension == "height": if self.fixed_dimension == 0:
self.frame_shape = (self.frame.get_height(), width)
else:
self.frame_shape = (height, self.frame.get_width()) self.frame_shape = (height, self.frame.get_width())
self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1) else:
self.frame_shape = (self.frame.get_height(), width)
self.resize_frame_shape(fixed_dimension=self.fixed_dimension)

60
camera/multi_camera.py Normal file
View File

@ -0,0 +1,60 @@
from __future__ import absolute_import
from camera.moving_camera import MovingCamera
from utils.iterables import list_difference_update
# Distinct notions of view frame vs. display frame
# For now, let's say it is the responsibility of the scene holding
# this camera to add all of the relevant image_mobjects_from_cameras,
# as well as their display_frames
class MultiCamera(MovingCamera):
CONFIG = {
# "lock_display_frame_dimensions_to_view_frame": True,
# "display_frame_fixed_dimension": 1, # Height
"allow_cameras_to_capture_their_own_display": False,
}
def __init__(self, *image_mobjects_from_cameras, **kwargs):
self.image_mobjects_from_cameras = []
for imfc in image_mobjects_from_cameras:
self.add_image_mobject_from_camera(imfc)
MovingCamera.__init__(self, **kwargs)
def add_image_mobject_from_camera(self, image_mobject_from_camera):
# A silly method to have right now, but maybe there are things
# we want to guarantee about any imfc's added later.
imfc = image_mobject_from_camera
self.image_mobjects_from_cameras.append(imfc)
def update_sub_cameras(self):
# if self.lock_display_frame_dimensions_to_view_frame:
# for imfc in self.image_mobjects_from_cameras:
# aspect_ratio = imfc.view_frame.get_width() / imfc.view_frame.get_height()
# Reshape sub_camera pixel_arrays
for imfc in self.image_mobjects_from_cameras:
frame_height, frame_width = self.frame_shape
pixel_height, pixel_width = self.get_pixel_array().shape[:2]
imfc.camera.frame_shape = (
imfc.camera.frame.get_height(),
imfc.camera.frame.get_width(),
)
imfc.camera.reset_pixel_shape((
int(pixel_height * imfc.get_height() / frame_height),
int(pixel_width * imfc.get_width() / frame_width),
))
def capture_mobjects(self, mobjects, **kwargs):
# Make sure all frames are in mobjects? Or not?
self.update_sub_cameras()
for imfc in self.image_mobjects_from_cameras:
to_add = list(mobjects)
if not self.allow_cameras_to_capture_their_own_display:
to_add = list_difference_update(
to_add, imfc.submobject_family()
)
imfc.camera.capture_mobjects(to_add, **kwargs)
MovingCamera.capture_mobjects(self, mobjects, **kwargs)

View File

@ -7,13 +7,51 @@ from PIL import Image
from constants import * from constants import *
from mobject.mobject import Mobject from mobject.mobject import Mobject
from mobject.shape_matchers import SurroundingRectangle
from utils.bezier import interpolate from utils.bezier import interpolate
from utils.color import color_to_int_rgb from utils.color import color_to_int_rgb
from utils.config_ops import digest_config from utils.config_ops import digest_config
from utils.images import get_full_raster_image_path from utils.images import get_full_raster_image_path
class ImageMobject(Mobject): class AbstractImageMobject(Mobject):
"""
Automatically filters out black pixels
"""
CONFIG = {
"filter_color": "black",
"invert": False,
# "use_cache" : True,
"height": 2.0,
"image_mode": "RGBA",
"pixel_array_dtype": "uint8",
}
def get_pixel_array(self):
raise Exception("Not implemented")
def set_color(self):
# Likely to be implemented in subclasses
pass
def init_points(self):
# Corresponding corners of image are fixed to these
# Three points
self.points = np.array([
UP + LEFT,
UP + RIGHT,
DOWN + LEFT,
])
self.center()
h, w = self.get_pixel_array().shape[:2]
self.stretch_to_fit_height(self.height)
self.stretch_to_fit_width(self.height * w / h)
def copy(self):
return self.deepcopy()
class ImageMobject(AbstractImageMobject):
""" """
Automatically filters out black pixels Automatically filters out black pixels
""" """
@ -37,7 +75,7 @@ class ImageMobject(Mobject):
self.change_to_rgba_array() self.change_to_rgba_array()
if self.invert: if self.invert:
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3] self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
Mobject.__init__(self, **kwargs) AbstractImageMobject.__init__(self, **kwargs)
def change_to_rgba_array(self): def change_to_rgba_array(self):
pa = self.pixel_array pa = self.pixel_array
@ -53,6 +91,9 @@ class ImageMobject(Mobject):
pa = np.append(pa, alphas, axis=2) pa = np.append(pa, alphas, axis=2)
self.pixel_array = pa self.pixel_array = pa
def get_pixel_array(self):
return self.pixel_array
def set_color(self, color, alpha=None, family=True): def set_color(self, color, alpha=None, family=True):
rgb = color_to_int_rgb(color) rgb = color_to_int_rgb(color)
self.pixel_array[:, :, :3] = rgb self.pixel_array[:, :, :3] = rgb
@ -63,19 +104,6 @@ class ImageMobject(Mobject):
self.color = color self.color = color
return self return self
def init_points(self):
# Corresponding corners of image are fixed to these
# Three points
self.points = np.array([
UP + LEFT,
UP + RIGHT,
DOWN + LEFT,
])
self.center()
h, w = self.pixel_array.shape[:2]
self.stretch_to_fit_height(self.height)
self.stretch_to_fit_width(self.height * w / h)
def set_opacity(self, alpha): def set_opacity(self, alpha):
self.pixel_array[:, :, 3] = int(255 * alpha) self.pixel_array[:, :, 3] = int(255 * alpha)
return self return self
@ -90,5 +118,26 @@ class ImageMobject(Mobject):
mobject1.pixel_array, mobject2.pixel_array, alpha mobject1.pixel_array, mobject2.pixel_array, alpha
).astype(self.pixel_array_dtype) ).astype(self.pixel_array_dtype)
def copy(self):
return self.deepcopy() class ImageMobjectFromCamera(AbstractImageMobject):
CONFIG = {
"default_display_frame_config": {
"stroke_width": 3,
"stroke_color": WHITE,
"buff": 0,
}
}
def __init__(self, camera, **kwargs):
self.camera = camera
AbstractImageMobject.__init__(self, **kwargs)
def get_pixel_array(self):
return self.camera.get_pixel_array()
def add_display_frame(self, **kwargs):
config = dict(self.default_display_frame_config)
config.update(kwargs)
self.display_frame = SurroundingRectangle(self, **config)
self.add(self.display_frame)
return self

View File

@ -5,12 +5,92 @@ import numpy as np
from scene.scene import Scene from scene.scene import Scene
from animation.creation import FadeIn from animation.creation import FadeIn
from camera.moving_camera import MovingCamera from camera.moving_camera import MovingCamera
from camera.multi_camera import MultiCamera
from mobject.geometry import Rectangle from mobject.geometry import Rectangle
from mobject.types.image_mobject import ImageMobjectFromCamera
from constants import * from constants import *
class ZoomedScene(Scene): class ZoomedScene(Scene):
CONFIG = {
"camera_class": MultiCamera,
"zoomed_display_height": 3,
"zoomed_display_width": 3,
"zoomed_display_center": None,
"zoomed_display_corner": UP + RIGHT,
"zoomed_display_corner_buff": DEFAULT_MOBJECT_TO_EDGE_BUFFER,
"zoomed_camera_config": {
"default_frame_stroke_width": 2,
},
"zoomed_camera_image_mobject_config": {},
"zoomed_camera_frame_starting_position": ORIGIN,
"zoom_factor": 0.15,
"image_frame_stroke_width": 3,
"zoom_activated": False,
}
def setup(self):
# Initialize camera and display
zoomed_camera = MovingCamera(**self.zoomed_camera_config)
zoomed_display = ImageMobjectFromCamera(
zoomed_camera, **self.zoomed_camera_image_mobject_config
)
zoomed_display.add_display_frame()
for mob in zoomed_camera.frame, zoomed_display:
mob.stretch_to_fit_height(self.zoomed_display_height)
mob.stretch_to_fit_width(self.zoomed_display_width)
zoomed_camera.frame.scale(self.zoom_factor)
# Position camera and display
zoomed_camera.frame.move_to(self.zoomed_camera_frame_starting_position)
if self.zoomed_display_center is not None:
zoomed_display.move_to(self.zoomed_display_center)
else:
zoomed_display.to_corner(
self.zoomed_display_corner,
buff=self.zoomed_display_corner_buff
)
self.zoomed_camera = zoomed_camera
self.zoomed_display = zoomed_display
def activate_zooming(self, animate=False, run_times=[3, 2]):
self.zoom_activated = True
zoomed_camera = self.zoomed_camera
zoomed_display = self.zoomed_display
self.camera.add_image_mobject_from_camera(zoomed_display)
to_add = [zoomed_camera.frame, zoomed_display]
if animate:
zoomed_display.save_state()
zoomed_display.replace(zoomed_camera.frame)
full_frame_height, full_frame_width = self.camera.frame_shape
zoomed_camera.frame.save_state()
zoomed_camera.frame.stretch_to_fit_width(full_frame_width)
zoomed_camera.frame.stretch_to_fit_height(full_frame_height)
zoomed_camera.frame.center()
zoomed_camera.frame.set_stroke(width=0)
for mover, run_time in zip(to_add, run_times):
self.add_foreground_mobject(mover)
self.play(mover.restore, run_time=run_time)
else:
self.add_foreground_mobjects(*to_add)
def get_moving_mobjects(self, *animations):
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
zoomed_mobjects = [self.zoomed_camera.frame, self.zoomed_display]
moving_zoomed_mobjects = set(moving_mobjects).intersection(zoomed_mobjects)
if self.zoom_activated and moving_zoomed_mobjects:
return self.mobjects
else:
return moving_mobjects
class OldZoomedScene(Scene):
""" """
Move around self.little_rectangle to determine Move around self.little_rectangle to determine
which part of the screen is zoomed in on. which part of the screen is zoomed in on.