diff --git a/camera.py b/camera.py index 586a6644..eabc7d60 100644 --- a/camera.py +++ b/camera.py @@ -7,7 +7,7 @@ from colour import Color import aggdraw from helpers import * -from mobject import PMobject, VMobject +from mobject import PMobject, VMobject, ImageMobject class Camera(object): CONFIG = { @@ -91,6 +91,10 @@ class Camera(object): mobject.points, mobject.rgbs, self.adjusted_thickness(mobject.stroke_width) ) + elif isinstance(mobject, ImageMobject): + self.display_image_mobject(mobject) + else: + raise Exception("Unknown mobject type: " + type(mobject)) #TODO, more? Call out if it's unknown? self.display_multiple_vectorized_mobjects(vmobjects) @@ -184,6 +188,50 @@ class Camera(object): new_pa[indices] = rgbs self.pixel_array = new_pa.reshape((ph, pw, 3)) + 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) + + 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, + ]) + 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)) + target_rgbs = flat_impa[inner_flat_coords, :] + + flat_pa = self.pixel_array.reshape((ow*oh, rgb_len)) + flat_pa[to_change] = target_rgbs + + def align_points_to_camera(self, points): ## This is where projection should live return points - self.space_center diff --git a/constants.py b/constants.py index 1b589e5f..f8d1b33e 100644 --- a/constants.py +++ b/constants.py @@ -59,11 +59,14 @@ BOTTOM = SPACE_HEIGHT*DOWN LEFT_SIDE = SPACE_WIDTH*LEFT RIGHT_SIDE = SPACE_WIDTH*RIGHT +# Change this to point to where you want +# animation files to output +MOVIE_DIR = os.path.join(os.path.expanduser('~'), "Dropbox/3b1b_videos/animations/") +### THIS_DIR = os.path.dirname(os.path.realpath(__file__)) FILE_DIR = os.path.join(THIS_DIR, "files") IMAGE_DIR = os.path.join(FILE_DIR, "images") GIF_DIR = os.path.join(FILE_DIR, "gifs") -MOVIE_DIR = os.path.join(FILE_DIR, "movies") STAGED_SCENES_DIR = os.path.join(FILE_DIR, "staged_scenes") TEX_DIR = os.path.join(FILE_DIR, "Tex") TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex") diff --git a/mobject/__init__.py b/mobject/__init__.py index 4001a4d5..e87c8285 100644 --- a/mobject/__init__.py +++ b/mobject/__init__.py @@ -6,4 +6,5 @@ __all__ = [ from mobject import Mobject, Group from point_cloud_mobject import Point, Mobject1D, Mobject2D, PMobject -from vectorized_mobject import VMobject, VGroup \ No newline at end of file +from vectorized_mobject import VMobject, VGroup +from image_mobject import ImageMobject \ No newline at end of file diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index 25082217..e2957725 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -8,95 +8,41 @@ from helpers import * from mobject import Mobject from point_cloud_mobject import PMobject -class ImageMobject(PMobject): +class ImageMobject(Mobject): """ Automatically filters out black pixels """ CONFIG = { "filter_color" : "black", "invert" : False, - "use_cache" : True, - "stroke_width" : 1, - "scale_factorue": 1.0, - "should_center" : True, + # "use_cache" : True, + "height": 2.0, } - def __init__(self, image_file, **kwargs): - digest_locals(self) - Mobject.__init__(self, **kwargs) - self.name = to_camel_case( - os.path.split(image_file)[-1].split(".")[0] - ) - path = get_full_image_path(image_file) - self.generate_points_from_file(path) - self.scale(self.scale_factorue) - if self.should_center: - self.center() - - def generate_points_from_file(self, path): - if self.use_cache and self.read_in_cached_attrs(path): - return - image = Image.open(path).convert('RGB') - if self.invert: - image = invert_image(image) - self.generate_points_from_image_array(np.array(image)) - self.cache_attrs(path) - - def get_cached_attr_files(self, path, attrs): - #Hash should be unique to (path, invert) pair - unique_hash = str(hash(path+str(self.invert))) - return [ - os.path.join(IMAGE_MOBJECT_DIR, unique_hash)+"."+attr - for attr in attrs - ] - - def read_in_cached_attrs(self, path, - attrs = ("points", "rgbs"), - dtype = "float64"): - cached_attr_files = self.get_cached_attr_files(path, attrs) - if all(map(os.path.exists, cached_attr_files)): - for attr, cache_file in zip(attrs, cached_attr_files): - arr = np.fromfile(cache_file, dtype = dtype) - arr = arr.reshape(arr.size/self.dim, self.dim) - setattr(self, attr, arr) - return True - return False - - def cache_attrs(self, path, - attrs = ("points", "rgbs"), - dtype = "float64"): - cached_attr_files = self.get_cached_attr_files(path, attrs) - for attr, cache_file in zip(attrs, cached_attr_files): - getattr(self, attr).astype(dtype).tofile(cache_file) - - - def generate_points_from_image_array(self, image_array): - height, width = image_array.shape[:2] - #Flatten array, and find indices where rgb is not filter_rgb - array = image_array.reshape((height * width, 3)) - filter_rgb = np.array(Color(self.filter_color).get_rgb()) - filter_rgb = 255*filter_rgb.astype('uint8') - bools = array == filter_rgb - bools = bools[:,0]*bools[:,1]*bools[:,2] - indices = np.arange(height * width, dtype = 'int')[~bools] - rgbs = array[indices, :].astype('float') / 255.0 - - points = np.zeros((indices.size, 3), dtype = 'float64') - points[:,0] = indices%width - width/2 - points[:,1] = -indices/width + height/2 - - height, width = map(float, (height, width)) - if height / width > float(DEFAULT_HEIGHT) / DEFAULT_WIDTH: - points *= 2 * SPACE_HEIGHT / height + def __init__(self, filename_or_array, **kwargs): + if isinstance(filename_or_array, str): + path = get_full_image_path(filename_or_array) + image = Image.open(path).convert("RGB") + self.pixel_array = np.array(image) else: - points *= 2 * SPACE_WIDTH / width - self.add_points(points, rgbs = rgbs) - return self - - -class MobjectFromPixelArray(ImageMobject): - def __init__(self, image_array, **kwargs): + self.pixel_array = np.array(filename_or_array) Mobject.__init__(self, **kwargs) - self.generate_points_from_image_array(image_array) + + + 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() + self.scale_to_fit_height(self.height) + h, w = self.pixel_array.shape[:2] + self.stretch_to_fit_width(self.height*w/h) + + +