2016-02-23 22:29:32 -08:00
|
|
|
import numpy as np
|
|
|
|
import itertools as it
|
|
|
|
import os
|
2016-04-17 12:59:53 -07:00
|
|
|
|
2016-02-23 22:29:32 -08:00
|
|
|
from PIL import Image
|
|
|
|
from colour import Color
|
2016-04-09 20:03:57 -07:00
|
|
|
import aggdraw
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
from helpers import *
|
2017-09-18 17:15:49 -07:00
|
|
|
from mobject import PMobject, VMobject, ImageMobject
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
class Camera(object):
|
2016-02-27 16:32:53 -08:00
|
|
|
CONFIG = {
|
2017-06-20 14:05:48 -07:00
|
|
|
"background_image" : None,
|
2016-02-27 13:33:46 -08:00
|
|
|
"pixel_shape" : (DEFAULT_HEIGHT, DEFAULT_WIDTH),
|
|
|
|
#this will be resized to match pixel_shape
|
|
|
|
"space_shape" : (SPACE_HEIGHT, SPACE_WIDTH),
|
|
|
|
"space_center" : ORIGIN,
|
2016-02-23 22:29:32 -08:00
|
|
|
"background_color" : BLACK,
|
2016-12-03 19:06:50 -08:00
|
|
|
#Points in vectorized mobjects with norm greater
|
|
|
|
#than this value will be rescaled.
|
2017-09-05 10:21:36 -07:00
|
|
|
"max_allowable_norm" : 2*SPACE_WIDTH,
|
2017-09-19 13:12:45 -07:00
|
|
|
"image_mode" : "RGBA",
|
|
|
|
"n_rgb_coords" : 4,
|
2016-02-23 22:29:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, background = None, **kwargs):
|
|
|
|
digest_config(self, kwargs, locals())
|
|
|
|
self.init_background()
|
2016-02-27 13:33:46 -08:00
|
|
|
self.resize_space_shape()
|
2016-02-23 22:29:32 -08:00
|
|
|
self.reset()
|
|
|
|
|
2016-02-27 13:33:46 -08:00
|
|
|
def resize_space_shape(self, fixed_dimension = 0):
|
|
|
|
"""
|
|
|
|
Changes space_shape to match the aspect ratio
|
|
|
|
of pixel_shape, where fixed_dimension determines
|
|
|
|
whether space_shape[0] (height) or space_shape[1] (width)
|
|
|
|
remains fixed while the other changes accordingly.
|
|
|
|
"""
|
|
|
|
aspect_ratio = float(self.pixel_shape[1])/self.pixel_shape[0]
|
|
|
|
space_height, space_width = self.space_shape
|
|
|
|
if fixed_dimension == 0:
|
|
|
|
space_width = aspect_ratio*space_height
|
|
|
|
else:
|
|
|
|
space_height = space_width/aspect_ratio
|
|
|
|
self.space_shape = (space_height, space_width)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
def init_background(self):
|
2017-06-20 14:05:48 -07:00
|
|
|
if self.background_image is not None:
|
|
|
|
path = get_full_image_path(self.background_image)
|
2017-09-19 13:12:45 -07:00
|
|
|
image = Image.open(path).convert(self.image_mode)
|
2017-06-20 14:05:48 -07:00
|
|
|
height, width = self.pixel_shape
|
|
|
|
#TODO, how to gracefully handle backgrounds
|
|
|
|
#with different sizes?
|
|
|
|
self.background = np.array(image)[:height, :width]
|
2016-02-23 22:29:32 -08:00
|
|
|
else:
|
2017-09-19 13:12:45 -07:00
|
|
|
background_rgba = color_to_int_rgba(
|
|
|
|
self.background_color, alpha = 0
|
|
|
|
)
|
2016-02-27 13:33:46 -08:00
|
|
|
self.background = np.zeros(
|
2017-09-19 13:12:45 -07:00
|
|
|
list(self.pixel_shape)+[self.n_rgb_coords],
|
2016-02-23 22:29:32 -08:00
|
|
|
dtype = 'uint8'
|
|
|
|
)
|
2017-09-19 13:12:45 -07:00
|
|
|
self.background[:,:] = background_rgba
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
def get_image(self):
|
|
|
|
return np.array(self.pixel_array)
|
|
|
|
|
|
|
|
def set_image(self, pixel_array):
|
|
|
|
self.pixel_array = np.array(pixel_array)
|
|
|
|
|
2016-11-23 17:50:25 -08:00
|
|
|
def set_background(self, pixel_array):
|
|
|
|
self.background = np.array(pixel_array)
|
|
|
|
|
2016-02-23 22:29:32 -08:00
|
|
|
def reset(self):
|
|
|
|
self.set_image(np.array(self.background))
|
|
|
|
|
|
|
|
def capture_mobject(self, mobject):
|
|
|
|
return self.capture_mobjects([mobject])
|
|
|
|
|
2016-04-17 00:31:38 -07:00
|
|
|
def capture_mobjects(self, mobjects, include_submobjects = True):
|
|
|
|
if include_submobjects:
|
2016-02-27 18:50:33 -08:00
|
|
|
mobjects = it.chain(*[
|
2016-07-18 14:03:25 -07:00
|
|
|
mob.family_members_with_points()
|
2016-02-23 22:29:32 -08:00
|
|
|
for mob in mobjects
|
2016-02-27 18:50:33 -08:00
|
|
|
])
|
2016-04-17 12:59:53 -07:00
|
|
|
vmobjects = []
|
2016-02-23 22:29:32 -08:00
|
|
|
for mobject in mobjects:
|
2016-04-14 19:30:47 -07:00
|
|
|
if isinstance(mobject, VMobject):
|
2016-04-17 12:59:53 -07:00
|
|
|
vmobjects.append(mobject)
|
2016-04-14 19:30:47 -07:00
|
|
|
elif isinstance(mobject, PMobject):
|
2016-12-02 13:12:58 -08:00
|
|
|
self.display_multiple_vectorized_mobjects(vmobjects)
|
|
|
|
vmobjects = []
|
2016-04-09 20:03:57 -07:00
|
|
|
self.display_point_cloud(
|
2017-09-19 13:12:45 -07:00
|
|
|
mobject.points, mobject.rgbas,
|
2016-04-10 12:34:28 -07:00
|
|
|
self.adjusted_thickness(mobject.stroke_width)
|
2016-02-27 12:44:52 -08:00
|
|
|
)
|
2017-09-18 17:15:49 -07:00
|
|
|
elif isinstance(mobject, ImageMobject):
|
|
|
|
self.display_image_mobject(mobject)
|
|
|
|
else:
|
|
|
|
raise Exception("Unknown mobject type: " + type(mobject))
|
2016-04-13 20:30:26 -07:00
|
|
|
#TODO, more? Call out if it's unknown?
|
2016-12-02 13:12:58 -08:00
|
|
|
self.display_multiple_vectorized_mobjects(vmobjects)
|
2016-09-06 16:48:04 -07:00
|
|
|
|
2016-12-02 13:12:58 -08:00
|
|
|
def display_multiple_vectorized_mobjects(self, vmobjects):
|
|
|
|
if len(vmobjects) == 0:
|
|
|
|
return
|
|
|
|
#More efficient to bundle together in one "canvas"
|
2017-09-19 13:12:45 -07:00
|
|
|
image = Image.fromarray(self.pixel_array, mode = self.image_mode)
|
2016-04-17 12:59:53 -07:00
|
|
|
canvas = aggdraw.Draw(image)
|
|
|
|
for vmobject in vmobjects:
|
|
|
|
self.display_vectorized(vmobject, canvas)
|
2016-09-06 16:48:04 -07:00
|
|
|
canvas.flush()
|
2017-09-19 13:12:45 -07:00
|
|
|
|
2017-05-31 12:19:57 -07:00
|
|
|
self.pixel_array[:,:] = image
|
2016-04-09 20:03:57 -07:00
|
|
|
|
2016-04-17 12:59:53 -07:00
|
|
|
def display_vectorized(self, vmobject, canvas):
|
2016-04-14 19:30:47 -07:00
|
|
|
if vmobject.is_subpath:
|
2016-04-12 21:57:53 -07:00
|
|
|
#Subpath vectorized mobjects are taken care
|
|
|
|
#of by their parent
|
|
|
|
return
|
2016-04-14 19:30:47 -07:00
|
|
|
pen, fill = self.get_pen_and_fill(vmobject)
|
|
|
|
pathstring = self.get_pathstring(vmobject)
|
2016-04-10 12:34:28 -07:00
|
|
|
symbol = aggdraw.Symbol(pathstring)
|
|
|
|
canvas.symbol((0, 0), symbol, pen, fill)
|
2016-04-17 12:59:53 -07:00
|
|
|
|
2016-04-14 19:30:47 -07:00
|
|
|
def get_pen_and_fill(self, vmobject):
|
2016-04-09 20:03:57 -07:00
|
|
|
pen = aggdraw.Pen(
|
2017-02-02 15:36:24 -08:00
|
|
|
self.get_stroke_color(vmobject).get_hex_l(),
|
2016-04-23 23:36:05 -07:00
|
|
|
max(vmobject.stroke_width, 0)
|
2016-04-09 20:03:57 -07:00
|
|
|
)
|
|
|
|
fill = aggdraw.Brush(
|
2017-02-02 15:36:24 -08:00
|
|
|
self.get_fill_color(vmobject).get_hex_l(),
|
2016-04-14 19:30:47 -07:00
|
|
|
opacity = int(255*vmobject.get_fill_opacity())
|
2016-04-09 20:03:57 -07:00
|
|
|
)
|
|
|
|
return (pen, fill)
|
|
|
|
|
2017-02-02 15:36:24 -08:00
|
|
|
def get_stroke_color(self, vmobject):
|
|
|
|
return vmobject.get_stroke_color()
|
|
|
|
|
|
|
|
def get_fill_color(self, vmobject):
|
|
|
|
return vmobject.get_fill_color()
|
|
|
|
|
2016-04-14 19:30:47 -07:00
|
|
|
def get_pathstring(self, vmobject):
|
2016-04-12 21:57:53 -07:00
|
|
|
result = ""
|
2016-04-17 12:59:53 -07:00
|
|
|
for mob in [vmobject]+vmobject.get_subpath_mobjects():
|
2016-04-12 21:57:53 -07:00
|
|
|
points = mob.points
|
2016-12-06 13:29:21 -08:00
|
|
|
# points = self.adjust_out_of_range_points(points)
|
2016-04-13 20:30:26 -07:00
|
|
|
if len(points) == 0:
|
|
|
|
continue
|
2016-11-11 11:18:41 -08:00
|
|
|
points = self.align_points_to_camera(points)
|
2016-04-12 21:57:53 -07:00
|
|
|
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
|
|
|
|
]
|
2016-04-17 00:31:38 -07:00
|
|
|
end = "Z" if vmobject.mark_paths_closed else ""
|
|
|
|
result += " ".join([start] + cubics + [end])
|
2016-04-12 21:57:53 -07:00
|
|
|
return result
|
2016-04-09 20:03:57 -07:00
|
|
|
|
2017-09-19 13:12:45 -07:00
|
|
|
def display_point_cloud(self, points, rgbas, thickness):
|
2016-02-23 22:29:32 -08:00
|
|
|
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
|
|
|
|
)
|
2017-09-19 13:12:45 -07:00
|
|
|
rgb_len = self.pixel_array.shape[2]
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2017-09-19 13:12:45 -07:00
|
|
|
rgbas = (255*rgbas).astype('uint8')
|
2016-02-23 22:29:32 -08:00
|
|
|
target_len = len(pixel_coords)
|
2017-09-19 13:12:45 -07:00
|
|
|
factor = target_len/len(rgbas)
|
|
|
|
rgbas = np.array([rgbas]*factor).reshape((target_len, rgb_len))
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
on_screen_indices = self.on_screen_pixels(pixel_coords)
|
|
|
|
pixel_coords = pixel_coords[on_screen_indices]
|
2017-09-19 13:12:45 -07:00
|
|
|
rgbas = rgbas[on_screen_indices]
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2016-02-27 13:33:46 -08:00
|
|
|
ph, pw = self.pixel_shape
|
|
|
|
|
|
|
|
flattener = np.array([1, pw], dtype = 'int')
|
2016-02-23 22:29:32 -08:00
|
|
|
flattener = flattener.reshape((2, 1))
|
|
|
|
indices = np.dot(pixel_coords, flattener)[:,0]
|
|
|
|
indices = indices.astype('int')
|
2016-02-27 13:33:46 -08:00
|
|
|
|
2017-09-19 13:12:45 -07:00
|
|
|
new_pa = self.pixel_array.reshape((ph*pw, rgb_len))
|
|
|
|
new_pa[indices] = rgbas
|
|
|
|
self.pixel_array = new_pa.reshape((ph, pw, rgb_len))
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2017-09-18 17:15:49 -07:00
|
|
|
def display_image_mobject(self, image_mobject):
|
|
|
|
corner_coords = self.points_to_pixel_coords(image_mobject.points)
|
|
|
|
ul_coords, ur_coords, dl_coords = corner_coords
|
|
|
|
right_vect = ur_coords - ul_coords
|
|
|
|
down_vect = dl_coords - ul_coords
|
|
|
|
|
|
|
|
impa = image_mobject.pixel_array
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
recentered_coords = all_pixel_coords - ul_coords
|
|
|
|
coord_norms = np.linalg.norm(recentered_coords, axis = 1)
|
|
|
|
|
2017-09-19 13:12:45 -07:00
|
|
|
with np.errstate(divide = 'ignore'):
|
2017-09-18 17:15:49 -07:00
|
|
|
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,
|
|
|
|
])
|
|
|
|
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))
|
2017-09-19 13:12:45 -07:00
|
|
|
target_rgbas = flat_impa[inner_flat_coords, :]
|
|
|
|
|
|
|
|
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')
|
2017-09-18 17:15:49 -07:00
|
|
|
|
2016-02-23 22:29:32 -08:00
|
|
|
def align_points_to_camera(self, points):
|
|
|
|
## This is where projection should live
|
|
|
|
return points - self.space_center
|
|
|
|
|
2016-12-03 19:06:50 -08:00
|
|
|
def adjust_out_of_range_points(self, points):
|
|
|
|
if not np.any(points > self.max_allowable_norm):
|
|
|
|
return points
|
|
|
|
norms = np.apply_along_axis(np.linalg.norm, 1, points)
|
|
|
|
violator_indices = norms > self.max_allowable_norm
|
|
|
|
violators = points[violator_indices,:]
|
|
|
|
violator_norms = norms[violator_indices]
|
|
|
|
reshaped_norms = np.repeat(
|
|
|
|
violator_norms.reshape((len(violator_norms), 1)),
|
|
|
|
points.shape[1], 1
|
|
|
|
)
|
|
|
|
rescaled = self.max_allowable_norm * violators / reshaped_norms
|
|
|
|
points[violator_indices] = rescaled
|
|
|
|
return points
|
|
|
|
|
2016-02-23 22:29:32 -08:00
|
|
|
def points_to_pixel_coords(self, points):
|
|
|
|
result = np.zeros((len(points), 2))
|
2016-02-27 13:33:46 -08:00
|
|
|
ph, pw = self.pixel_shape
|
|
|
|
sh, sw = self.space_shape
|
|
|
|
width_mult = pw/sw/2
|
|
|
|
width_add = pw/2
|
|
|
|
height_mult = ph/sh/2
|
|
|
|
height_add = ph/2
|
2016-02-23 22:29:32 -08:00
|
|
|
#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.astype('int')
|
|
|
|
|
|
|
|
def on_screen_pixels(self, pixel_coords):
|
|
|
|
return reduce(op.and_, [
|
|
|
|
pixel_coords[:,0] >= 0,
|
2016-02-27 13:33:46 -08:00
|
|
|
pixel_coords[:,0] < self.pixel_shape[1],
|
2016-02-23 22:29:32 -08:00
|
|
|
pixel_coords[:,1] >= 0,
|
2016-02-27 13:33:46 -08:00
|
|
|
pixel_coords[:,1] < self.pixel_shape[0],
|
2016-02-23 22:29:32 -08:00
|
|
|
])
|
|
|
|
|
|
|
|
def adjusted_thickness(self, thickness):
|
2016-03-07 19:07:00 -08:00
|
|
|
big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"]
|
|
|
|
factor = sum(big_shape)/sum(self.pixel_shape)
|
|
|
|
return 1 + (thickness-1)/factor
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
def get_thickening_nudges(self, thickness):
|
|
|
|
_range = range(-thickness/2+1, thickness/2+1)
|
2016-03-15 20:03:23 -07:00
|
|
|
return np.array(
|
|
|
|
list(it.product([0], _range))+
|
|
|
|
list(it.product(_range, [0]))
|
|
|
|
)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
def thickened_coordinates(self, pixel_coords, thickness):
|
|
|
|
nudges = self.get_thickening_nudges(thickness)
|
|
|
|
pixel_coords = np.array([
|
|
|
|
pixel_coords + nudge
|
|
|
|
for nudge in nudges
|
|
|
|
])
|
|
|
|
size = pixel_coords.size
|
|
|
|
return pixel_coords.reshape((size/2, 2))
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-02-27 12:44:52 -08:00
|
|
|
class MovingCamera(Camera):
|
|
|
|
"""
|
|
|
|
Stays in line with the height, width and position
|
|
|
|
of a given mobject
|
|
|
|
"""
|
2016-02-27 16:32:53 -08:00
|
|
|
CONFIG = {
|
2016-02-27 16:29:11 -08:00
|
|
|
"aligned_dimension" : "width" #or height
|
2016-02-27 13:33:46 -08:00
|
|
|
}
|
2016-02-27 12:44:52 -08:00
|
|
|
def __init__(self, mobject, **kwargs):
|
|
|
|
digest_locals(self)
|
|
|
|
Camera.__init__(self, **kwargs)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2016-02-27 12:44:52 -08:00
|
|
|
def capture_mobjects(self, *args, **kwargs):
|
|
|
|
self.space_center = self.mobject.get_center()
|
2016-02-27 16:29:11 -08:00
|
|
|
self.realign_space_shape()
|
|
|
|
Camera.capture_mobjects(self, *args, **kwargs)
|
|
|
|
|
|
|
|
def realign_space_shape(self):
|
|
|
|
height, width = self.space_shape
|
|
|
|
if self.aligned_dimension == "height":
|
|
|
|
self.space_shape = (self.mobject.get_height()/2, width)
|
|
|
|
else:
|
|
|
|
self.space_shape = (height, self.mobject.get_width()/2)
|
|
|
|
self.resize_space_shape(
|
|
|
|
0 if self.aligned_dimension == "height" else 1
|
|
|
|
)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|