Spaces:
Build error
Build error
| import os | |
| import re | |
| from random import random | |
| import torch | |
| import pickle | |
| import argparse | |
| import numpy as np | |
| from helper import * | |
| from PIL import Image | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| from config.GlobalVariables import * | |
| from tensorboardX import SummaryWriter | |
| from SynthesisNetwork import SynthesisNetwork | |
| from DataLoader import DataLoader | |
| # import ffmpeg # for problems with ffmpeg uninstall ffmpeg and then install ffmpeg-python | |
| L = 256 | |
| def get_mean_global_W(net, loaded_data, device): | |
| """gets the mean global style vector for a given writer""" | |
| [_, _, _, _, _, _, all_word_level_stroke_in, all_word_level_stroke_out, all_word_level_stroke_length, all_word_level_term, all_word_level_char, all_word_level_char_length, all_segment_level_stroke_in, all_segment_level_stroke_out, | |
| all_segment_level_stroke_length, all_segment_level_term, all_segment_level_char, all_segment_level_char_length] = loaded_data | |
| batch_word_level_stroke_in = [torch.FloatTensor(a).to(device) for a in all_word_level_stroke_in] | |
| batch_word_level_stroke_out = [torch.FloatTensor(a).to(device) for a in all_word_level_stroke_out] | |
| batch_word_level_stroke_length = [torch.LongTensor(a).to(device).unsqueeze(-1) for a in all_word_level_stroke_length] | |
| batch_word_level_term = [torch.FloatTensor(a).to(device) for a in all_word_level_term] | |
| batch_word_level_char = [torch.LongTensor(a).to(device) for a in all_word_level_char] | |
| batch_word_level_char_length = [torch.LongTensor(a).to(device).unsqueeze(-1) for a in all_word_level_char_length] | |
| batch_segment_level_stroke_in = [[torch.FloatTensor(a).to(device) for a in b] for b in all_segment_level_stroke_in] | |
| batch_segment_level_stroke_out = [[torch.FloatTensor(a).to(device) for a in b] for b in all_segment_level_stroke_out] | |
| batch_segment_level_stroke_length = [[torch.LongTensor(a).to(device).unsqueeze(-1) for a in b] for b in all_segment_level_stroke_length] | |
| batch_segment_level_term = [[torch.FloatTensor(a).to(device) for a in b] for b in all_segment_level_term] | |
| batch_segment_level_char = [[torch.LongTensor(a).to(device) for a in b] for b in all_segment_level_char] | |
| batch_segment_level_char_length = [[torch.LongTensor(a).to(device).unsqueeze(-1) for a in b] for b in all_segment_level_char_length] | |
| with torch.no_grad(): | |
| word_inf_state_out = net.inf_state_fc1(batch_word_level_stroke_out[0]) | |
| word_inf_state_out = net.inf_state_relu(word_inf_state_out) | |
| word_inf_state_out, _ = net.inf_state_lstm(word_inf_state_out) | |
| user_word_level_char = batch_word_level_char[0] | |
| user_word_level_term = batch_word_level_term[0] | |
| original_Wc = [] | |
| word_batch_id = 0 | |
| curr_seq_len = batch_word_level_stroke_length[0][word_batch_id][0] | |
| curr_char_len = batch_word_level_char_length[0][word_batch_id][0] | |
| char_vector = torch.eye(len(CHARACTERS))[user_word_level_char[word_batch_id][:curr_char_len]].to(device) | |
| current_term = user_word_level_term[word_batch_id][:curr_seq_len].unsqueeze(-1) | |
| split_ids = torch.nonzero(current_term)[:, 0] | |
| char_vector_1 = net.char_vec_fc_1(char_vector) | |
| char_vector_1 = net.char_vec_relu_1(char_vector_1) | |
| char_out_1 = char_vector_1.unsqueeze(0) | |
| char_out_1, (c, h) = net.char_lstm_1(char_out_1) | |
| char_out_1 = char_out_1.squeeze(0) | |
| char_out_1 = net.char_vec_fc2_1(char_out_1) | |
| char_matrix_1 = char_out_1.view([-1, 1, 256, 256]) | |
| char_matrix_1 = char_matrix_1.squeeze(1) | |
| char_matrix_inv_1 = torch.inverse(char_matrix_1) | |
| W_c_t = word_inf_state_out[word_batch_id][:curr_seq_len] | |
| W_c = torch.stack([W_c_t[i] for i in split_ids]) | |
| original_Wc.append(W_c) | |
| W = torch.bmm(char_matrix_inv_1, W_c.unsqueeze(2)).squeeze(-1) | |
| mean_global_W = torch.mean(W, 0) | |
| return mean_global_W | |
| def get_DSD(net, target_word, writer_mean_Ws, all_loaded_data, device): | |
| """ | |
| returns a style vector and character matrix for each character/segment in target_word | |
| n is the number of writers | |
| M is the number of characters in the target word | |
| L is the latent vector size (in this case 256) | |
| input: | |
| - target_word, a string of length M to be converted to a DSD | |
| - writer_mean_Ws, a list of n style vectors of size L | |
| output: | |
| - all_writer_Ws, a tensor of size n x M x L representing the style vectors for each writer and character | |
| - all_writer_Cs, a tensor of size n x M x L x L representing the corresponding character matrix | |
| """ | |
| n = len(all_loaded_data) | |
| M = len(target_word) | |
| all_writer_Ws = torch.zeros(n, M, L) | |
| all_writer_Cs = torch.zeros(n, M, L, L) | |
| for i in range(n): | |
| np.random.seed(0) | |
| [_, _, _, _, _, _, all_word_level_stroke_in, all_word_level_stroke_out, all_word_level_stroke_length, all_word_level_term, all_word_level_char, all_word_level_char_length, all_segment_level_stroke_in, all_segment_level_stroke_out, | |
| all_segment_level_stroke_length, all_segment_level_term, all_segment_level_char, all_segment_level_char_length] = all_loaded_data[i] | |
| available_segments = {} | |
| for sid, sentence in enumerate(all_segment_level_char[0]): | |
| for wid, word in enumerate(sentence): | |
| segment = ''.join([CHARACTERS[i] for i in word]) | |
| split_ids = np.asarray(np.nonzero(all_segment_level_term[0][sid][wid])) | |
| if segment in available_segments: | |
| available_segments[segment].append([all_segment_level_stroke_out[0][sid][wid][:all_segment_level_stroke_length[0][sid][wid]], split_ids]) | |
| else: | |
| available_segments[segment] = [[all_segment_level_stroke_out[0][sid][wid][:all_segment_level_stroke_length[0][sid][wid]], split_ids]] | |
| index = 0 | |
| all_W = [] | |
| all_C = [] | |
| # while index <= len(target_word): | |
| while index < len(target_word): | |
| available = False | |
| # Currently this just uses each character individually instead of the whole segment | |
| # for end_index in range(len(target_word), index, -1): | |
| # segment = target_word[index:end_index] | |
| # print (segment) | |
| segment = target_word[index] | |
| if segment in available_segments: # method beta | |
| # print(f'in dic - {segment}') | |
| available = True | |
| candidates = available_segments[segment] | |
| segment_level_stroke_out, split_ids = candidates[np.random.randint(len(candidates))] | |
| out = net.inf_state_fc1(torch.FloatTensor(segment_level_stroke_out).to(device).unsqueeze(0)) | |
| out = net.inf_state_relu(out) | |
| seg_W_c, (h_n, _) = net.inf_state_lstm(out) | |
| character = segment[0] # take the first character of the segment? | |
| # get character matrix using same method as method beta | |
| char_vector = torch.eye(len(CHARACTERS))[CHARACTERS.index(character)].to(device).unsqueeze(0) | |
| out = net.char_vec_fc_1(char_vector) | |
| out = net.char_vec_relu_1(out) | |
| out, _ = net.char_lstm_1(out.unsqueeze(0)) | |
| out = out.squeeze(0) | |
| out = net.char_vec_fc2_1(out) | |
| char_matrix = out.view([-1, 256, 256]) | |
| inv_char_matrix = char_matrix.inverse() | |
| id = split_ids[0][0] | |
| W_c_vector = seg_W_c[0, id].squeeze() | |
| # invert to get writer-independed DSD | |
| W_vector = torch.bmm(inv_char_matrix, W_c_vector.repeat(inv_char_matrix.size(0), 1).unsqueeze(2)) | |
| all_W.append(W_vector) | |
| all_C.append(char_matrix) | |
| index += 1 | |
| if index == len(target_word): | |
| break | |
| if not available: # method alpha | |
| character = target_word[index] | |
| # print(f'no dic - {character}') | |
| char_vector = torch.eye(len(CHARACTERS))[CHARACTERS.index(character)].to(device).unsqueeze(0) | |
| out = net.char_vec_fc_1(char_vector) | |
| out = net.char_vec_relu_1(out) | |
| out, _ = net.char_lstm_1(out.unsqueeze(0)) | |
| out = out.squeeze(0) | |
| out = net.char_vec_fc2_1(out) | |
| char_matrix = out.view([-1, 256, 256]) | |
| W_vector = writer_mean_Ws[i].repeat(char_matrix.size(0), 1).unsqueeze(2) | |
| # all_W.append([W_vector]) | |
| all_W.append(W_vector) | |
| all_C.append(char_matrix) | |
| index += 1 | |
| all_writer_Ws[i, :, :] = torch.stack(all_W).squeeze() | |
| all_writer_Cs[i, :, :, :] = torch.stack(all_C).squeeze() | |
| return all_writer_Ws, all_writer_Cs | |
| def get_writer_blend_W_c(writer_weights, all_Ws, all_Cs): | |
| """ | |
| generates character-dependent style-dependent DSDs for each character/segement in target_word, | |
| averaging together the styles of the handwritings using provided weights | |
| n is the number of writers | |
| M is the number of characters in the target word | |
| L is the latent vector size (in this case 256) | |
| input: | |
| - writer_weights, a list of length n weights for each writer that sum to one | |
| - all_writer_Ws, an n x M x L tensor representing each weiter's style vector for every character | |
| - all_writer_Cs, an n x M x L x L tensor representing the style's correspodning character matrix | |
| output: | |
| - an M x 1 x L tensor of M scharacter-dependent style-dependent DSDs | |
| """ | |
| n, M, _ = all_Ws.shape | |
| weights_tensor = torch.tensor(writer_weights).repeat_interleave(M * L).reshape(n, M, L) # repeat accross remaining dimensions | |
| W_vectors = (weights_tensor * all_Ws).sum(axis=0).unsqueeze(-1) # take weighted sum accross writers axis | |
| char_matrices = all_Cs[0, :, :, :] # character matrices are independent of writer | |
| W_cs = torch.bmm(char_matrices, W_vectors) | |
| return W_cs.reshape(M, 1, L) | |
| def get_character_blend_W_c(character_weights, all_Ws, all_Cs): | |
| """ | |
| generates a single character-dependent style-dependent DSD, | |
| averaging together the characters using provided weights | |
| M is the number of characters to blend | |
| L is the latent vector size (in this case 256) | |
| input: | |
| - character_weights, a list of length M weights for each character that sum to one | |
| - all_Ws, a 1 x M x L tensor representing the wwiter's style vector for each character | |
| - all_Cs, 1 x M x L x L tensor representing the style's correspodning character matrix | |
| output: | |
| - a 1 x 1 x L tensor representing the character-dependent style-dependent DSDs | |
| """ | |
| M = len(character_weights) | |
| W_vector = all_Ws[0, 0, :].unsqueeze(-1) | |
| weights_tensor = torch.tensor(character_weights).repeat_interleave(L * L).reshape(1, M, L, L) # repeat accross remaining dimensions | |
| char_matrix = (weights_tensor * all_Cs).sum(axis=1).squeeze() # take weighted sum accross characters axis | |
| W_c = char_matrix @ W_vector | |
| return W_c.reshape(1, 1, L) | |
| def get_commands(net, target_word, all_W_c): # seems like target_word is only used for length | |
| """converts character-dependent style-dependent DSDs to a list of commands for drawing""" | |
| all_commands = [] | |
| current_id = 0 | |
| while True: | |
| word_Wc_rec_TYPE_D = [] | |
| TYPE_D_REF = [] | |
| cid = 0 | |
| for segment_batch_id in range(len(all_W_c)): | |
| if len(TYPE_D_REF) == 0: | |
| for each_segment_Wc in all_W_c[segment_batch_id]: | |
| if cid >= current_id: | |
| word_Wc_rec_TYPE_D.append(each_segment_Wc) | |
| cid += 1 | |
| if len(word_Wc_rec_TYPE_D) > 0: | |
| TYPE_D_REF.append(all_W_c[segment_batch_id][-1]) | |
| else: | |
| for each_segment_Wc in all_W_c[segment_batch_id]: | |
| magic_inp = torch.cat([torch.stack(TYPE_D_REF, 0), each_segment_Wc.unsqueeze(0)], 0) | |
| magic_inp = magic_inp.unsqueeze(0) | |
| TYPE_D_out, (c, h) = net.magic_lstm(magic_inp) | |
| TYPE_D_out = TYPE_D_out.squeeze(0) | |
| word_Wc_rec_TYPE_D.append(TYPE_D_out[-1]) | |
| TYPE_D_REF.append(all_W_c[segment_batch_id][-1]) | |
| WC_ = torch.stack(word_Wc_rec_TYPE_D) | |
| tmp_commands, res = net.sample_from_w_fix(WC_) | |
| current_id += res | |
| if len(all_commands) == 0: | |
| all_commands.append(tmp_commands) | |
| else: | |
| all_commands.append(tmp_commands[1:]) | |
| if res < 0 or current_id >= len(target_word): | |
| break | |
| commands = [] | |
| px, py = 0, 100 | |
| for coms in all_commands: | |
| for i, [dx, dy, t] in enumerate(coms): | |
| x = px + dx * 5 | |
| y = py + dy * 5 | |
| commands.append([x, y, t]) | |
| px, py = x, y | |
| commands = np.asarray(commands) | |
| commands[:, 0] -= np.min(commands[:, 0]) | |
| return commands | |
| def mdn_video(target_word, num_samples, scale_sd, clamp_mdn, net, all_loaded_data, device): | |
| ''' | |
| Method creating gif of mdn samples | |
| num_samples: number of samples to be inputted | |
| max_scale: the maximum value used to scale SD while sampling (increment is based on num samples) | |
| ''' | |
| words = target_word.split(' ') | |
| us_target_word = re.sub(r"\s+", '_', target_word) | |
| os.makedirs(f"./results/{us_target_word}_mdn_samples", exist_ok=True) | |
| for i in range(num_samples): | |
| im = Image.fromarray(np.zeros([160, 750])) | |
| dr = ImageDraw.Draw(im) | |
| width = 50 | |
| net.scale_sd = scale_sd | |
| net.clamp_mdn = clamp_mdn | |
| mean_global_W = get_mean_global_W(net, all_loaded_data[0], device) | |
| for word in words: | |
| writer_Ws, writer_Cs = get_DSD(net, word, [mean_global_W], [all_loaded_data[0]], device) | |
| all_W_c = get_writer_blend_W_c([1], writer_Ws, writer_Cs) | |
| all_commands = get_commands(net, word, all_W_c) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line((px+width, py, x+width, y), 255, 1) | |
| px, py = x, y | |
| width += np.max(all_commands[:, 0]) + 25 | |
| im.convert("RGB").save(f'results/{us_target_word}_mdn_samples/sample_{i}.png') | |
| # Convert fromes to video using ffmpeg | |
| photos = ffmpeg.input(f'results/{us_target_word}_mdn_samples/sample_*.png', pattern_type='glob', framerate=10) | |
| videos = photos.output(f'results/{us_target_word}_video.mov', vcodec="libx264", pix_fmt="yuv420p") | |
| videos.run(overwrite_output=True) | |
| def sample_blended_writers(writer_weights, target_sentence, net, all_loaded_data, device="cpu"): | |
| """Generates an image of handwritten text based on target_sentence""" | |
| words = target_sentence.split(' ') | |
| im = Image.fromarray(np.zeros([160, 750])) | |
| dr = ImageDraw.Draw(im) | |
| width = 50 | |
| writer_mean_Ws = [] | |
| for loaded_data in all_loaded_data: | |
| mean_global_W = get_mean_global_W(net, loaded_data, device) | |
| writer_mean_Ws.append(mean_global_W) | |
| for word in words: | |
| all_writer_Ws, all_writer_Cs = get_DSD(net, word, writer_mean_Ws, all_loaded_data, device) | |
| all_W_c = get_writer_blend_W_c(writer_weights, all_writer_Ws, all_writer_Cs) | |
| all_commands = get_commands(net, word, all_W_c) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line((px+width, py, x+width, y), 255, 1) | |
| px, py = x, y | |
| width += np.max(all_commands[:, 0]) + 25 | |
| return im | |
| def sample_character_grid(letters, grid_size, net, all_loaded_data, device="cpu"): | |
| """Generates an image of handwritten text based on target_sentence""" | |
| width = 60 | |
| im = Image.fromarray(np.zeros([(grid_size + 1) * width, (grid_size + 1) * width])) | |
| dr = ImageDraw.Draw(im) | |
| M = len(letters) | |
| mean_global_W = get_mean_global_W(net, all_loaded_data[0], device) | |
| # all_Ws = torch.zeros(1, M, L) | |
| all_Cs = torch.zeros(1, M, L, L) | |
| for i in range(M): # get corners of grid | |
| W_vector, char_matrix = get_DSD(net, letters[i], [mean_global_W], [all_loaded_data[0]], device) | |
| # all_Ws[:, i, :] = W_vector | |
| all_Cs[:, i, :, :] = char_matrix | |
| all_Ws = mean_global_W.reshape(1, 1, L) | |
| for i in range(grid_size): | |
| for j in range(grid_size): | |
| wx = i / (grid_size - 1) | |
| wy = j / (grid_size - 1) | |
| character_weights = [(1 - wx) * (1 - wy), # top left is 1 at (0, 0) | |
| wx * (1 - wy), # top right is 1 at (1, 0) | |
| (1 - wx) * wy, # bottom left is 1 at (0, 1) | |
| wx * wy] # bottom right is 1 at (1, 1) | |
| all_W_c = get_character_blend_W_c(character_weights, all_Ws, all_Cs) | |
| all_commands = get_commands(net, letters[0], all_W_c) | |
| offset_x = i * width | |
| offset_y = j * width | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line(( | |
| px + offset_x + width/2, | |
| py + offset_y - width/2, # letters are shifted down for some reason | |
| x + offset_x + width/2, | |
| y + offset_y - width/2), 255, 1) | |
| px, py = x, y | |
| return im | |
| def writer_interpolation_video(target_sentence, transition_time, net, all_loaded_data, device="cpu"): | |
| """ | |
| Generates a video of interpolating between each provided writer | |
| """ | |
| n = len(all_loaded_data) | |
| os.makedirs(f"./results/{target_sentence}_blend_frames", exist_ok=True) | |
| words = target_sentence.split(' ') | |
| writer_mean_Ws = [] | |
| for loaded_data in all_loaded_data: | |
| mean_global_W = get_mean_global_W(net, loaded_data, device) | |
| writer_mean_Ws.append(mean_global_W) | |
| word_Ws = [] | |
| word_Cs = [] | |
| for word in words: | |
| all_writer_Ws, all_writer_Cs = get_DSD(net, word, writer_mean_Ws, all_loaded_data, device) | |
| word_Ws.append(all_writer_Ws) | |
| word_Cs.append(all_writer_Cs) | |
| for i in range(n - 1): | |
| for j in range(transition_time): | |
| im = Image.fromarray(np.zeros([160, 750])) | |
| dr = ImageDraw.Draw(im) | |
| width = 50 | |
| completion = j/(transition_time) | |
| individual_weights = [1 - completion, completion] | |
| writer_weights = [0] * i + individual_weights + [0] * (n - 2 - i) | |
| for k, word in enumerate(words): | |
| all_writer_Ws, all_writer_Cs = word_Ws[k], word_Cs[k] | |
| all_W_c = get_writer_blend_W_c(writer_weights, all_writer_Ws, all_writer_Cs) | |
| all_commands = get_commands(net, word, all_W_c) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line((px+width, py, x+width, y), 255, 1) | |
| px, py = x, y | |
| width += np.max(all_commands[:, 0]) + 25 | |
| im.convert("RGB").save(f"./results/{target_sentence}_blend_frames/frame_{str(i * transition_time + j).zfill(3)}.png") | |
| # Convert fromes to video using ffmpeg | |
| photos = ffmpeg.input(f"./results/{target_sentence}_blend_frames/frame_*.png", pattern_type='glob', framerate=10) | |
| videos = photos.output(f"results/{target_sentence}_blend_video.mov", vcodec="libx264", pix_fmt="yuv420p") | |
| videos.run(overwrite_output=True) | |
| def mdn_single_sample(target_word, scale_sd, clamp_mdn, net, all_loaded_data, device): | |
| ''' | |
| Method creating gif of mdn samples | |
| num_samples: number of samples to be inputted | |
| max_scale: the maximum value used to scale SD while sampling (increment is based on num samples) | |
| ''' | |
| words = target_word.split(' ') | |
| im = Image.fromarray(np.zeros([160, 750])) | |
| dr = ImageDraw.Draw(im) | |
| width = 50 | |
| net.scale_sd = scale_sd | |
| net.clamp_mdn = clamp_mdn | |
| mean_global_W = get_mean_global_W(net, all_loaded_data[0], device) | |
| for word in words: | |
| writer_Ws, writer_Cs = get_DSD(net, word, [mean_global_W], [all_loaded_data[0]], device) | |
| all_W_c = get_writer_blend_W_c([1], writer_Ws, writer_Cs) | |
| all_commands = get_commands(net, word, all_W_c) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line((px+width, py, x+width, y), 255, 1) | |
| px, py = x, y | |
| width += np.max(all_commands[:, 0]) + 25 | |
| return im | |
| def sample_blended_chars(character_weights, letters, net, all_loaded_data, device="cpu"): | |
| """Generates an image of handwritten text based on target_sentence""" | |
| width = 60 | |
| im = Image.fromarray(np.zeros([100, 100])) | |
| dr = ImageDraw.Draw(im) | |
| M = len(letters) | |
| mean_global_W = get_mean_global_W(net, all_loaded_data[0], device) | |
| all_Cs = torch.zeros(1, M, L, L) | |
| for i in range(M): # get corners of grid | |
| W_vector, char_matrix = get_DSD(net, letters[i], [mean_global_W], [all_loaded_data[0]], device) | |
| all_Cs[:, i, :, :] = char_matrix | |
| all_Ws = mean_global_W.reshape(1, 1, L) | |
| all_W_c = get_character_blend_W_c(character_weights, all_Ws, all_Cs) | |
| all_commands = get_commands(net, letters[0], all_W_c) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line(( | |
| px + width/2, | |
| py - width/2, # letters are shifted down for some reason | |
| x + width/2, | |
| y - width/2), 255, 1) | |
| px, py = x, y | |
| return im | |
| def char_interpolation_video(letters, transition_time, net, all_loaded_data, device="cpu"): | |
| """Generates an image of handwritten text based on target_sentence""" | |
| os.makedirs(f"./results/{''.join(letters)}_frames", exist_ok=True) # make a folder for the frames | |
| width = 50 | |
| M = len(letters) | |
| mean_global_W = get_mean_global_W(net, all_loaded_data[0], device) | |
| all_Cs = torch.zeros(1, M, L, L) | |
| for i in range(M): # get corners of grid | |
| W_vector, char_matrix = get_DSD(net, letters[i], [mean_global_W], [all_loaded_data[0]], device) | |
| all_Cs[:, i, :, :] = char_matrix | |
| all_Ws = mean_global_W.reshape(1, 1, L) | |
| for i in range(M - 1): | |
| for j in range(transition_time): | |
| completion = j / (transition_time - 1) | |
| individual_weights = [1 - completion, completion] | |
| character_weights = [0] * i + individual_weights + [0] * (M - 2 - i) | |
| all_W_c = get_character_blend_W_c(character_weights, all_Ws, all_Cs) | |
| all_commands = get_commands(net, "change this later!", all_W_c) | |
| im = Image.fromarray(np.zeros([100, 100])) | |
| dr = ImageDraw.Draw(im) | |
| for [x, y, t] in all_commands: | |
| if t == 0: | |
| dr.line(( | |
| px + width/2, | |
| py - width/2, # letters are shifted down for some reason | |
| x + width/2, | |
| y - width/2), 255, 1) | |
| px, py = x, y | |
| im.convert("RGB").save(f"results/{''.join(letters)}_frames/frames_{str(i * transition_time + j).zfill(3)}.png") | |
| # Convert fromes to video using ffmpeg | |
| photos = ffmpeg.input(f"results/{''.join(letters)}_frames/frames_*.png", pattern_type='glob', framerate=24) | |
| videos = photos.output(f"results/{''.join(letters)}_video.mov", vcodec="libx264", pix_fmt="yuv420p") | |
| videos.run(overwrite_output=True) | |