tex mobjects implemented

This commit is contained in:
Grant Sanderson
2015-03-26 22:49:22 -06:00
parent 5750a40bc6
commit 3ae36a3ac9
7 changed files with 291 additions and 92 deletions

20
Tex/template.tex Normal file
View File

@ -0,0 +1,20 @@
\documentclass[12pt]{beamer}
\usepackage[english]{babel}
\usepackage{mathtools}
\mode<presentation>
{
\usetheme{default} % or try Darmstadt, Madrid, Warsaw, ...
\usecolortheme{default} % or try albatross, beaver, crane, ...
\usefonttheme[onlymath]{serif} % or try serif, structurebold, ...
\setbeamertemplate{navigation symbols}{}
\setbeamertemplate{caption}[numbered]
}
\begin{document}
\begin{frame}
\centering
SizeHere
$YourTextHere$
\end{frame}
\end{document}

View File

@ -16,19 +16,19 @@ import displayer as disp
class Animation(object):
def __init__(self,
mobject,
mobject_or_animation,
run_time = DEFAULT_ANIMATION_RUN_TIME,
alpha_func = high_inflection_0_to_1,
name = None):
if isinstance(mobject, type) and issubclass(mobject, Mobject):
self.mobject = mobject()
self.starting_mobject = mobject()
elif isinstance(mobject, Mobject):
self.mobject = mobject
self.starting_mobject = copy.deepcopy(mobject)
self.embedded_animation = None
if isinstance(mobject_or_animation, type) and issubclass(mobject_or_animation, Mobject):
self.mobject = mobject_or_animation()
elif isinstance(mobject_or_animation, Mobject):
self.mobject = mobject_or_animation
else:
raise Exception("Invalid mobject parameter, must be \
raise Exception("Invalid mobject_or_animation parameter, must be \
subclass or instance of Mobject")
self.starting_mobject = copy.deepcopy(self.mobject)
self.reference_mobjects = [self.starting_mobject]
self.alpha_func = alpha_func or (lambda x : x)
self.run_time = run_time

View File

@ -1,12 +1,12 @@
import os
PRODUCTION_QUALITY = True
PRODUCTION_QUALITY = False
DEFAULT_POINT_DENSITY_2D = 25 if PRODUCTION_QUALITY else 20
DEFAULT_POINT_DENSITY_1D = 200 if PRODUCTION_QUALITY else 50
DEFAULT_POINT_DENSITY_2D = 25 #if PRODUCTION_QUALITY else 20
DEFAULT_POINT_DENSITY_1D = 200 #if PRODUCTION_QUALITY else 50
HEIGHT = 1024#1440 if PRODUCTION_QUALITY else 480
WIDTH = 1024#2560 if PRODUCTION_QUALITY else 640
DEFAULT_HEIGHT = 1440 #if PRODUCTION_QUALITY else 480
DEFAULT_WIDTH = 2560 #if PRODUCTION_QUALITY else 640
#All in seconds
DEFAULT_FRAME_DURATION = 0.04 if PRODUCTION_QUALITY else 0.1
DEFAULT_ANIMATION_RUN_TIME = 3.0
@ -15,22 +15,28 @@ DEFAULT_DITHER_TIME = 1.0
GENERALLY_BUFF_POINTS = PRODUCTION_QUALITY
BACKGROUND_COLOR = "black" #TODO, this is never actually enforced anywhere.
DEFAULT_NUM_STARS = 1000
SPACE_HEIGHT = 4.0
SPACE_WIDTH = WIDTH * SPACE_HEIGHT / HEIGHT
SPACE_WIDTH = DEFAULT_WIDTH * SPACE_HEIGHT / DEFAULT_HEIGHT
PDF_DENSITY = 400
IMAGE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images")
GIF_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_gifs")
MOVIE_DIR = os.path.join(os.getenv("HOME"), "Desktop", "math_movies")
PDF_DIR = os.path.join(os.getenv("HOME"), "Documents", "Tex", "Animations")
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
IMAGE_DIR = os.path.join(THIS_DIR, "images")
GIF_DIR = os.path.join(THIS_DIR, "gifs")
MOVIE_DIR = os.path.join(THIS_DIR, "movies")
TEX_DIR = os.path.join(THIS_DIR, "Tex")
TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex")
TMP_IMAGE_DIR = "/tmp/animation_images/"
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TMP_IMAGE_DIR]:
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, TEX_IMAGE_DIR]:
if not os.path.exists(folder):
os.mkdir(folder)
LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png")
PDF_DENSITY = 400
SIZE_TO_REPLACE = "SizeHere"
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(TEX_DIR, "template.tex")
LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png")

View File

@ -2,39 +2,47 @@ import numpy as np
import itertools as it
import os
from PIL import Image
import subprocess
import cv2
from animate import *
def paint_mobject(mobject, image_array = None):
if image_array is None:
pixels = np.zeros((DEFAULT_HEIGHT, DEFAULT_WIDTH, 3))
else:
pixels = np.array(image_array)
assert len(pixels.shape) == 3 and pixels.shape[2] == 3
height = pixels.shape[0]
width = pixels.shape[1]
space_height = SPACE_HEIGHT
space_width = SPACE_HEIGHT * width / height
def get_image(points, rgbs):
return Image.fromarray(get_pixels(points, rgbs))
def get_pixels(points, rgbs):
#TODO, Let z add a depth componenet?
points = points[:, :2]
points = np.array(mobject.points[:, :2])
#Flips y-axis
points[:,1] *= -1
#Map points to pixel space, then create pixel array first in terms
#of its flattened version
points += np.array(
[SPACE_WIDTH, SPACE_HEIGHT]*points.shape[0]
[space_width, space_height]*points.shape[0]
).reshape(points.shape)
points *= np.array(
[HEIGHT / (2.0 * SPACE_HEIGHT), WIDTH / (2.0 * SPACE_WIDTH)]*\
[width / (2.0 * space_width), height / (2.0 * space_height)]*\
points.shape[0]
).reshape(points.shape)
points = points.astype('int')
flattener = np.array([1, WIDTH], dtype = 'int').reshape((2, 1))
flattener = np.array([1, width], dtype = 'int').reshape((2, 1))
indices = np.dot(points, flattener)
indices = indices.reshape(indices.size)
admissibles = (indices < HEIGHT * WIDTH) * (indices > 0)
admissibles = (indices < height * width) * (indices > 0)
indices = indices[admissibles]
rgbs = rgbs[admissibles]
rgbs = mobject.rgbs[admissibles]
rgbs = (rgbs * 255).astype(int)
pixels = np.zeros((HEIGHT * WIDTH, 3))
pixels = pixels.reshape((height * width, 3))
pixels[indices] = rgbs
return pixels.reshape((HEIGHT, WIDTH, 3)).astype('uint8')
pixels = pixels.reshape((height, width, 3)).astype('uint8')
return pixels
def write_to_gif(scene, name):
#TODO, find better means of compression
@ -43,29 +51,31 @@ def write_to_gif(scene, name):
filepath = os.path.join(GIF_DIR, name)
temppath = os.path.join(GIF_DIR, "Temp.gif")
print "Writing " + name + "..."
writeGif(temppath, scene.frames, scene.frame_duration)
images = [Image.fromarray(frame) for frame in scene.frames]
writeGif(temppath, images, scene.frame_duration)
print "Compressing..."
os.system("gifsicle -O " + temppath + " > " + filepath)
os.system("rm " + temppath)
def write_to_movie(scene, name):
#TODO, incorporate pause time
frames = scene.frames
progress_bar = progressbar.ProgressBar(maxval=len(frames))
progress_bar.start()
print "writing " + name + "..."
filepath = os.path.join(MOVIE_DIR, name)
filedir = "/".join(filepath.split("/")[:-1])
if not os.path.exists(filedir):
os.makedirs(filedir)
rate = int(1/scene.frame_duration)
tmp_stem = os.path.join(TMP_IMAGE_DIR, name.replace("/", "_"))
suffix = "-%04d.png"
image_files = []
for frame, count in zip(frames, it.count()):
progress_bar.update(int(0.9 * count))
frame.save(tmp_stem + suffix%count)
Image.fromarray(frame).save(tmp_stem + suffix%count)
image_files.append(tmp_stem + suffix%count)
filepath = os.path.join(MOVIE_DIR, name + ".mp4")
filedir = "/".join(filepath.split("/")[:-1])
if not os.path.exists(filedir):
os.makedirs(filedir)
commands = [
"ffmpeg",
"-y",
@ -77,7 +87,7 @@ def write_to_movie(scene, name):
"libx264",
"-vf",
"fps=%d,format=yuv420p"%int(1/scene.frame_duration),
filepath
filepath + ".mp4"
]
os.system(" ".join(commands))
for image_file in image_files:
@ -85,10 +95,17 @@ def write_to_movie(scene, name):
progress_bar.finish()
# vs = VideoSink(scene.shape, filepath, rate)
# for frame in frames:
# vs.run(frame)
# vs.close()
# progress_bar.finish()
# filepath = os.path.join(MOVIE_DIR, name + ".mov")
# fourcc = cv2.cv.FOURCC(*"8bps")
# out = cv2.VideoWriter(
# filepath, fourcc, 1.0/animation.frame_duration, (WIDTH, HEIGHT), True
# filepath, fourcc, 1.0/scene.frame_duration, (DEFAULT_WIDTH, DEFAULT_HEIGHT), True
# )
# progress = 0
# for frame in frames:
@ -103,7 +120,35 @@ def write_to_movie(scene, name):
# progress_bar.finish()
class VideoSink(object):
def __init__(self, size, filename="output", rate=10, byteorder="bgra") :
self.size = size
cmdstring = [
'mencoder',
'/dev/stdin',
'-demuxer', 'rawvideo',
'-rawvideo', 'w=%i:h=%i'%size[::-1]+":fps=%i:format=%s"%(rate,byteorder),
'-o', filename+'.mp4',
'-ovc', 'lavc',
]
self.p = subprocess.Popen(cmdstring, stdin=subprocess.PIPE, shell=False)
def run(self, image):
"""
Image comes in as HEIGHTxWIDTHx3 numpy array, order rgb
"""
assert image.shape == self.size + (3,)
r, g, b = [image[:,:,i].astype('uint32') for i in range(3)]
a = np.ones(image.shape[:2], dtype = 'uint32')
#hacky
image = sum([
arr << 8**i
for arr, i in zip(range(4), [a, r, g, b])
])
self.p.stdin.write(image.tostring())
def close(self):
self.p.stdin.close()

View File

@ -5,9 +5,10 @@ from PIL import Image
from random import random
from animate import *
from tex_image_utils import NAME_TO_IMAGE_FILE
from tex_utils import *
import displayer as disp
class Mobject(object):
"""
Mathematical Object
@ -40,10 +41,10 @@ class Mobject(object):
return self.name
def display(self):
disp.get_image(self.points, self.rgbs).show()
Image.fromarray(disp.paint_mobject(self)).show()
def save_image(self, name = None):
disp.get_image(self.points, self.rgbs).save(
Image.fromarray(disp.paint_mobject(self)).save(
os.path.join(MOVIE_DIR, (name or str(self)) + ".png")
)
@ -286,7 +287,7 @@ class Line(Mobject1D):
])
class Cube(Mobject2D):
class CubeWithFaces(Mobject2D):
def generate_points(self):
self.add_points([
sgn * np.array(coords)
@ -300,7 +301,7 @@ class Cube(Mobject2D):
def unit_normal(self, coords):
return np.array(map(lambda x : 1 if abs(x) == 1 else 0, coords))
class CubeShell(Mobject1D):
class Cube(Mobject1D):
DEFAULT_COLOR = "yellow"
def generate_points(self):
self.add_points([
@ -449,24 +450,15 @@ class NumberLine(Mobject1D):
for x in np.arange(-self.radius, self.radius, self.interval_size)
for y in np.arange(-self.tick_size, self.tick_size, self.epsilon)
])
if self.with_numbers: #TODO, make these numbers a separate object
if self.with_numbers:
#TODO, test
vertical_displacement = -0.3
max_explicit_num = 3
num_to_name = dict(
(x, str(x))
for x in range(-max_explicit_num, max_explicit_num + 1)
)
num_to_name[max_explicit_num + 1] = "cdots"
num_to_name[-max_explicit_num - 1] = "cdots"
nums = CompoundMobject(*[
ImageMobject(
NAME_TO_IMAGE_FILE[num_to_name[x]]
).scale(0.6).center().shift(
[x * self.interval_size, vertical_displacement, 0]
)
for x in range(-max_explicit_num - 1, max_explicit_num + 2)
])
self.add_points(nums.points, nums.rgbs)
nums = range(-self.radius, self.radius)
nums = map(lambda x : x / self.interval_size, nums)
mobs = tex_mobjects(*[str(num) for num in nums])
for num, mob in zip(nums, mobs):
mob.center().shift([num, vertical_displacement, 0])
self.add(*mobs)
# class ComplexPlane(Grid):
# def __init__(self, *args, **kwargs):
@ -529,15 +521,20 @@ class ImageMobject(Mobject2D):
for i in indices
], dtype = 'float64')
height, width = map(float, (height, width))
if height / width > float(HEIGHT) / WIDTH:
if height / width > float(DEFAULT_HEIGHT) / DEFAULT_WIDTH:
points *= 2 * SPACE_HEIGHT / height
else:
points *= 2 * SPACE_WIDTH / width
self.add_points(points, rgbs = rgbs)
def tex_mobjects(expression, size = "\HUGE"):
images = tex_to_image(expression, size)
if isinstance(images, list):
#TODO, is checking listiness really the best here?
return [ImageMobject(im) for im in images]
else:
return ImageMobject(images)

View File

@ -1,6 +1,7 @@
from PIL import Image
from colour import Color
import numpy as np
import itertools as it
import warnings
import time
import os
@ -16,29 +17,47 @@ import displayer as disp
class Scene(object):
def __init__(self,
name = None,
frame_duration = DEFAULT_FRAME_DURATION,
name = None):
background = None,
height = DEFAULT_HEIGHT,
width = DEFAULT_WIDTH,):
self.frame_duration = frame_duration
self.frames = []
self.mobjects = set([])
self.mobjects = []
if background:
self.background = np.array(background)
#TODO, Error checking?
else:
self.background = np.zeros((height, width, 3))
self.shape = self.background.shape[:2]
self.name = name
def __str__(self):
return self.name or "Babadinook" #TODO
def add(self, *mobjects):
#TODO, perhaps mobjects should be ordered, for foreground/background
self.mobjects.update(mobjects)
"""
Mobjects will be displayed, from background to foreground,
in the order with which they are entered.
"""
for mobject in mobjects:
#In case it's already in there, it should
#now be closer to the foreground.
self.remove(mobject)
self.mobjects.append(mobject)
def remove(self, *mobjects):
self.mobjects.difference_update(mobjects)
for mobject in mobjects:
while mobject in self.mobjects:
self.mobjects.remove(mobject)
def animate(self, animations,
dither_time = DEFAULT_DITHER_TIME):
if isinstance(animations, Animation):
animations = [animations]
self.pause(dither_time)
run_time = max([anim.run_time for anim in animations])
def animate(self, *animations):
#Runtime is determined by the first animation
run_time = animations[0].run_time
moving_mobjects = [a.mobject for a in animations]
self.remove(*moving_mobjects)
background = self.get_frame()
print "Generating animations..."
progress_bar = progressbar.ProgressBar(maxval=run_time)
@ -46,28 +65,31 @@ class Scene(object):
for t in np.arange(0, run_time, self.frame_duration):
progress_bar.update(t)
new_frame = background
for anim in animations:
anim.update(t)
self.frames.append(self.get_frame(*animations))
anim.update(t / anim.run_time)
new_frame = disp.paint_mobject(anim.mobject, new_frame)
self.frames.append(new_frame)
for anim in animations:
anim.clean_up()
self.add(*moving_mobjects)
progress_bar.finish()
def pause(self, duration):
def get_frame(self):
frame = self.background
for mob in self.mobjects:
frame = disp.paint_mobject(mob, frame)
return frame
def dither(self, duration = DEFAULT_DITHER_TIME):
self.frames += [self.get_frame()]*int(duration / self.frame_duration)
def get_frame(self, *animations):
#Include animations so as to display mobjects not in the list
#TODO, This is temporary
mob = list(self.mobjects)[0]
return disp.get_image(mob.points, mob.rgbs)
def write_to_gif(self, name = None, end_dither_time = DEFAULT_DITHER_TIME):
self.pause(end_dither_time)
self.dither(end_dither_time)
disp.write_to_gif(self, name or str(self))
def write_to_movie(self, name = None, end_dither_time = DEFAULT_DITHER_TIME):
self.pause(end_dither_time)
self.dither(end_dither_time)
disp.write_to_movie(self, name or str(self))

109
tex_utils.py Normal file
View File

@ -0,0 +1,109 @@
import os
import itertools as it
from PIL import Image
from constants import *
def tex_to_image(expression, size = "\HUGE"):
"""
Returns list of images for correpsonding with a list of expressions
"""
return_list = False
arg = expression
if not isinstance(expression, str):
#TODO, verify that expression is iterable of strings
expression = "\n".join([
"\onslide<%d>{"%count + exp + "}"
for count, exp in zip(it.count(1), expression)
])
return_list = True
filestem = os.path.join(
TEX_DIR, str(hash(expression + size))
)
if not os.path.exists(filestem + ".dvi"):
if not os.path.exists(filestem + ".tex"):
print " ".join([
"Writing ",
"".join(arg),
"at size %s to "%(size),
filestem,
])
with open(TEMPLATE_TEX_FILE, "r") as infile:
body = infile.read()
body = body.replace(SIZE_TO_REPLACE, size)
body = body.replace(TEX_TEXT_TO_REPLACE, expression)
with open(filestem + ".tex", "w") as outfile:
outfile.write(body)
commands = [
"latex",
"-interaction=batchmode",
"-output-directory=" + TEX_DIR,
filestem + ".tex"
]
#TODO, Error check
os.system(" ".join(commands))
assert os.path.exists(filestem + ".dvi")
result = dvi_to_png(filestem + ".dvi")
return result if return_list else result[0]
def dvi_to_png(filename, regen_if_exists = False):
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
possible_paths = [
filename,
os.path.join(TEX_DIR, filename),
os.path.join(TEX_DIR, filename + ".dvi"),
]
for path in possible_paths:
if os.path.exists(path):
directory, filename = os.path.split(path)
name = filename.split(".")[0]
images_dir = os.path.join(TEX_IMAGE_DIR, name)
if not os.path.exists(images_dir):
os.mkdir(images_dir)
if os.listdir(images_dir) == [] or regen_if_exists:
commands = [
"convert",
"-density",
str(PDF_DENSITY),
path,
"-size",
str(DEFAULT_WIDTH) + "x" + str(DEFAULT_HEIGHT),
os.path.join(images_dir, name + ".png")
]
os.system(" ".join(commands))
image_paths = [
os.path.join(images_dir, name)
for name in os.listdir(images_dir)
if name.endswith(".png")
]
image_paths.sort(cmp_enumerated_files)
return [Image.open(path).convert('RGB') for path in image_paths]
raise IOError("File not Found")
def cmp_enumerated_files(name1, name2):
num1, num2 = [
int(name.split(".")[0].split("-")[-1])
for name in (name1, name2)
]
return num1 - num2