Merge branch 'master' of github.com:3b1b/manim into alt-calc

This commit is contained in:
Grant Sanderson 2018-05-19 11:21:38 -07:00
commit 17a1ea6db5
22 changed files with 322 additions and 462 deletions

View file

@ -206,7 +206,7 @@ class LeaveItToComputers(TeacherStudentsScene):
class PrerequisiteKnowledge(TeacherStudentsScene):
CONFIG = {
"camera_config": {"background_opacity": 1}
"camera_config": {"background_alpha": 255}
}
def construct(self):

View file

@ -140,13 +140,6 @@ class ApplyPointwiseFunction(ApplyMethod):
)
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
def __init__(self, function, mobject, **kwargs):
ApplyMethod.__init__(
self, mobject.move_to, function(mobject.get_center()), **kwargs
)
class FadeToColor(ApplyMethod):
def __init__(self, mobject, color, **kwargs):
ApplyMethod.__init__(self, mobject.set_color, color, **kwargs)

View file

@ -1,6 +1,5 @@
import itertools as it
import numpy as np
import operator as op
import aggdraw
import copy
@ -8,10 +7,9 @@ import time
from PIL import Image
from colour import Color
from scipy.spatial.distance import pdist
from constants import *
from mobject.types.image_mobject import AbstractImageMobject
from mobject.types.image_mobject import ImageMobject
from mobject.mobject import Mobject
from mobject.types.point_cloud_mobject import PMobject
from mobject.types.vectorized_mobject import VMobject
@ -23,26 +21,22 @@ from utils.iterables import batch_by_property
from utils.iterables import list_difference_update
from utils.iterables import remove_list_redundancies
from utils.simple_functions import fdiv
from utils.space_ops import angle_of_vector
class Camera(object):
CONFIG = {
"background_image": None,
"pixel_height": DEFAULT_PIXEL_HEIGHT,
"pixel_width": DEFAULT_PIXEL_WIDTH,
# Note: frame height and width will be resized to match
# the pixel aspect ratio
"frame_height": FRAME_HEIGHT,
"frame_width": FRAME_WIDTH,
"frame_center": ORIGIN,
"pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
# Note: frame_shape will be resized to match pixel_shape
"frame_shape": (FRAME_HEIGHT, FRAME_WIDTH),
"space_center": ORIGIN,
"background_color": BLACK,
"background_opacity": 0,
# Points in vectorized mobjects with norm greater
# than this value will be rescaled.
"max_allowable_norm": FRAME_WIDTH,
"image_mode": "RGBA",
"n_rgb_coords": 4,
"background_alpha": 0, # Out of rgb_max_val
"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.
@ -64,72 +58,36 @@ class Camera(object):
self.canvas = None
return copy.copy(self)
def reset_pixel_shape(self, new_height, new_width):
self.pixel_width = new_width
self.pixel_height = new_height
self.init_background()
self.resize_frame_shape()
self.reset()
def get_pixel_height(self):
return self.pixel_height
def get_pixel_width(self):
return self.pixel_width
def get_frame_height(self):
return self.frame_height
def get_frame_width(self):
return self.frame_width
def get_frame_center(self):
return self.frame_center
def set_frame_height(self, frame_height):
self.frame_height = frame_height
def set_frame_width(self, frame_width):
self.frame_width = frame_width
def set_frame_center(self, frame_center):
self.frame_center = frame_center
def resize_frame_shape(self, fixed_dimension=0):
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
whether frame_height or frame_width
of pixel_shape, where fixed_dimension determines
whether frame_shape[0] (height) or frame_shape[1] (width)
remains fixed while the other changes accordingly.
"""
pixel_height = self.get_pixel_height()
pixel_width = self.get_pixel_width()
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
aspect_ratio = fdiv(pixel_width, pixel_height)
aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0]
frame_width, frame_height = self.frame_shape
if fixed_dimension == 0:
frame_height = frame_width / aspect_ratio
frame_height = aspect_ratio * frame_width
else:
frame_width = aspect_ratio * frame_height
self.set_frame_height(frame_height)
self.set_frame_width(frame_width)
frame_width = frame_height / aspect_ratio
self.frame_shape = (frame_width, frame_height)
def init_background(self):
height = self.get_pixel_height()
width = self.get_pixel_width()
if self.background_image is not None:
path = get_full_raster_image_path(self.background_image)
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]
self.background = self.background.astype(self.pixel_array_dtype)
else:
background_rgba = color_to_int_rgba(
self.background_color, self.background_opacity
self.background_color, alpha=self.background_alpha
)
self.background = np.zeros(
(height, width, self.n_rgb_coords),
list(self.pixel_shape) + [self.n_rgb_coords],
dtype=self.pixel_array_dtype
)
self.background[:, :] = background_rgba
@ -147,10 +105,10 @@ class Camera(object):
retval = np.array(pixel_array)
if convert_from_floats:
retval = np.apply_along_axis(
lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype),
lambda f: (
f * self.rgb_max_val).astype(self.pixel_array_dtype),
2,
retval
)
retval)
return retval
def set_pixel_array(self, pixel_array, convert_from_floats=False):
@ -190,7 +148,6 @@ class Camera(object):
def reset(self):
self.set_pixel_array(self.background)
return self
####
@ -244,7 +201,7 @@ class Camera(object):
type_func_pairs = [
(VMobject, self.display_multiple_vectorized_mobjects),
(PMobject, self.display_multiple_point_cloud_mobjects),
(AbstractImageMobject, self.display_multiple_image_mobjects),
(ImageMobject, self.display_multiple_image_mobjects),
(Mobject, lambda batch: batch), # Do nothing
]
@ -344,7 +301,8 @@ class Camera(object):
# points = self.adjust_out_of_range_points(points)
if len(points) == 0:
continue
coords = self.points_to_pixel_coords(points)
aligned_points = self.align_points_to_camera(points)
coords = self.points_to_pixel_coords(aligned_points)
coord_strings = coords.flatten().astype(str)
# Start new path string with M
coord_strings[0] = "M" + coord_strings[0]
@ -384,6 +342,7 @@ class Camera(object):
def display_point_cloud(self, points, rgbas, thickness):
if len(points) == 0:
return
points = self.align_points_to_camera(points)
pixel_coords = self.points_to_pixel_coords(points)
pixel_coords = self.thickened_coordinates(
pixel_coords, thickness
@ -399,8 +358,7 @@ class Camera(object):
pixel_coords = pixel_coords[on_screen_indices]
rgbas = rgbas[on_screen_indices]
ph = self.get_pixel_height()
pw = self.get_pixel_width()
ph, pw = self.pixel_shape
flattener = np.array([1, pw], dtype='int')
flattener = flattener.reshape((2, 1))
@ -420,57 +378,95 @@ class Camera(object):
ul_coords, ur_coords, dl_coords = corner_coords
right_vect = ur_coords - ul_coords
down_vect = dl_coords - ul_coords
center_coords = ul_coords + (right_vect + down_vect) / 2
sub_image = Image.fromarray(
image_mobject.get_pixel_array(),
mode="RGBA"
)
impa = image_mobject.pixel_array
# Reshape
pixel_width = int(pdist([ul_coords, ur_coords]))
pixel_height = int(pdist([ul_coords, dl_coords]))
sub_image = sub_image.resize(
(pixel_width, pixel_height), resample=Image.BICUBIC
)
oh, ow = self.pixel_array.shape[:2] # Outer width and height
ih, iw = impa.shape[:2] # inner with and height
rgb_len = self.pixel_array.shape[2]
# Rotate
angle = angle_of_vector(right_vect)
adjusted_angle = -int(360 * angle / TAU)
if adjusted_angle != 0:
sub_image = sub_image.rotate(
adjusted_angle, resample=Image.BICUBIC, expand=1
)
image = np.zeros((oh, ow, rgb_len), dtype=self.pixel_array_dtype)
# TODO, there is no accounting for a shear...
if right_vect[1] == 0 and down_vect[0] == 0:
rv0 = right_vect[0]
dv1 = down_vect[1]
x_indices = np.arange(rv0, dtype='int') * iw / rv0
y_indices = np.arange(dv1, dtype='int') * ih / dv1
stretched_impa = impa[y_indices][:, x_indices]
# Paste into an image as large as the camear's pixel array
full_image = Image.fromarray(
np.zeros((self.get_pixel_height(), self.get_pixel_width())),
mode="RGBA"
)
new_ul_coords = center_coords - np.array(sub_image.size) / 2
full_image.paste(
sub_image,
box=(
new_ul_coords[0],
new_ul_coords[1],
new_ul_coords[0] + sub_image.size[0],
new_ul_coords[1] + sub_image.size[1],
)
)
x0, x1 = ul_coords[0], ur_coords[0]
y0, y1 = ul_coords[1], dl_coords[1]
if x0 >= ow or x1 < 0 or y0 >= oh or y1 < 0:
return
siy0 = max(-y0, 0) # stretched_impa y0
siy1 = dv1 - max(y1 - oh, 0)
six0 = max(-x0, 0)
six1 = rv0 - max(x1 - ow, 0)
x0 = max(x0, 0)
y0 = max(y0, 0)
image[y0:y1, x0:x1] = stretched_impa[siy0:siy1, six0:six1]
else:
# Alternate (slower) tactic if image is tilted
# List of all coordinates of pixels, given as (x, y),
# which matches the return type of points_to_pixel_coords,
# even though np.array indexing naturally happens as (y, x)
all_pixel_coords = np.zeros((oh * ow, 2), dtype='int')
a = np.arange(oh * ow, dtype='int')
all_pixel_coords[:, 0] = a % ow
all_pixel_coords[:, 1] = a / ow
# Paint on top of existing pixel array
self.overlay_PIL_image(full_image)
recentered_coords = all_pixel_coords - ul_coords
with np.errstate(divide='ignore'):
ix_coords, iy_coords = [
np.divide(
dim * np.dot(recentered_coords, vect),
np.dot(vect, vect),
)
for vect, dim in (right_vect, iw), (down_vect, ih)
]
to_change = reduce(op.and_, [
ix_coords >= 0, ix_coords < iw,
iy_coords >= 0, iy_coords < ih,
])
inner_flat_coords = iw * \
iy_coords[to_change] + ix_coords[to_change]
flat_impa = impa.reshape((iw * ih, rgb_len))
target_rgbas = flat_impa[inner_flat_coords, :]
image = image.reshape((ow * oh, rgb_len))
image[to_change] = target_rgbas
image = image.reshape((oh, ow, rgb_len))
self.overlay_rgba_array(image)
def overlay_rgba_array(self, arr):
self.overlay_PIL_image(Image.fromarray(arr, mode="RGBA"))
fg = arr
bg = self.pixel_array
# rgba_max_val = self.rgb_max_val
src_rgb, src_a, dst_rgb, dst_a = [
a.astype(np.float32) / self.rgb_max_val
for a in fg[..., :3], fg[..., 3], bg[..., :3], bg[..., 3]
]
def overlay_PIL_image(self, image):
self.pixel_array = np.array(
Image.alpha_composite(self.get_image(), image)
out_a = src_a + dst_a * (1.0 - src_a)
# When the output alpha is 0 for full transparency,
# we have a choice over what RGB value to use in our
# output representation. We choose 0 here.
out_rgb = fdiv(
src_rgb * src_a[..., None] +
dst_rgb * dst_a[..., None] * (1.0 - src_a[..., None]),
out_a[..., None],
zero_over_zero_value=0
)
self.pixel_array[..., :3] = out_rgb * self.rgb_max_val
self.pixel_array[..., 3] = out_a * self.rgb_max_val
def align_points_to_camera(self, points):
# This is where projection should live
return points - self.space_center
def adjust_out_of_range_points(self, points):
if not np.any(points > self.max_allowable_norm):
return points
@ -487,43 +483,31 @@ class Camera(object):
return points
def points_to_pixel_coords(self, points):
shifted_points = points - self.get_frame_center()
result = np.zeros((len(points), 2))
pixel_height = self.get_pixel_height()
pixel_width = self.get_pixel_width()
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
width_mult = pixel_width / frame_width
width_add = pixel_width / 2
height_mult = pixel_height / frame_height
height_add = pixel_height / 2
ph, pw = self.pixel_shape
sh, sw = self.frame_shape
width_mult = pw / sw
width_add = pw / 2
height_mult = ph / sh
height_add = ph / 2
# Flip on y-axis as you go
height_mult *= -1
result[:, 0] = shifted_points[:, 0] * width_mult + width_add
result[:, 1] = shifted_points[:, 1] * height_mult + height_add
result[:, 0] = points[:, 0] * width_mult + width_add
result[:, 1] = points[:, 1] * height_mult + height_add
return result.astype('int')
def on_screen_pixels(self, pixel_coords):
return reduce(op.and_, [
pixel_coords[:, 0] >= 0,
pixel_coords[:, 0] < self.get_pixel_width(),
pixel_coords[:, 0] < self.pixel_shape[1],
pixel_coords[:, 1] >= 0,
pixel_coords[:, 1] < self.get_pixel_height(),
pixel_coords[:, 1] < self.pixel_shape[0],
])
def adjusted_thickness(self, thickness):
# TODO: This seems...unsystematic
big_sum = op.add(
PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_height"],
PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"],
)
this_sum = op.add(
self.get_pixel_height(),
self.get_pixel_width(),
)
factor = fdiv(big_sum, this_sum)
big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"]
factor = sum(big_shape) / sum(self.pixel_shape)
return 1 + (thickness - 1) / factor
def get_thickening_nudges(self, thickness):
@ -541,20 +525,13 @@ class Camera(object):
def get_coords_of_all_pixels(self):
# These are in x, y order, to help me keep things straight
full_space_dims = np.array([
self.get_frame_width(),
self.get_frame_height()
])
full_pixel_dims = np.array([
self.get_pixel_width(),
self.get_pixel_height()
])
full_space_dims = np.array(self.frame_shape)[::-1]
full_pixel_dims = np.array(self.pixel_shape)[::-1]
# These are addressed in the same y, x order as in pixel_array, but the values in them
# are listed in x, y order
uncentered_pixel_coords = np.indices(
[self.get_pixel_height(), self.get_pixel_width()]
)[::-1].transpose(1, 2, 0)
uncentered_pixel_coords = np.indices(self.pixel_shape)[
::-1].transpose(1, 2, 0)
uncentered_space_coords = fdiv(
uncentered_pixel_coords * full_space_dims,
full_pixel_dims)
@ -564,8 +541,7 @@ class Camera(object):
# overflow is unlikely to be a problem)
centered_space_coords = (
uncentered_space_coords - fdiv(full_space_dims, 2)
)
uncentered_space_coords - fdiv(full_space_dims, 2))
# Have to also flip the y coordinates to account for pixel array being listed in
# top-to-bottom order, opposite of screen coordinate convention

View file

@ -43,9 +43,8 @@ class MappingCamera(Camera):
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
# CameraPlusOverlay class)
# TODO, the classes below should likely be deleted
class OldMultiCamera(Camera):
class MultiCamera(Camera):
def __init__(self, *cameras_with_start_positions, **kwargs):
self.shifted_cameras = [
DictAsObject(
@ -53,8 +52,8 @@ class OldMultiCamera(Camera):
"camera": camera_with_start_positions[0],
"start_x": camera_with_start_positions[1][1],
"start_y": camera_with_start_positions[1][0],
"end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].get_pixel_width(),
"end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].get_pixel_height(),
"end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].pixel_shape[1],
"end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].pixel_shape[0],
})
for camera_with_start_positions in cameras_with_start_positions
]
@ -93,23 +92,23 @@ class OldMultiCamera(Camera):
for shifted_camera in self.shifted_cameras:
shifted_camera.camera.init_background()
# A OldMultiCamera which, when called with two full-size cameras, initializes itself
# A MultiCamera which, when called with two full-size cameras, initializes itself
# as a splitscreen, also taking care to resize each individual camera within it
class SplitScreenCamera(OldMultiCamera):
class SplitScreenCamera(MultiCamera):
def __init__(self, left_camera, right_camera, **kwargs):
digest_config(self, kwargs)
self.left_camera = left_camera
self.right_camera = right_camera
half_width = self.get_pixel_width() / 2
half_width = self.pixel_shape[1] / 2
for camera in [self.left_camera, self.right_camera]:
# TODO: Round up on one if width is odd
camera.reset_pixel_shape(camera.get_pixel_height(), half_width)
camera.pixel_shape = (self.pixel_shape[0], half_width)
camera.init_background()
camera.resize_frame_shape()
camera.reset()
OldMultiCamera.__init__(
self,
(left_camera, (0, 0)),
(right_camera, (0, half_width)),
)
MultiCamera.__init__(self, (left_camera, (0, 0)),
(right_camera, (0, half_width)))

View file

@ -1,77 +1,40 @@
from __future__ import absolute_import
from constants import FRAME_HEIGHT
from constants import WHITE
from camera.camera import Camera
from mobject.frame import ScreenRectangle
from utils.config_ops import digest_config
class MovingCamera(Camera):
"""
Stays in line with the height, width and position of it's 'frame', which is a Rectangle
Stays in line with the height, width and position
of a given mobject
"""
CONFIG = {
"fixed_dimension": 0, # width
"default_frame_stroke_color": WHITE,
"default_frame_stroke_width": 0,
"aligned_dimension": "width" # or height
}
def __init__(self, frame=None, **kwargs):
"""
frame is a Mobject, (should almost certainly be a rectangle)
determining which region of space the camera displys
frame is a Mobject, (should be a rectangle) determining
which region of space the camera displys
"""
digest_config(self, kwargs)
if frame is None:
frame = ScreenRectangle(height=FRAME_HEIGHT)
frame.set_stroke(
self.default_frame_stroke_color,
self.default_frame_stroke_width,
)
frame.fade(1)
self.frame = frame
Camera.__init__(self, **kwargs)
# TODO, make these work for a rotated frame
def get_frame_height(self):
return self.frame.get_height()
def capture_mobjects(self, *args, **kwargs):
self.space_center = self.frame.get_center()
self.realign_frame_shape()
Camera.capture_mobjects(self, *args, **kwargs)
def get_frame_width(self):
return self.frame.get_width()
def get_frame_center(self):
return self.frame.get_center()
def set_frame_height(self, frame_height):
self.frame.stretch_to_fit_height(frame_height)
def set_frame_width(self, frame_width):
self.frame.stretch_to_fit_width(frame_width)
def set_frame_center(self, frame_center):
self.frame.move_to(frame_center)
def capture_mobjects(self, mobjects, **kwargs):
# self.reset_frame_center()
# self.realign_frame_shape()
Camera.capture_mobjects(self, mobjects, **kwargs)
# def reset_frame_center(self):
# self.frame_center = self.frame.get_center()
# def realign_frame_shape(self):
# height, width = self.frame_shape
# if self.fixed_dimension == 0:
# self.frame_shape = (height, self.frame.get_width())
# else:
# self.frame_shape = (self.frame.get_height(), width)
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
def get_mobjects_indicating_movement(self):
"""
Returns all mobjets whose movement implies that the camera
should think of all other mobjects on the screen as moving
"""
return [self.frame]
def realign_frame_shape(self):
height, width = self.frame_shape
if self.aligned_dimension == "height":
self.frame_shape = (self.frame.get_height(), width)
else:
self.frame_shape = (height, self.frame.get_width())
self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1)

View file

@ -1,59 +0,0 @@
from __future__ import absolute_import
from camera.moving_camera import MovingCamera
from utils.iterables import list_difference_update
class MultiCamera(MovingCamera):
CONFIG = {
"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
assert(isinstance(imfc.camera, MovingCamera))
self.image_mobjects_from_cameras.append(imfc)
def update_sub_cameras(self):
""" Reshape sub_camera pixel_arrays """
for imfc in self.image_mobjects_from_cameras:
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() / self.get_frame_height()),
int(pixel_width * imfc.get_width() / self.get_frame_width()),
)
def reset(self):
for imfc in self.image_mobjects_from_cameras:
imfc.camera.reset()
MovingCamera.reset(self)
return self
def capture_mobjects(self, mobjects, **kwargs):
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)
def get_mobjects_indicating_movement(self):
return [self.frame] + [
imfc.camera.frame
for imfc in self.image_mobjects_from_cameras
]

View file

@ -12,8 +12,6 @@ from utils.bezier import interpolate
from utils.space_ops import rotation_about_z
from utils.space_ops import rotation_matrix
# TODO: Make sure this plays well with latest camera updates
class CameraWithPerspective(Camera):
CONFIG = {
@ -51,7 +49,7 @@ class ThreeDCamera(CameraWithPerspective):
self.rotation_mobject = VectorizedPoint()
# moving_center lives in the x-y-z space
# It representes the center of rotation
self.moving_center = VectorizedPoint(self.frame_center)
self.moving_center = VectorizedPoint(self.space_center)
self.set_position(self.phi, self.theta, self.distance)
def modified_rgb(self, vmobject, rgb):
@ -165,7 +163,7 @@ class ThreeDCamera(CameraWithPerspective):
center_of_rotation = self.get_center_of_rotation(
center_x, center_y, center_z)
self.moving_center.move_to(center_of_rotation)
self.frame_center = self.moving_center.points[0]
self.space_center = self.moving_center.points[0]
def get_view_transformation_matrix(self):
return (self.default_distance / self.get_distance()) * np.dot(
@ -176,6 +174,6 @@ class ThreeDCamera(CameraWithPerspective):
def points_to_pixel_coords(self, points):
matrix = self.get_view_transformation_matrix()
new_points = np.dot(points, matrix.T)
self.frame_center = self.moving_center.points[0]
self.space_center = self.moving_center.points[0]
return Camera.points_to_pixel_coords(self, new_points)

View file

@ -17,20 +17,17 @@ LOW_QUALITY_FRAME_DURATION = 1. / 15
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60
# There might be other configuration than pixel shape later...
# There might be other configuration than pixel_shape later...
PRODUCTION_QUALITY_CAMERA_CONFIG = {
"pixel_height": DEFAULT_PIXEL_HEIGHT,
"pixel_width": DEFAULT_PIXEL_WIDTH,
"pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
}
MEDIUM_QUALITY_CAMERA_CONFIG = {
"pixel_height": 720,
"pixel_width": 1280,
"pixel_shape": (720, 1280),
}
LOW_QUALITY_CAMERA_CONFIG = {
"pixel_height": 480,
"pixel_width": 854,
"pixel_shape": (480, 854),
}
DEFAULT_POINT_DENSITY_2D = 25

View file

@ -224,8 +224,11 @@ def get_module_posix(file_name):
module_name = file_name.replace(".py", "")
last_module = imp.load_module(".", *imp.find_module("."))
for part in module_name.split(os.sep):
load_args = imp.find_module(part, last_module.__path__)
last_module = imp.load_module(part, *load_args)
try:
load_args = imp.find_module(part, last_module.__path__)
last_module = imp.load_module(part, *load_args)
except ImportError:
continue
return last_module

View file

@ -199,15 +199,6 @@ class Mobject(Container):
)
return self
def apply_function_to_position(self, function):
self.move_to(function(self.get_center()))
return self
def apply_function_to_submobject_positions(self, function):
for submob in self.submobjects:
submob.apply_function_to_position(function)
return self
def apply_matrix(self, matrix, **kwargs):
# Default to applying matrix about the origin, not mobjects center
if len(kwargs) == 0:

View file

@ -7,49 +7,23 @@ from PIL import Image
from constants import *
from mobject.mobject import Mobject
from mobject.shape_matchers import SurroundingRectangle
from utils.bezier import interpolate
from utils.color import color_to_int_rgb
from utils.config_ops import digest_config
from utils.images import get_full_raster_image_path
class AbstractImageMobject(Mobject):
class ImageMobject(Mobject):
"""
Automatically filters out black pixels
"""
CONFIG = {
"height": 2.0,
"pixel_array_dtype": "uint8",
}
def get_pixel_array(self):
raise Exception("Not implemented")
def set_color(self):
# Likely to be implemented in subclasses, but no obgligation
pass
def init_points(self):
# Corresponding corners of image are fixed to these 3 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):
CONFIG = {
"filter_color": "black",
"invert": False,
# "use_cache" : True,
"height": 2.0,
"image_mode": "RGBA",
"pixel_array_dtype": "uint8",
}
def __init__(self, filename_or_array, **kwargs):
@ -63,7 +37,7 @@ class ImageMobject(AbstractImageMobject):
self.change_to_rgba_array()
if self.invert:
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
AbstractImageMobject.__init__(self, **kwargs)
Mobject.__init__(self, **kwargs)
def change_to_rgba_array(self):
pa = self.pixel_array
@ -79,9 +53,6 @@ class ImageMobject(AbstractImageMobject):
pa = np.append(pa, alphas, axis=2)
self.pixel_array = pa
def get_pixel_array(self):
return self.pixel_array
def set_color(self, color, alpha=None, family=True):
rgb = color_to_int_rgb(color)
self.pixel_array[:, :, :3] = rgb
@ -92,6 +63,19 @@ class ImageMobject(AbstractImageMobject):
self.color = color
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):
self.pixel_array[:, :, 3] = int(255 * alpha)
return self
@ -106,30 +90,5 @@ class ImageMobject(AbstractImageMobject):
mobject1.pixel_array, mobject2.pixel_array, alpha
).astype(self.pixel_array_dtype)
# TODO, add the ability to have the dimensions/orientation of this
# mobject more strongly tied to the frame of the camera it contains,
# in the case where that's a MovingCamera
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
def copy(self):
return self.deepcopy()

View file

@ -668,7 +668,7 @@ class ColorMappedByFuncScene(Scene):
# We hash just based on output image
# Thus, multiple scenes with same output image can re-use it
# without recomputation
full_hash = hash((func_hash, self.camera.get_pixel_width()))
full_hash = hash((func_hash, self.camera.pixel_shape))
self.background_image_file = self.short_path_to_long_path(
"color_mapped_bg_hash_" + str(full_hash) + ".png"
)

View file

@ -2884,7 +2884,7 @@ class ZeroFoundOnBoundary(Scene):
class AllOfTheVideos(Scene):
CONFIG = {
"camera_config" : {
"background_opacity" : 1,
"background_alpha" : 255,
}
}
def construct(self):

View file

@ -2056,7 +2056,7 @@ class IPTScene1(PiCreatureScene):
# use the following for the zoomed inset
if show_detail:
self.camera.frame_shape = (0.02 * FRAME_HEIGHT, 0.02 * FRAME_WIDTH)
self.camera.frame_center = C
self.camera.space_center = C
SCREEN_SCALE = 0.01
SCREEN_THICKNESS = 0.02

View file

@ -1479,7 +1479,7 @@ class AskAboutLayers(PreviewMNistNetwork):
class BreakUpMacroPatterns(IntroduceEachLayer):
CONFIG = {
"camera_config" : {"background_opacity" : 1},
"camera_config" : {"background_alpha" : 255},
"prefixes" : [
"nine", "eight", "four",
"upper_loop", "right_line",
@ -1868,7 +1868,7 @@ class BreakUpMicroPatterns(BreakUpMacroPatterns):
class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer):
CONFIG = {
"camera_config" : {
"background_opacity" : 1,
"background_alpha" : 255,
},
"network_mob_config" : {
"layer_to_layer_buff" : 2,
@ -2081,7 +2081,7 @@ class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer):
class EdgeDetection(Scene):
CONFIG = {
"camera_config" : {"background_opacity" : 1}
"camera_config" : {"background_alpha" : 255}
}
def construct(self):
lion = ImageMobject("Lion")

View file

@ -468,7 +468,7 @@ class TauFalls(Scene):
class EulerWrites628(Scene):
CONFIG = {
"camera_config" : {
"background_opacity" : 1,
"background_alpha" : 255,
}
}
def construct(self):
@ -548,7 +548,7 @@ class EulerWrites628(Scene):
class HeroAndVillain(Scene):
CONFIG = {
"camera_config" : {
"background_opacity" : 1,
"background_alpha" : 255,
}
}
def construct(self):
@ -987,7 +987,7 @@ class SpecialThanks(Scene):
class EndScene(PatreonEndScreen):
CONFIG = {
"camera_config" : {
"background_opacity" : 1,
"background_alpha" : 255,
}
}
def construct(self):

View file

@ -4958,7 +4958,7 @@ class KeeperAndSailorForSineProduct(KeeperAndSailor):
class Conclusion(TeacherStudentsScene):
CONFIG = {
"camera_config": {"background_opacity": 1},
"camera_config": {"background_alpha": 255},
}
def construct(self):

View file

@ -1,32 +1,26 @@
from __future__ import absolute_import
from constants import *
from scene.scene import Scene
from camera.moving_camera import MovingCamera
from utils.iterables import list_update
from mobject.frame import ScreenRectangle
class MovingCameraScene(Scene):
CONFIG = {
"camera_class": MovingCamera
}
def setup(self):
Scene.setup(self)
assert(isinstance(self.camera, MovingCamera))
self.camera_frame = self.camera.frame
# Hmm, this currently relies on the fact that MovingCamera
# willd default to a full-sized frame. Is that okay?
self.camera_frame = ScreenRectangle(height=FRAME_HEIGHT)
self.camera_frame.set_stroke(width=0)
self.camera = MovingCamera(
self.camera_frame, **self.camera_config
)
return self
def get_moving_mobjects(self, *animations):
# TODO: Code repetition from ZoomedScene
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
all_moving_mobjects = self.camera.extract_mobject_family_members(
moving_mobjects
)
movement_indicators = self.camera.get_mobjects_indicating_movement()
for movement_indicator in movement_indicators:
if movement_indicator in all_moving_mobjects:
# When one of these is moving, the camera should
# consider all mobjects to be moving
return list_update(self.mobjects, moving_mobjects)
return moving_mobjects
if self.camera_frame in moving_mobjects:
# When the camera is moving, so is everything,
return self.mobjects
else:
return moving_mobjects

View file

@ -572,8 +572,7 @@ class Scene(Container):
self.args_to_rename_file = (temp_file_path, file_path)
fps = int(1 / self.frame_duration)
height = self.camera.get_pixel_height()
width = self.camera.get_pixel_width()
height, width = self.camera.pixel_shape
command = [
FFMPEG_BIN,

View file

@ -1,19 +1,20 @@
from __future__ import absolute_import
from scene.moving_camera_scene import MovingCameraScene
import numpy as np
from scene.scene import Scene
from animation.creation import FadeIn
from camera.moving_camera import MovingCamera
from camera.multi_camera import MultiCamera
from mobject.types.image_mobject import ImageMobjectFromCamera
from utils.simple_functions import fdiv
from mobject.geometry import Rectangle
from constants import *
from animation.transform import ApplyMethod
# Note, any scenes from old videos using ZoomedScene will almost certainly
# break, as it was restructured.
class ZoomedScene(MovingCameraScene):
class ZoomedScene(Scene):
"""
Move around self.little_rectangle to determine
which part of the screen is zoomed in on.
"""
CONFIG = {
"camera_class": MultiCamera,
"zoomed_display_height": 3,
@ -32,62 +33,109 @@ class ZoomedScene(MovingCameraScene):
"zoom_activated": False,
}
def setup(self):
MovingCameraScene.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)
def activate_zooming(self):
self.generate_big_rectangle()
self.setup_zoomed_canvas()
self.setup_zoomed_camera()
self.zoom_activated = True
# 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
def animate_activate_zooming(self):
self.activate_zooming()
self.play(*map(FadeIn, [
self.little_rectangle, self.big_rectangle
]))
def disactivate_zooming(self):
self.remove(self.big_rectangle, self.little_rectangle)
self.zoom_activated = False
def get_zoomed_camera_mobject(self):
return self.little_rectangle
def get_zoomed_screen(self):
return self.big_rectangle
def generate_big_rectangle(self):
height, width = self.zoomed_canvas_frame_shape
self.big_rectangle = Rectangle(
height=height,
width=width,
color=self.square_color
)
if self.zoomed_canvas_center is not None:
self.big_rectangle.shift(self.zoomed_canvas_center)
elif self.zoomed_canvas_corner is not None:
self.big_rectangle.to_corner(
self.zoomed_canvas_corner,
buff=self.zoomed_canvas_corner_buff
)
self.add(self.big_rectangle)
def setup_zoomed_canvas(self):
upper_left = self.big_rectangle.get_corner(UP + LEFT)
lower_right = self.big_rectangle.get_corner(DOWN + RIGHT)
pixel_coords = self.camera.points_to_pixel_coords(
np.array([upper_left, lower_right])
)
self.zoomed_canvas_pixel_indices = pixel_coords
(up, left), (down, right) = pixel_coords
self.zoomed_canvas_pixel_shape = (
right - left,
down - up,
)
def setup_zoomed_camera(self):
self.little_rectangle = self.big_rectangle.copy()
self.little_rectangle.scale(1. / self.zoom_factor)
self.little_rectangle.move_to(
self.little_rectangle_start_position
)
self.zoomed_camera = MovingCamera(
self.little_rectangle,
pixel_shape=self.zoomed_canvas_pixel_shape,
background=self.zoomed_camera_background
)
self.add(self.little_rectangle)
# TODO, is there a better way to hanld this?
self.zoomed_camera.adjusted_thickness = lambda x: x
def get_frame(self):
frame = Scene.get_frame(self)
if self.zoom_activated:
(up, left), (down, right) = self.zoomed_canvas_pixel_indices
frame[left:right, up:down, :] = self.zoomed_camera.get_image()
return frame
def set_camera_pixel_array(self, pixel_array):
self.camera.set_pixel_array(pixel_array)
if self.zoom_activated:
(up, left), (down, right) = self.zoomed_canvas_pixel_indices
self.zoomed_camera.set_pixel_array(
pixel_array[left:right, up:down])
def set_camera_background(self, background):
self.set_camera_pixel_array(self, background)
# TODO, check this...
def reset_camera(self):
self.camera.reset()
if self.zoom_activated:
self.zoomed_camera.reset()
def capture_mobjects_in_camera(self, mobjects, **kwargs):
self.camera.capture_mobjects(mobjects, **kwargs)
if self.zoom_activated:
if self.big_rectangle in mobjects:
mobjects = list(mobjects)
mobjects.remove(self.big_rectangle)
self.zoomed_camera.capture_mobjects(
mobjects, **kwargs
)
self.zoomed_camera = zoomed_camera
self.zoomed_display = zoomed_display
def activate_zooming(self, animate=False):
self.zoom_activated = True
self.camera.add_image_mobject_from_camera(self.zoomed_display)
if animate:
self.play(self.get_zoom_in_animation())
self.play(self.get_zoomed_display_pop_out_animation())
self.add_foreground_mobjects(
self.zoomed_camera.frame,
self.zoomed_display,
)
def get_zoom_in_animation(self, run_time=2, **kwargs):
frame = self.zoomed_camera.frame
full_frame_height = self.camera.get_frame_height()
full_frame_width = self.camera.get_frame_width()
frame.save_state()
frame.stretch_to_fit_width(full_frame_width)
frame.stretch_to_fit_height(full_frame_height)
frame.center()
frame.set_stroke(width=0)
return ApplyMethod(frame.restore, run_time=run_time, **kwargs)
def get_zoomed_display_pop_out_animation(self, **kwargs):
display = self.zoomed_display
display.save_state(use_deepcopy=True)
display.replace(self.zoomed_camera.frame, stretch=True)
return ApplyMethod(display.restore)
def get_zoom_factor(self):
return fdiv(
self.zoomed_camera.frame.get_height(),
self.zoomed_display.get_height()
)
def get_moving_mobjects(self, *animations):
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
if self.zoom_activated and self.little_rectangle in moving_mobjects:
# When the camera is moving, so is everything,
return self.mobjects
else:
return moving_mobjects

View file

@ -39,8 +39,7 @@ def color_to_int_rgb(color):
return (255 * color_to_rgb(color)).astype('uint8')
def color_to_int_rgba(color, opacity=1.0):
alpha = int(255 * opacity)
def color_to_int_rgba(color, alpha=255):
return np.append(color_to_int_rgb(color), alpha)

View file

@ -34,7 +34,7 @@ def get_scene_output_directory(scene_class):
def get_movie_output_directory(scene_class, camera_config, frame_duration):
directory = get_scene_output_directory(scene_class)
sub_dir = "%dp%d" % (
camera_config["pixel_height"],
camera_config["pixel_shape"][0],
int(1.0 / frame_duration)
)
return guarantee_existance(os.path.join(directory, sub_dir))