Source code for Frame

''' Everything related to making LinMaze Frames. '''
import os
import math
from time import sleep
from multiprocessing import Pool

import cv2
import dill as pickle
import numpy as np
from PIL import Image

from GramophoneTools.LinMaze import perlin
from GramophoneTools.LinMaze.Tools import Stopwatch, progressbar


[docs]def multi_make(list_of_frames): ''' Renders a list of frames with multiprocessing. :param list_of_frames: A list of Frame objects that sould be rendered. :type list_of_frames: [Frame] :returns: A list of rendered Frames. ''' pool = Pool() runtime = Stopwatch.Stopwatch() results = pool.imap(Frame.ext_make, list_of_frames) pool.close() while results._index < len(list_of_frames): progressbar.printProgressBar(results._index, len( list_of_frames), prefix=' Rendering', suffix=' (' + str(runtime) + ')') sleep(0.1) progressbar.printProgressBar(results._index, len( list_of_frames), prefix=' Rendering', suffix=' (' + str(runtime) + ')') pool.join() print() return list(results)
[docs]def transition(left, right, transition_width): """ Takes two 2 dimensional matrices representing a grayscale image and returns them with a smooth transition of given width between them. :param left: The image on the left. :type left: np.array with ndim=2 :param right: The image on the right. :type right: np.array with ndim=2 :param transition_width: The width of the smooth transition. :type transition_width: int """ if transition_width % 2: transition_width += 1 tw1 = transition_width tw2 = transition_width // 2 decrease = np.linspace(1, 0, tw1) increase = np.linspace(0, 1, tw1) if left.ndim < 3 and right.ndim < 3: # can't transition between RGB frames transition = np.round( decrease * left[:, -tw1:] + increase * right[:, :tw1]) left = left[:, :-tw2] right = right[:, tw2:] left[:, -tw2:] = transition[:, :tw2] right[:, :tw2] = transition[:, -tw2:] return (left, right)
def frame_transitions(list_of_frames, transition_width): ''' Makes a list of frames with smooth transitions between them. :param list_of_frames: A list of Frame objects without transitions. :type list_of_frames: [Frame] :param transition_width: The width of the smooth transition between the given Frames in pixels. :type transition_width: int :returns: A list of Frames with transitions. ''' frames = list_of_frames if transition_width > 0: for i, _ in enumerate(frames): j = (i+1) % (len(frames)) left_r, right_r = transition( frames[i].texture[..., 0], frames[j].texture[..., 0], transition_width) left_g, right_g = transition( frames[i].texture[..., 1], frames[j].texture[..., 1], transition_width) left_b, right_b = transition( frames[i].texture[..., 2], frames[j].texture[..., 2], transition_width) frames[i].texture = np.dstack((left_r, left_g, left_b)) frames[j].texture = np.dstack((right_r, right_g, right_b)) # frames[-1].frame, frames[0].frame = transition( # frames[-1].frame, frames[0].frame, transition_width) for i in range(len(frames)): if isinstance(frames[i], ImageFile): frames[i].frame = frames[i].texture return list(frames)
[docs]def combine(list_of_frames): ''' Makes a Frame that is a combination of a list of Frames. :param list_of_frames: A list of Frame objects without transitions. :type list_of_frames: [Frame] :returns: Frame ''' combined = list_of_frames[0].texture for fid in range(len(list_of_frames) - 1): combined = np.concatenate( (combined, list_of_frames[fid + 1].texture), axis=1) combined_frame = Frame(combined.shape[1], combined.shape[0]) combined_frame.texture = combined combined_frame.made = True return combined_frame
[docs]class Frame(object): ''' The visual component of the blocks that the mouse navigates in. :param width: The width of the Frame in pixels. :type width: int :param height: The height of the Frame in pixels. :type height: int :param rgb: The Red, Green and Blue levels of the Frame. :type rgb: (float, float, float) ''' frame_id = 0 display_name = "Frame" def __init__(self, width, height, rgb=(1, 1, 1)): Frame.frame_id += 1 self.frame_id = Frame.frame_id self.width = int(width) self.height = int(height) self.frame = None self.texture = None self.made = False self.rgb = rgb def __str__(self): string = "Frame #" + str(self.frame_id).ljust(2) + \ " - Size: " + str(self.width) + "×" + str(self.height) return string def __eq__(self, other): ''' Compares Frames based on type and all properties except id. ''' return type(self) is type(other) and \ list(self.__dict__.items())[1:] == list(other.__dict__.items())[1:] def __hash__(self): ''' The hash of the Frame. ''' import binascii hash_keys = ['width', 'height', 'seed', 'side_length', 'wavelength', 'angle'] hash_comps = [self.__dict__.get(key) for key in hash_keys] hash_comps = [comp for comp in hash_comps if comp is not None] hash_string = str(type(self)) + str(hash_comps) frame_hash = binascii.crc32(hash_string.encode('utf-8')) & 0xFFFFFFFF return int(frame_hash)
[docs] @staticmethod def ext_make(frame): ''' Tries to load the given Frame from cache or makes it and returns the rendered Frame. ''' frame.make() return frame
[docs] def make(self): ''' Renders the Frame. ''' self.make_texture() self.made = True
[docs] def make_img(self): ''' Makes a PIL Image of the Frame. ''' if not self.made: self.make() return Image.fromarray(np.flip(self.texture, 0))
[docs] def show_img(self): ''' Displays the bitmap image of the Frame. ''' self.make_img().show()
[docs] def save_img(self, filename): ''' Saves the bitmap image of the Frame with the given filename. :param filename: The filename the image should be saved as. :type filename: str ''' self.make_img().convert('RGB').save(filename)
[docs] def make_texture(self): ''' Makes the Frame's texture field that stores RGB data to be used as an OpenGL texture . ''' if self.texture is None: # Flip Frame up and down to fit with opengl indexing flipped_frame = np.flip( self.frame, 0) frame_r = np.round(self.rgb[0] * flipped_frame) frame_g = np.round(self.rgb[1] * flipped_frame) frame_b = np.round(self.rgb[2] * flipped_frame) data = np.dstack((frame_r, frame_g, frame_b)) self.texture = data.astype(np.uint8)
[docs] def mirror(self): ''' Horizontally mirrors the Frame ''' self.frame = np.flip(self.frame, 1)
[docs]class RandomFrame(Frame): ''' Abstract class all randomly generated Frames inherit from. :param seed: The seed of the random number generator ''' def __init__(self, width, height, seed): super().__init__(width, height) self.seed = seed
[docs]class BinaryNoise(RandomFrame): ''' Creates a Frame with noise, that only has black and white pixels ''' display_name = "Binary noise" def __str__(self): return super().__str__() + " - Type: Binary noise"
[docs] def make(self): if self.seed is not None: np.random.seed(self.seed) self.frame = 255 * np.random.randint(2, size=(self.height, self.width)) super().make()
[docs]class GreyNoise(RandomFrame): ''' Generates a frame with grayscale noise ''' display_name = "Grayscale noise" def __str__(self): return super().__str__() + " - Type: Greyscale noise"
[docs] def make(self): self.frame = np.random.randint(256, size=(self.height, self.width)) super().make()
[docs]class Checkerboard(Frame): ''' A Frame with a checkerboard pattern. :param side_length: The lenght of sides of the squares that make up the checkerboard pattern in pixels. :type side_length: int ''' display_name = "Checkerboard" def __init__(self, width, height, side_length): super().__init__(width, height) self.side_length = side_length def __str__(self): return super().__str__() + " - Type: Checkerboard - Side length: " + str(self.side_length)
[docs] def make(self): self.frame = np.zeros((self.height, self.width)) horizontal = math.ceil(self.width / (2 * self.side_length)) vertical = math.ceil(self.height / (2 * self.side_length)) black = np.zeros((self.side_length, self.side_length)) white = np.ones((self.side_length, self.side_length)) * 255 even_rows = np.concatenate(horizontal * [white, black], 1) odd_rows = np.concatenate(horizontal * [black, white], 1) block = np.concatenate(vertical * [even_rows, odd_rows], 0) self.frame = block[0:self.height, 0:self.width] super().make()
[docs]class Cloud(RandomFrame): ''' A Frame with a random cloud pattern in it. ''' display_name = "Cloud" def __str__(self): return super().__str__() + " - Type: Cloud"
[docs] def make(self): self.frame = perlin.cloud(self.width, self.height, self.seed) super().make()
[docs]class Marble(RandomFrame): ''' Generates a Frame with a marble pattern. ''' display_name = "Marble" def __str__(self): return super().__str__() + " - Type: Marble"
[docs] def make(self): self.frame = perlin.marble(self.width, self.height, self.seed) super().make()
[docs]class Wood(RandomFrame): ''' Generates a Frame with a wood grain pattern. ''' display_name = "Wood" def __str__(self): return super().__str__() + " - Type: Wood"
[docs] def make(self): self.frame = perlin.wood(self.width, self.height, self.seed) super().make()
[docs]class WaveFrame(Frame): ''' Generic grating Frame, for more specific ones to inherit form. :param wavelength: The wavelength of the grating in pixels, ie. the distance between the middles of the white portions. :type wavelength: int :param angle: The angle of the graing where 0 is vertical. :type angle: float ''' display_name = "Generic wave" def __init__(self, width, height, wavelength, angle): super().__init__(width, height) self.wavelength = wavelength self.angle = np.deg2rad(angle) self.wave_temp = np.linspace(0, 2 * np.pi, wavelength)
[docs] @staticmethod def wave_func(val): ''' Trigonometric function for a sinusoidal wave. ''' return math.ceil(127.5 + 127.5 * -1 * math.cos(val))
[docs] def make(self): self.frame = np.zeros((self.height, self.width)) def norm(x, y): """ Distance from origin along a line with angle. """ return abs(math.sin(self.angle) * y + math.cos(self.angle) * x) for [y, x], _ in np.ndenumerate(self.frame): val = norm(x, y) val %= self.wavelength if val % 1: below = math.floor(val) above = math.ceil(val) if above == self.wavelength: above = 0 val = (1 - (val % 1)) * \ self.wave_temp[below] + (val % 1) * self.wave_temp[above] else: val = self.wave_temp[int(val)] self.frame[y, x] = val super().make()
[docs]class SineWave(WaveFrame): ''' Sine wave modulated grating pattern Frame. ''' display_name = "Sine wave" def __str__(self): return super().__str__() + " - Type: Sine wave - Wavelength: " + \ str(self.wavelength) + " - Angle: " + str(math.degrees(self.angle))
[docs] def make(self): for index, val in np.ndenumerate(self.wave_temp): self.wave_temp[index] = WaveFrame.wave_func(val) super().make()
[docs]class SquareWave(WaveFrame): ''' Square wave modulated grating pattern Frame. ''' display_name = "Square wave" def __str__(self): return super().__str__() + " - Type: Square wave - Wavelength: " + \ str(self.wavelength) + " - Angle: " + str(math.degrees(self.angle))
[docs] def make(self): for index, val in np.ndenumerate(self.wave_temp): self.wave_temp[index] = int(WaveFrame.wave_func(val) > 127.5) * 255 super().make()
[docs]class ImageFile(Frame): """ A Frame made from a given image file. :param filename: The name of the file the Frame should be made from. :type filename: string """ display_name = "Image file" def __init__(self, height, filename): self.filename = filename img = cv2.imread(filename) if img.shape[0] != height: target_width = round(img.shape[1]*(height/img.shape[0])) img = cv2.resize(img, (target_width, height)) img = np.flip(img, 0) img = np.flip(img, 2) img_height, img_width, _ = img.shape super().__init__(img_width, img_height) self.frame = img.astype(np.uint8) def __str__(self): return super().__str__() + " - Type: Image file - Filename: " + self.filename
[docs] def make(self): self.make_texture() self.made = True
[docs] def make_texture(self): self.texture = self.frame