|
|
import streamlit as st |
|
|
import os |
|
|
import numpy as np |
|
|
from PIL import Image, ImageEnhance, ImageFilter, ImageDraw |
|
|
import time |
|
|
from concurrent.futures import ThreadPoolExecutor |
|
|
from functools import partial |
|
|
|
|
|
class Animator: |
|
|
def __init__(self): |
|
|
self.frame_cache = {} |
|
|
self.aspect_ratio = "1:1" |
|
|
self.frames_per_animation = 15 |
|
|
|
|
|
def set_aspect_ratio(self, aspect_ratio): |
|
|
"""Set the aspect ratio for animations""" |
|
|
self.aspect_ratio = aspect_ratio |
|
|
|
|
|
def set_frames_per_animation(self, frames): |
|
|
"""Set the number of frames per animation""" |
|
|
self.frames_per_animation = max(10, min(frames, 20)) |
|
|
|
|
|
def apply_cinematic_effects(self, image): |
|
|
"""Apply cinematic effects to enhance the frame quality""" |
|
|
try: |
|
|
|
|
|
if isinstance(image, str): |
|
|
img = Image.open(image) |
|
|
else: |
|
|
img = image |
|
|
|
|
|
|
|
|
enhancer = ImageEnhance.Contrast(img) |
|
|
img = enhancer.enhance(1.2) |
|
|
|
|
|
|
|
|
enhancer = ImageEnhance.Color(img) |
|
|
img = enhancer.enhance(1.1) |
|
|
|
|
|
|
|
|
|
|
|
mask = Image.new('L', img.size, 255) |
|
|
draw = ImageDraw.Draw(mask) |
|
|
|
|
|
width, height = img.size |
|
|
center_x, center_y = width // 2, height // 2 |
|
|
max_radius = min(width, height) // 2 |
|
|
|
|
|
for y in range(height): |
|
|
for x in range(width): |
|
|
|
|
|
distance = np.sqrt((x - center_x)**2 + (y - center_y)**2) |
|
|
|
|
|
intensity = int(255 * (1 - 0.3 * (distance / max_radius)**2)) |
|
|
mask.putpixel((x, y), intensity) |
|
|
|
|
|
|
|
|
img = Image.composite(img, Image.new('RGB', img.size, (0, 0, 0)), mask) |
|
|
|
|
|
|
|
|
grain = Image.effect_noise((img.width, img.height), 10) |
|
|
grain = grain.convert('L') |
|
|
grain = grain.filter(ImageFilter.GaussianBlur(radius=1)) |
|
|
img = Image.blend(img, Image.composite(img, Image.new('RGB', img.size, (128, 128, 128)), grain), 0.05) |
|
|
|
|
|
return img |
|
|
except Exception as e: |
|
|
|
|
|
if isinstance(image, str): |
|
|
return Image.open(image) |
|
|
return image |
|
|
|
|
|
def add_zoom_animation(self, image_path, num_frames=None, zoom_factor=1.05, output_dir="temp"): |
|
|
"""Add a simple zoom animation to an image with cinematic effects""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
|
|
|
cache_key = f"zoom_{image_path}_{num_frames}_{zoom_factor}_{self.aspect_ratio}" |
|
|
if cache_key in self.frame_cache: |
|
|
return self.frame_cache[cache_key] |
|
|
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
|
|
|
|
|
img = Image.open(image_path) |
|
|
|
|
|
|
|
|
frames = [] |
|
|
for scale in np.linspace(1.0, zoom_factor, num_frames): |
|
|
size = (int(img.width * scale), int(img.height * scale)) |
|
|
scaled_img = img.resize(size, Image.LANCZOS) |
|
|
|
|
|
|
|
|
new_img = Image.new("RGB", (img.width, img.height)) |
|
|
left = (img.width - scaled_img.width) // 2 |
|
|
top = (img.height - scaled_img.height) // 2 |
|
|
new_img.paste(scaled_img, (left, top)) |
|
|
|
|
|
|
|
|
new_img = self.apply_cinematic_effects(new_img) |
|
|
|
|
|
|
|
|
frame_path = f"{output_dir}/frame_{os.path.basename(image_path)}_{len(frames)}.png" |
|
|
new_img.save(frame_path) |
|
|
frames.append(frame_path) |
|
|
|
|
|
|
|
|
self.frame_cache[cache_key] = frames |
|
|
return frames |
|
|
|
|
|
def add_pan_animation(self, image_path, num_frames=None, direction="right", output_dir="temp"): |
|
|
"""Add a simple panning animation to an image with cinematic effects""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
|
|
|
cache_key = f"pan_{image_path}_{num_frames}_{direction}_{self.aspect_ratio}" |
|
|
if cache_key in self.frame_cache: |
|
|
return self.frame_cache[cache_key] |
|
|
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
|
|
|
|
|
img = Image.open(image_path) |
|
|
|
|
|
|
|
|
frames = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pan_factor = 0.1 |
|
|
|
|
|
if self.aspect_ratio == "9:16" and (direction == "left" or direction == "right"): |
|
|
pan_factor = 0.05 |
|
|
elif self.aspect_ratio == "16:9" and (direction == "up" or direction == "down"): |
|
|
pan_factor = 0.05 |
|
|
|
|
|
|
|
|
if direction == "right": |
|
|
x_shifts = np.linspace(0, img.width * pan_factor, num_frames) |
|
|
y_shifts = np.zeros(num_frames) |
|
|
elif direction == "left": |
|
|
x_shifts = np.linspace(0, -img.width * pan_factor, num_frames) |
|
|
y_shifts = np.zeros(num_frames) |
|
|
elif direction == "down": |
|
|
x_shifts = np.zeros(num_frames) |
|
|
y_shifts = np.linspace(0, img.height * pan_factor, num_frames) |
|
|
elif direction == "up": |
|
|
x_shifts = np.zeros(num_frames) |
|
|
y_shifts = np.linspace(0, -img.height * pan_factor, num_frames) |
|
|
else: |
|
|
|
|
|
x_shifts = np.linspace(0, img.width * pan_factor, num_frames) |
|
|
y_shifts = np.zeros(num_frames) |
|
|
|
|
|
for i in range(num_frames): |
|
|
|
|
|
new_img = Image.new("RGB", (img.width, img.height)) |
|
|
|
|
|
|
|
|
new_img.paste(img, (int(x_shifts[i]), int(y_shifts[i]))) |
|
|
|
|
|
|
|
|
new_img = self.apply_cinematic_effects(new_img) |
|
|
|
|
|
|
|
|
frame_path = f"{output_dir}/frame_{os.path.basename(image_path)}_{i}.png" |
|
|
new_img.save(frame_path) |
|
|
frames.append(frame_path) |
|
|
|
|
|
|
|
|
self.frame_cache[cache_key] = frames |
|
|
return frames |
|
|
|
|
|
def add_fade_animation(self, image_path, num_frames=None, fade_type="in", output_dir="temp"): |
|
|
"""Add a fade in/out animation to an image with cinematic effects""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
|
|
|
cache_key = f"fade_{image_path}_{num_frames}_{fade_type}_{self.aspect_ratio}" |
|
|
if cache_key in self.frame_cache: |
|
|
return self.frame_cache[cache_key] |
|
|
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
|
|
|
|
|
img = Image.open(image_path) |
|
|
|
|
|
|
|
|
frames = [] |
|
|
|
|
|
if fade_type == "in": |
|
|
alphas = np.linspace(0.3, 1.0, num_frames) |
|
|
elif fade_type == "out": |
|
|
alphas = np.linspace(1.0, 0.3, num_frames) |
|
|
else: |
|
|
|
|
|
alphas = np.linspace(0.3, 1.0, num_frames) |
|
|
|
|
|
for i, alpha in enumerate(alphas): |
|
|
|
|
|
enhancer = Image.new("RGBA", img.size, (0, 0, 0, 0)) |
|
|
new_img = Image.blend(enhancer, img.convert("RGBA"), alpha) |
|
|
new_img = new_img.convert("RGB") |
|
|
|
|
|
|
|
|
new_img = self.apply_cinematic_effects(new_img) |
|
|
|
|
|
|
|
|
frame_path = f"{output_dir}/frame_{os.path.basename(image_path)}_{i}.png" |
|
|
new_img.save(frame_path) |
|
|
frames.append(frame_path) |
|
|
|
|
|
|
|
|
self.frame_cache[cache_key] = frames |
|
|
return frames |
|
|
|
|
|
def add_ken_burns_effect(self, image_path, num_frames=None, output_dir="temp"): |
|
|
"""Add a Ken Burns effect (combination of pan and zoom) with cinematic effects""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
|
|
|
cache_key = f"kenburns_{image_path}_{num_frames}_{self.aspect_ratio}" |
|
|
if cache_key in self.frame_cache: |
|
|
return self.frame_cache[cache_key] |
|
|
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
|
|
|
|
|
img = Image.open(image_path) |
|
|
|
|
|
|
|
|
frames = [] |
|
|
|
|
|
|
|
|
import random |
|
|
if self.aspect_ratio == "16:9": |
|
|
|
|
|
direction = random.choice(["right", "left"]) |
|
|
elif self.aspect_ratio == "9:16": |
|
|
|
|
|
direction = random.choice(["up", "down"]) |
|
|
else: |
|
|
|
|
|
direction = random.choice(["right", "left", "up", "down"]) |
|
|
|
|
|
|
|
|
if direction == "right": |
|
|
x_shifts = np.linspace(0, img.width * 0.05, num_frames) |
|
|
y_shifts = np.zeros(num_frames) |
|
|
elif direction == "left": |
|
|
x_shifts = np.linspace(0, -img.width * 0.05, num_frames) |
|
|
y_shifts = np.zeros(num_frames) |
|
|
elif direction == "down": |
|
|
x_shifts = np.zeros(num_frames) |
|
|
y_shifts = np.linspace(0, img.height * 0.05, num_frames) |
|
|
elif direction == "up": |
|
|
x_shifts = np.zeros(num_frames) |
|
|
y_shifts = np.linspace(0, -img.height * 0.05, num_frames) |
|
|
|
|
|
|
|
|
zoom_factors = np.linspace(1.0, 1.05, num_frames) |
|
|
|
|
|
for i in range(num_frames): |
|
|
|
|
|
size = (int(img.width * zoom_factors[i]), int(img.height * zoom_factors[i])) |
|
|
zoomed_img = img.resize(size, Image.LANCZOS) |
|
|
|
|
|
|
|
|
new_img = Image.new("RGB", (img.width, img.height)) |
|
|
|
|
|
|
|
|
left = (img.width - zoomed_img.width) // 2 + int(x_shifts[i]) |
|
|
top = (img.height - zoomed_img.height) // 2 + int(y_shifts[i]) |
|
|
|
|
|
|
|
|
new_img.paste(zoomed_img, (left, top)) |
|
|
|
|
|
|
|
|
new_img = self.apply_cinematic_effects(new_img) |
|
|
|
|
|
|
|
|
frame_path = f"{output_dir}/frame_{os.path.basename(image_path)}_{i}.png" |
|
|
new_img.save(frame_path) |
|
|
frames.append(frame_path) |
|
|
|
|
|
|
|
|
self.frame_cache[cache_key] = frames |
|
|
return frames |
|
|
|
|
|
def animate_single_image(self, img_path, animation_type="random", output_dir="temp", num_frames=None): |
|
|
"""Animate a single image with cinematic effects""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
|
|
|
animation_types = ["zoom", "pan_right", "pan_left", "fade_in", "ken_burns"] |
|
|
|
|
|
|
|
|
if self.aspect_ratio == "16:9": |
|
|
|
|
|
animation_types = ["zoom", "pan_left", "pan_right", "ken_burns", "fade_in"] |
|
|
elif self.aspect_ratio == "9:16": |
|
|
|
|
|
animation_types = ["zoom", "ken_burns", "fade_in", "pan_up", "pan_down"] |
|
|
|
|
|
if animation_type == "random": |
|
|
|
|
|
import random |
|
|
random.seed(hash(img_path)) |
|
|
chosen_type = random.choice(animation_types) |
|
|
else: |
|
|
chosen_type = animation_type |
|
|
|
|
|
|
|
|
if chosen_type == "ken_burns": |
|
|
frames = self.add_ken_burns_effect(img_path, num_frames=num_frames, output_dir=output_dir) |
|
|
elif chosen_type.startswith("pan"): |
|
|
direction = chosen_type.split("_")[1] if "_" in chosen_type else "right" |
|
|
frames = self.add_pan_animation(img_path, num_frames=num_frames, direction=direction, output_dir=output_dir) |
|
|
elif chosen_type.startswith("fade"): |
|
|
fade_type = chosen_type.split("_")[1] if "_" in chosen_type else "in" |
|
|
frames = self.add_fade_animation(img_path, num_frames=num_frames, fade_type=fade_type, output_dir=output_dir) |
|
|
else: |
|
|
frames = self.add_zoom_animation(img_path, num_frames=num_frames, output_dir=output_dir) |
|
|
|
|
|
return frames |
|
|
|
|
|
def animate_images(self, image_paths, animation_type="random", output_dir="temp", |
|
|
progress_callback=None, parallel=False, max_workers=4, batch_size=2, num_frames=None): |
|
|
"""Add animations to a list of images with parallel processing and batching""" |
|
|
if num_frames is None: |
|
|
num_frames = self.frames_per_animation |
|
|
|
|
|
all_animated_frames = [] |
|
|
|
|
|
if parallel and len(image_paths) > 1: |
|
|
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor: |
|
|
|
|
|
animate_func = partial(self.animate_single_image, |
|
|
animation_type=animation_type, |
|
|
output_dir=output_dir, |
|
|
num_frames=num_frames) |
|
|
|
|
|
|
|
|
if progress_callback: |
|
|
progress_callback("Animating images in parallel...") |
|
|
|
|
|
|
|
|
all_animated_frames = list(executor.map(animate_func, image_paths)) |
|
|
else: |
|
|
|
|
|
for i in range(0, len(image_paths), batch_size): |
|
|
batch = image_paths[i:i+batch_size] |
|
|
|
|
|
if progress_callback: |
|
|
progress_callback(f"Animating batch {i//batch_size + 1}/{(len(image_paths) + batch_size - 1)//batch_size}...") |
|
|
|
|
|
batch_frames = [] |
|
|
for img_path in batch: |
|
|
frames = self.animate_single_image(img_path, animation_type, output_dir, num_frames) |
|
|
batch_frames.append(frames) |
|
|
|
|
|
all_animated_frames.extend(batch_frames) |
|
|
|
|
|
return all_animated_frames |
|
|
|
|
|
def clear_cache(self): |
|
|
"""Clear the animation frame cache""" |
|
|
self.frame_cache = {} |
|
|
return True |
|
|
|