Finally have faster video writing capabilities

This commit is contained in:
Grant Sanderson
2015-10-11 15:22:47 -07:00
parent 8a2f411a1a
commit 7564beb393
7 changed files with 90 additions and 120 deletions

View File

@ -59,7 +59,6 @@ TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex") TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex")
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
TMP_IMAGE_DIR = "/tmp/animation_images/"
for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR, for folder in [IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, TMP_IMAGE_DIR,
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]: TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]:

View File

@ -1,8 +1,9 @@
import numpy as np import numpy as np
import itertools as it import itertools as it
import subprocess as sp
import os import os
import sys
from PIL import Image from PIL import Image
import subprocess
import cv2 import cv2
from colour import Color from colour import Color
import progressbar import progressbar
@ -10,6 +11,8 @@ import progressbar
from mobject import * from mobject import *
from constants import * from constants import *
FFMPEG_BIN = "ffmpeg"
def get_pixels(image_array): #TODO, FIX WIDTH/HEIGHT PROBLEM HERE def get_pixels(image_array): #TODO, FIX WIDTH/HEIGHT PROBLEM HERE
if image_array is None: if image_array is None:
return np.zeros( return np.zeros(
@ -126,97 +129,34 @@ def write_to_gif(scene, name):
os.system("rm " + temppath) os.system("rm " + temppath)
def write_to_movie(scene, name): def write_to_movie(scene, name):
frames = scene.frames filepath = os.path.join(MOVIE_DIR, name) + ".mp4"
progress_bar = progressbar.ProgressBar(maxval=len(frames)) print "Writing to %s"%filepath
progress_bar.start()
print "writing " + name + "..."
filepath = os.path.join(MOVIE_DIR, name) fps = int(1/scene.display_config["frame_duration"])
filedir = "/".join(filepath.split("/")[:-1]) dim = (scene.display_config["width"], scene.display_config["height"])
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("/", "_")) command = [
suffix = "-%04d.png" FFMPEG_BIN,
image_files = [] '-y', # overwrite output file if it exists
for frame, count in zip(frames, it.count()): '-f', 'rawvideo',
progress_bar.update(int(0.9 * count)) '-vcodec','rawvideo',
Image.fromarray(frame).save(tmp_stem + suffix%count) '-s', '%dx%d'%dim, # size of one frame
image_files.append(tmp_stem + suffix%count) '-pix_fmt', 'rgb24',
commands = [ '-r', str(fps), # frames per second
"ffmpeg", '-i', '-', # The imput comes from a pipe
"-y", '-an', # Tells FFMPEG not to expect any audio
"-loglevel", '-vcodec', 'mpeg',
"error", '-c:v', 'libx264',
"-i", '-pix_fmt', 'yuv420p',
tmp_stem + suffix, '-loglevel', 'error',
"-c:v", filepath,
"libx264",
"-vf",
"fps=%d,format=yuv420p"%rate,
filepath + ".mp4"
] ]
os.system(" ".join(commands)) process = sp.Popen(command, stdin=sp.PIPE)
for image_file in image_files: for frame in scene.frames:
os.remove(image_file) process.stdin.write(frame.tostring())
progress_bar.finish()
process.stdin.close()
# vs = VideoSink(scene.shape, filepath, rate) process.wait()
# 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/scene.frame_duration, (DEFAULT_WIDTH, DEFAULT_HEIGHT), True
# )
# progress = 0
# for frame in frames:
# if progress == 0:
# print "Writing movie"
# progress_bar.update(progress)
# r, g, b = cv2.split(np.array(frame))
# bgr_frame = cv2.merge([b, g, r])
# out.write(bgr_frame)
# progress += 1
# out.release()
# 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

@ -196,6 +196,7 @@ class NumberPlane(Mobject1D):
def generate_points(self): def generate_points(self):
color = self.color color = self.color
faded = Color(rgb = self.fade_factor*np.array(color.get_rgb())) faded = Color(rgb = self.fade_factor*np.array(color.get_rgb()))
#Vertical Lines
freq_color_pairs = [ freq_color_pairs = [
(self.x_faded_line_frequency, faded), (self.x_faded_line_frequency, faded),
(self.x_line_frequency, color) (self.x_line_frequency, color)

View File

@ -16,10 +16,10 @@ from script_wrapper import command_line_create_scene
class SampleScene(Scene): class SampleScene(Scene):
def construct(self): def construct(self):
plane = NumberPlane() circle = Circle().repeat(6)
arrow1 = Arrow(ORIGIN, UP, color = "green", point_thickness = 5) self.play(Transform(circle, Square(), run_time = 3))
arrow2 = Arrow(ORIGIN, LEFT, color = "Red") self.dither()
self.add(plane, arrow1, arrow2)
if __name__ == "__main__": if __name__ == "__main__":
command_line_create_scene() command_line_create_scene()

View File

@ -51,6 +51,7 @@ class Scene(object):
return self.name return self.name
return self.__class__.__name__ + \ return self.__class__.__name__ + \
self.args_to_string(*self.construct_args) self.args_to_string(*self.construct_args)
def set_name(self, name): def set_name(self, name):
self.name = name self.name = name
return self return self

View File

@ -3,6 +3,7 @@ import getopt
import imp import imp
import itertools as it import itertools as it
import inspect import inspect
import traceback
from helpers import cammel_case_initials from helpers import cammel_case_initials
from scene import Scene from scene import Scene
@ -58,8 +59,8 @@ def get_configuration(sys_argv):
config["write_all"] = True config["write_all"] = True
config["quiet"] = True config["quiet"] = True
#By default, write to file #By default, write to file
actions = set(["write", "preview", "save_image"]) actions = ["write", "preview", "save_image"]
if len(actions.intersection(config)) == 0: if not any([config[key] for key in actions]):
config["write"] = True config["write"] = True
if len(args) > 0: if len(args) > 0:
@ -78,13 +79,15 @@ def handle_scene(scene, **config):
if config["write"]: if config["write"]:
scene.write_to_movie(os.path.join(config["movie_prefix"], name)) scene.write_to_movie(os.path.join(config["movie_prefix"], name))
if config["save_image"]: if config["save_image"]:
scene.show_frame() if not config["write_all"]:
scene.show_frame()
path = os.path.join(MOVIE_DIR, config["movie_prefix"], "images") path = os.path.join(MOVIE_DIR, config["movie_prefix"], "images")
if not os.path.exists(path): if not os.path.exists(path):
os.mkdir(path) os.mkdir(path)
scene.save_image(path, name) scene.save_image(path, name)
if config["quiet"]: if config["quiet"]:
sys.stdout.close()
sys.stdout = curr_stdout sys.stdout = curr_stdout
def prompt_user_for_args(SceneClass): def prompt_user_for_args(SceneClass):
@ -143,7 +146,14 @@ def command_line_create_scene(movie_prefix = ""):
if type(args) is not tuple: if type(args) is not tuple:
args = (args,) args = (args,)
scene_kwargs["construct_args"] = args scene_kwargs["construct_args"] = args
handle_scene(SceneClass(**scene_kwargs), **config) try:
handle_scene(SceneClass(**scene_kwargs), **config)
except:
print "\n\n"
traceback.print_exc()
print "\n\n"

View File

@ -15,6 +15,9 @@ from script_wrapper import command_line_create_scene
MOVIE_PREFIX = "matrix_as_transform_2d/" MOVIE_PREFIX = "matrix_as_transform_2d/"
ARROW_CONFIG = {"point_thickness" : 2*DEFAULT_POINT_THICKNESS}
LIGHT_RED = "#F40"
def matrix_to_string(matrix): def matrix_to_string(matrix):
return "--".join(["-".join(map(str, row)) for row in matrix]) return "--".join(["-".join(map(str, row)) for row in matrix])
@ -44,9 +47,21 @@ class ShowMultiplication(NumberLineScene):
def args_to_string(num, show_original_line): def args_to_string(num, show_original_line):
end_string = "WithCopiedOriginalLine" if show_original_line else "" end_string = "WithCopiedOriginalLine" if show_original_line else ""
return str(num) + end_string return str(num) + end_string
@staticmethod
def string_to_args(string):
parts = string.split()
if len(parts) == 2:
num, original_line = parts
show_original_line = original_line == "WithCopiedOriginalLine"
return float(num), False
else:
return float(parts[0]), False
def construct(self, num, show_original_line): def construct(self, num, show_original_line):
config = {"density" : abs(num)*DEFAULT_POINT_DENSITY_1D} config = {
"density" : abs(num)*DEFAULT_POINT_DENSITY_1D,
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
}
if abs(num) < 1: if abs(num) < 1:
config["numerical_radius"] = SPACE_WIDTH/num config["numerical_radius"] = SPACE_WIDTH/num
@ -54,7 +69,7 @@ class ShowMultiplication(NumberLineScene):
if show_original_line: if show_original_line:
self.copy_original_line() self.copy_original_line()
self.dither() self.dither()
self.show_multiplication(num, run_time = 2.0) self.show_multiplication(num, run_time = 1.5)
self.dither() self.dither()
def copy_original_line(self): def copy_original_line(self):
@ -62,7 +77,7 @@ class ShowMultiplication(NumberLineScene):
copied_num_mobs = deepcopy(self.number_mobs) copied_num_mobs = deepcopy(self.number_mobs)
self.play( self.play(
ApplyFunction( ApplyFunction(
lambda m : m.shift(DOWN).highlight("green"), lambda m : m.shift(DOWN).highlight("lightgreen"),
copied_line copied_line
), *[ ), *[
ApplyMethod(mob.shift, DOWN) ApplyMethod(mob.shift, DOWN)
@ -90,7 +105,7 @@ class ExamplesOfNonlinearOneDimensionalTransforms(NumberLineScene):
def shift_zero((x, y, z)): def shift_zero((x, y, z)):
return (2*x+4, y, z) return (2*x+4, y, z)
self.nonlinear = text_mobject("Not a Linear Transform") self.nonlinear = text_mobject("Not a Linear Transform")
self.nonlinear.highlight("red").to_edge(UP) self.nonlinear.highlight(LIGHT_RED).to_edge(UP, buff = 1.5)
pairs = [ pairs = [
(sinx_plux_x, "numbers don't remain evenly spaced"), (sinx_plux_x, "numbers don't remain evenly spaced"),
(shift_zero, "zero does not remain fixed") (shift_zero, "zero does not remain fixed")
@ -104,7 +119,7 @@ class ExamplesOfNonlinearOneDimensionalTransforms(NumberLineScene):
self.clear() self.clear()
self.add(self.nonlinear) self.add(self.nonlinear)
NumberLineScene.construct(self) NumberLineScene.construct(self)
words = text_mobject(explanation).highlight("red") words = text_mobject(explanation).highlight(LIGHT_RED)
words.next_to(self.nonlinear, DOWN, buff = 0.5) words.next_to(self.nonlinear, DOWN, buff = 0.5)
self.add(words) self.add(words)
@ -136,7 +151,6 @@ class ShowTwoThenThree(ShowMultiplication):
self.dither() self.dither()
######################################################## ########################################################
class TransformScene2D(Scene): class TransformScene2D(Scene):
@ -144,7 +158,8 @@ class TransformScene2D(Scene):
config = { config = {
"x_radius" : 2*SPACE_WIDTH, "x_radius" : 2*SPACE_WIDTH,
"y_radius" : 2*SPACE_HEIGHT, "y_radius" : 2*SPACE_HEIGHT,
"density" : DEFAULT_POINT_DENSITY_1D*density_factor "density" : DEFAULT_POINT_DENSITY_1D*density_factor,
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
} }
if not use_faded_lines: if not use_faded_lines:
config["x_faded_line_frequency"] = None config["x_faded_line_frequency"] = None
@ -153,20 +168,22 @@ class TransformScene2D(Scene):
self.add(self.number_plane) self.add(self.number_plane)
def add_background(self): def add_background(self):
self.paint_into_background( grey_plane = NumberPlane(color = "grey")
NumberPlane(color = "grey").add_coordinates() num_mobs = grey_plane.get_coordinate_labels()
) self.paint_into_background(grey_plane, *num_mobs)
def add_x_y_arrows(self): def add_x_y_arrows(self):
self.x_arrow = Arrow( self.x_arrow = Arrow(
ORIGIN, ORIGIN,
self.number_plane.num_pair_to_point((1, 0)), self.number_plane.num_pair_to_point((1, 0)),
color = "green" color = "lightgreen",
**ARROW_CONFIG
) )
self.y_arrow = Arrow( self.y_arrow = Arrow(
ORIGIN, ORIGIN,
self.number_plane.num_pair_to_point((0, 1)), self.number_plane.num_pair_to_point((0, 1)),
color = "red" color = LIGHT_RED,
**ARROW_CONFIG
) )
self.add(self.x_arrow, self.y_arrow) self.add(self.x_arrow, self.y_arrow)
self.number_plane.filter_out( self.number_plane.filter_out(
@ -180,6 +197,8 @@ class TransformScene2D(Scene):
class ShowMatrixTransform(TransformScene2D): class ShowMatrixTransform(TransformScene2D):
args_list = [ args_list = [
([[1, 3], [-2, 0]], False, False),
([[1, 3], [-2, 0]], True, False),
([[1, 0.5], [0.5, 1]], True, False), ([[1, 0.5], [0.5, 1]], True, False),
([[2, 0], [0, 2]], True, False), ([[2, 0], [0, 2]], True, False),
([[0.5, 0], [0, 0.5]], True, False), ([[0.5, 0], [0, 0.5]], True, False),
@ -223,7 +242,8 @@ class ShowMatrixTransform(TransformScene2D):
new_arrow = Arrow( new_arrow = Arrow(
ORIGIN, ORIGIN,
self.number_plane.num_pair_to_point(matrix[:,index]), self.number_plane.num_pair_to_point(matrix[:,index]),
color = arrow.get_color() color = arrow.get_color(),
**ARROW_CONFIG
) )
arrow.remove_tip() arrow.remove_tip()
new_arrow.remove_tip() new_arrow.remove_tip()
@ -233,8 +253,6 @@ class ShowMatrixTransform(TransformScene2D):
anims.append(Transform(arrow, new_arrow, **kwargs)) anims.append(Transform(arrow, new_arrow, **kwargs))
self.play(*anims) self.play(*anims)
self.dither() self.dither()
self.set_name(str(self) + self.args_to_string(matrix, with_background, show_matrix))
self.save_image(os.path.join(MOVIE_DIR, MOVIE_PREFIX, "images"))
def get_density_factor(self, matrix): def get_density_factor(self, matrix):
max_norm = max([ max_norm = max([
@ -288,7 +306,7 @@ class ExamplesOfNonlinearTwoDimensionalTransformations(Scene):
def shift_zero((x, y, z)): def shift_zero((x, y, z)):
return (2*x + 3*y + 4, -1*x+y+2, z) return (2*x + 3*y + 4, -1*x+y+2, z)
self.nonlinear = text_mobject("Nonlinear Transform") self.nonlinear = text_mobject("Nonlinear Transform")
self.nonlinear.highlight("red").to_edge(UP) self.nonlinear.highlight(LIGHT_RED).to_edge(UP, buff = 1.5)
pairs = [ pairs = [
(squiggle, "lines to not remain straight"), (squiggle, "lines to not remain straight"),
(shift_zero, "the origin does not remain fixed") (shift_zero, "the origin does not remain fixed")
@ -305,7 +323,7 @@ class ExamplesOfNonlinearTwoDimensionalTransformations(Scene):
density = 3*DEFAULT_POINT_DENSITY_1D, density = 3*DEFAULT_POINT_DENSITY_1D,
) )
numbers = number_plane.get_coordinate_labels() numbers = number_plane.get_coordinate_labels()
words = text_mobject(explanation).highlight("red") words = text_mobject(explanation).highlight(LIGHT_RED)
words.next_to(self.nonlinear, DOWN, buff = 0.5) words.next_to(self.nonlinear, DOWN, buff = 0.5)
self.add(number_plane, self.nonlinear, words, *numbers) self.add(number_plane, self.nonlinear, words, *numbers)
@ -330,8 +348,8 @@ class TrickyExamplesOfNonlinearTwoDimensionalTransformations(Scene):
phrase1, phrase2 = text_mobject([ phrase1, phrase2 = text_mobject([
"These might look like they keep lines straight...", "These might look like they keep lines straight...",
"but diagonal lines get curved" "but diagonal lines get curved"
]).to_edge(UP).split() ]).to_edge(UP, buff = 1.5).split()
phrase2.highlight("red") phrase2.highlight(LIGHT_RED)
diagonal = Line( diagonal = Line(
DOWN*SPACE_HEIGHT+LEFT*SPACE_WIDTH, DOWN*SPACE_HEIGHT+LEFT*SPACE_WIDTH,
UP*SPACE_HEIGHT+RIGHT*SPACE_WIDTH UP*SPACE_HEIGHT+RIGHT*SPACE_WIDTH
@ -363,7 +381,7 @@ class TrickyExamplesOfNonlinearTwoDimensionalTransformations(Scene):
############# HORRIBLE! ########################## ############# HORRIBLE! ##########################
class ShowMatrixTransformHack(TransformScene2D): class ShowMatrixTransformWithDot(TransformScene2D):
args_list = [ args_list = [
([[1, 3], [-2, 0]], True, False), ([[1, 3], [-2, 0]], True, False),
] ]
@ -392,7 +410,7 @@ class ShowMatrixTransformHack(TransformScene2D):
return mobject return mobject
dot = Dot((-1, 2, 0), color = "yellow") dot = Dot((-1, 2, 0), color = "yellow")
x_arrow_copy = deepcopy(self.x_arrow) x_arrow_copy = deepcopy(self.x_arrow)
y_arrow_copy = Arrow(LEFT, LEFT+2*UP, color = "red") y_arrow_copy = Arrow(LEFT, LEFT+2*UP, color = LIGHT_RED, **ARROW_CONFIG)
self.number_plane.add(dot) self.number_plane.add(dot)
self.play(ApplyMethod(x_arrow_copy.rotate, np.pi)) self.play(ApplyMethod(x_arrow_copy.rotate, np.pi))
@ -409,7 +427,8 @@ class ShowMatrixTransformHack(TransformScene2D):
new_arrow = Arrow( new_arrow = Arrow(
ORIGIN, ORIGIN,
self.number_plane.num_pair_to_point(matrix[:,index]), self.number_plane.num_pair_to_point(matrix[:,index]),
color = arrow.get_color() color = arrow.get_color(),
**ARROW_CONFIG
) )
arrow.remove_tip() arrow.remove_tip()
new_arrow.remove_tip() new_arrow.remove_tip()
@ -421,7 +440,7 @@ class ShowMatrixTransformHack(TransformScene2D):
self.dither() self.dither()
x_arrow_copy = deepcopy(self.x_arrow) x_arrow_copy = deepcopy(self.x_arrow)
y_arrow_copy = Arrow(LEFT+2*UP, 5*RIGHT+2*UP, color = "red") y_arrow_copy = Arrow(LEFT+2*UP, 5*RIGHT+2*UP, color = LIGHT_RED, **ARROW_CONFIG)
self.play(ApplyMethod(x_arrow_copy.rotate, np.pi)) self.play(ApplyMethod(x_arrow_copy.rotate, np.pi))
self.play(ShowCreation(y_arrow_copy)) self.play(ShowCreation(y_arrow_copy))
self.remove(x_arrow_copy, y_arrow_copy) self.remove(x_arrow_copy, y_arrow_copy)