Files
lazniak a5e6d4350e Update liquid_time_node.py
limit of max frames extended to higher value
2024-11-14 19:10:41 +01:00

355 lines
16 KiB
Python

# liquid_time_node.py
import os
import torch
import numpy as np
import urllib.request
from tqdm import tqdm
import math
class DownloadProgressBar(tqdm):
def update_to(self, b=1, bsize=1, tsize=None):
if tsize is not None:
self.total = tsize
self.update(b * bsize - self.n)
class LiquidTimeNode:
def __init__(self):
self.model = None
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.precision = torch.float16 if torch.cuda.is_available() else torch.float32
print(f"[LiquidTime] Initializing with device: {self.device}, precision: {self.precision}")
self.model_urls = {
'fp16': 'https://github.com/dajes/frame-interpolation-pytorch/releases/download/v1.0.2/film_net_fp16.pt',
'fp32': 'https://github.com/dajes/frame-interpolation-pytorch/releases/download/v1.0.2/film_net_fp32.pt'
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"target_frames": ("INT", {"default": 60, "min": 2, "max": 100000}),
"start_curve": ("INT", {"default": 0, "min": -100, "max": 100}),
"middle_curve": ("INT", {"default": 0, "min": -100, "max": 100}),
"middle_moment": ("INT", {"default": 50, "min": 0, "max": 100}),
"end_curve": ("INT", {"default": 0, "min": -100, "max": 100})
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "interpolate_frames"
CATEGORY = "animation/LiquidTime"
def download_model(self, model_path):
"""Download model with progress bar"""
model_type = 'fp16' if self.precision == torch.float16 else 'fp32'
url = self.model_urls[model_type]
print(f"\n[LiquidTime] Creating model directory...")
os.makedirs(os.path.dirname(model_path), exist_ok=True)
print(f"[LiquidTime] Starting download of {model_type} model from {url}")
try:
with DownloadProgressBar(unit='B', unit_scale=True, miniters=1,
desc=f"[LiquidTime] Downloading {model_type} model",
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]') as t:
urllib.request.urlretrieve(
url,
filename=model_path,
reporthook=t.update_to
)
print(f"[LiquidTime] Successfully downloaded {model_type} model to {model_path}")
except Exception as e:
print(f"[LiquidTime] Error downloading model: {str(e)}")
if os.path.exists(model_path):
os.remove(model_path)
raise
def get_model_path(self):
"""Get path to model and ensure it exists"""
base_path = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join(base_path, 'models')
os.makedirs(models_dir, exist_ok=True)
model_type = 'fp16' if self.precision == torch.float16 else 'fp32'
model_path = os.path.join(models_dir, f'film_net_{model_type}.pt')
if not os.path.exists(model_path):
print(f"[LiquidTime] Model not found at {model_path}")
self.download_model(model_path)
return model_path
def load_model(self):
if self.model is None:
print("[LiquidTime] Loading model...")
model_path = self.get_model_path()
try:
self.model = torch.jit.load(model_path, map_location='cpu')
self.model.eval().to(device=self.device, dtype=self.precision)
print(f"[LiquidTime] Successfully loaded model with precision {self.precision} on {self.device}")
except Exception as e:
print(f"[LiquidTime] Error loading model: {str(e)}")
raise RuntimeError(f"Error loading model: {str(e)}")
def sample_frames(self, source_frames, target_count):
"""
Wybiera target_count klatek z source_frames w równych odstępach czasowych.
"""
if target_count >= len(source_frames):
return source_frames
print(f"[LiquidTime] Reducing frames from {len(source_frames)} to {target_count}")
# Obliczamy indeksy klatek do zachowania
indices = np.linspace(0, len(source_frames) - 1, target_count)
indices = np.round(indices).astype(int)
# Wybieramy klatki
selected_frames = [source_frames[i] for i in indices]
print(f"[LiquidTime] Selected frame indices: {indices}")
return selected_frames
def create_time_mapping(self, num_points, start_curve, middle_curve, middle_moment, end_curve):
"""
Ulepszone mapowanie czasu z mocniejszym wpływem parametrów.
"""
print(f"[LiquidTime] Creating enhanced time mapping with curves:")
print(f" - Start curve: {start_curve}")
print(f" - Middle curve: {middle_curve} at {middle_moment}%")
print(f" - End curve: {end_curve}")
# Konwertujemy middle_moment z procentów na zakres 0-1
middle_t = middle_moment / 100.0
# Generujemy bazowe punkty czasowe
t_values = np.linspace(0, 1, num_points)
# Wzmocnienie efektu krzywych
curve_power = 2.5 # Zwiększa intensywność efektu
def enhanced_curve_influence(value):
"""Wzmacnia wpływ krzywej zachowując znak"""
return np.sign(value) * abs(value / 100.0) ** 0.75 * curve_power
# Konwertujemy wartości krzywych
start_influence = enhanced_curve_influence(start_curve)
middle_influence = enhanced_curve_influence(middle_curve)
end_influence = enhanced_curve_influence(end_curve)
def transform_time(t):
# Bazowa transformacja dla middle_moment (aktywna nawet bez middle_curve)
middle_shift = (middle_t - 0.5) * 0.5 # Maksymalne przesunięcie o 25% w każdą stronę
t_shifted = t + middle_shift * np.sin(np.pi * t) # Sinusoidalne przesunięcie
# Obliczamy wpływy poszczególnych krzywych
start_factor = np.exp(-4 * t_shifted) * start_influence
end_factor = np.exp(-4 * (1 - t_shifted)) * end_influence
# Wpływ middle_curve z gaussowskim rozkładem wokół middle_moment
middle_width = 0.3 # Szerszy zakres wpływu
middle_gaussian = np.exp(-((t_shifted - middle_t) ** 2) / (2 * middle_width ** 2))
middle_factor = middle_gaussian * middle_influence
# Łączymy wszystkie wpływy
combined_shift = (start_factor + middle_factor + end_factor)
# Aplikujemy przesunięcie z zachowaniem granic
transformed_t = t_shifted + combined_shift
# Zapewniamy monotoniczność (zawsze do przodu)
transformed_t = t + (transformed_t - t) * 0.7 # 0.7 to siła efektu
return transformed_t
# Aplikujemy transformację do wszystkich punktów
transformed_points = np.array([transform_time(t) for t in t_values])
# Normalizujemy do zakresu [0, 1] zachowując proporcje
transformed_points = (transformed_points - transformed_points.min()) / \
(transformed_points.max() - transformed_points.min())
# Wygładzamy przejścia z zachowaniem kształtu
def smooth_points(points, window=5):
smoothed = np.copy(points)
half_window = window // 2
for i in range(half_window, len(points) - half_window):
# Ważone średnia z większym wpływem punktów centralnych
weights = np.exp(-0.5 * (np.arange(window) - half_window) ** 2)
window_slice = points[i - half_window:i + half_window + 1]
smoothed[i] = np.sum(window_slice * weights) / np.sum(weights)
# Zachowujemy początek i koniec
smoothed[:half_window] = points[:half_window]
smoothed[-half_window:] = points[-half_window:]
return smoothed
smoothed_points = smooth_points(transformed_points)
# Końcowa normalizacja
smoothed_points = (smoothed_points - smoothed_points[0]) / \
(smoothed_points[-1] - smoothed_points[0])
# Debug info
print(f"[LiquidTime] Time mapping statistics:")
print(f" - Average speed: {1/(len(smoothed_points)-1):.4f}")
print(f" - Max speed: {np.max(np.diff(smoothed_points)):.4f}")
print(f" - Min speed: {np.min(np.diff(smoothed_points)):.4f}")
print(f" - Middle point actual position: {np.argmin(np.abs(smoothed_points - 0.5))/len(smoothed_points)*100:.1f}%")
return smoothed_points
def interpolate_pair(self, img1, img2, time_step):
"""Interpolate between two frames"""
# Convert images to torch tensors in the correct format
img1_tensor = torch.from_numpy(img1).permute(2, 0, 1).unsqueeze(0)
img2_tensor = torch.from_numpy(img2).permute(2, 0, 1).unsqueeze(0)
# Move to device and cast to correct precision
img1_tensor = img1_tensor.to(device=self.device, dtype=self.precision)
img2_tensor = img2_tensor.to(device=self.device, dtype=self.precision)
dt = img1_tensor.new_full((1, 1), float(time_step))
with torch.no_grad():
try:
interpolated = self.model(img1_tensor, img2_tensor, dt)
return interpolated.squeeze(0).permute(1, 2, 0).cpu().numpy()
except RuntimeError as e:
if "out of memory" in str(e):
torch.cuda.empty_cache()
print("[LiquidTime] GPU out of memory error occurred!")
raise RuntimeError("GPU out of memory. Try reducing image size or using CPU mode.")
raise
def interpolate_frames(self, images, target_frames, start_curve=0, middle_curve=0, middle_moment=50, end_curve=0):
print(f"\n[LiquidTime] Starting interpolation process...")
print(f"[LiquidTime] Input: {len(images)} frames, Target: {target_frames} frames")
print(f"[LiquidTime] Curve parameters: start={start_curve}, middle={middle_curve} at {middle_moment}%, end={end_curve}")
# Konwertujemy input na numpy jeśli potrzeba
if torch.is_tensor(images):
images = images.cpu().numpy()
num_source_frames = len(images)
# Obsługa redukcji klatek
if target_frames < num_source_frames:
print(f"[LiquidTime] Reducing frames from {num_source_frames} to {target_frames}")
images = self.sample_frames(images, target_frames)
return (torch.from_numpy(np.stack(images)),)
# Obsługa równej liczby klatek
if target_frames == num_source_frames:
print(f"[LiquidTime] Target frames equals source frames, returning without changes")
return (torch.from_numpy(np.stack(images)),)
# Interpolacja
self.load_model()
# Obliczamy punkty czasowe używając krzywej czasowej
time_points = self.create_time_mapping(target_frames, start_curve, middle_curve, middle_moment, end_curve)
# Obliczamy ile klatek przypada na każdy segment źródłowy
source_segments = np.linspace(0, 1, num_source_frames)
output_frames = []
try:
# Main interpolation loop with progress bar
segment_progress = 0
last_progress_update = 0
progress_update_interval = max(1, target_frames // 100) # Aktualizacja co 1% postępu
with tqdm(total=target_frames,
desc="[LiquidTime] Generating frames",
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]') as pbar:
current_source_idx = 0
for frame_idx, t in enumerate(time_points):
# Znajdujemy odpowiedni segment źródłowy
while (current_source_idx < len(source_segments) - 1 and
t > source_segments[current_source_idx + 1]):
current_source_idx += 1
segment_progress = 0
if current_source_idx >= len(images) - 1:
current_source_idx = len(images) - 2
# Obliczamy lokalny time_step w aktualnym segmencie
local_segment_start = source_segments[current_source_idx]
local_segment_end = source_segments[current_source_idx + 1]
local_t = (t - local_segment_start) / (local_segment_end - local_segment_start)
local_t = max(0, min(1, local_t)) # Clamp to [0, 1]
# Obliczamy procent ukończenia aktualnego segmentu
segment_progress = local_t * 100
# Aktualizujemy informacje o postępie
if frame_idx - last_progress_update >= progress_update_interval:
overall_progress = (frame_idx / target_frames) * 100
print(f"[LiquidTime] Processing: Segment {current_source_idx + 1}/{len(images) - 1} "
f"({segment_progress:.1f}% complete) | "
f"Overall: {overall_progress:.1f}% | "
f"Frames: {frame_idx}/{target_frames}")
last_progress_update = frame_idx
# Interpolujemy klatkę
interpolated = self.interpolate_pair(
images[current_source_idx],
images[current_source_idx + 1],
local_t
)
output_frames.append(interpolated)
pbar.update(1)
# Informacja o rozpoczęciu nowego segmentu
if segment_progress == 0:
print(f"[LiquidTime] Starting new segment: {current_source_idx + 1}/{len(images) - 1}")
# Okresowe czyszczenie pamięci GPU
if frame_idx % 100 == 0:
torch.cuda.empty_cache()
final_frame_count = len(output_frames)
print(f"[LiquidTime] Generated frames breakdown:")
print(f" - Initial source frames: {num_source_frames}")
print(f" - Total output frames: {final_frame_count}")
print(f" - Target frames: {target_frames}")
if final_frame_count != target_frames:
print(f"[LiquidTime] WARNING: Frame count mismatch!")
print(f" Expected: {target_frames}, Got: {final_frame_count}")
else:
print(f"[LiquidTime] Frame count matches target exactly!")
# Convert back to torch tensor for ComfyUI compatibility
result = torch.from_numpy(np.stack(output_frames))
print(f"[LiquidTime] Output shape: {result.shape}")
print(f"[LiquidTime] Process completed successfully!")
# Dodajemy informacje o autorze i wsparciu
print("\n" + "="*50)
print("LiquidTime - by PabloGFX")
print("YouTube: https://youtube.com/@lazniak")
print("For support: https://buymeacoffee.com/eyb8tkx3to")
print("="*50 + "\n")
return (result,)
except Exception as e:
print(f"[LiquidTime] Error during interpolation: {str(e)}")
raise
NODE_CLASS_MAPPINGS = {
"LiquidTime": LiquidTimeNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LiquidTime": "LiquidTime - by PabloGFX"
}