|
|
import torch
|
|
|
import numpy as np
|
|
|
from PIL import Image, ImageDraw
|
|
|
import cv2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GridAnimator:
|
|
|
CATEGORY = "animation/generators"
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
|
RETURN_NAMES = ("images",)
|
|
|
FUNCTION = "generate_animation"
|
|
|
OUTPUT_NODE = False
|
|
|
|
|
|
@staticmethod
|
|
|
def _project_3d_to_2d(points_3d, rotation_yaw, rotation_pitch, focal_length, canvas_size):
|
|
|
w, h = canvas_size
|
|
|
yaw, pitch = np.deg2rad(rotation_yaw), np.deg2rad(rotation_pitch)
|
|
|
R_yaw = np.array([[np.cos(yaw), 0, np.sin(yaw)], [0, 1, 0], [-np.sin(yaw), 0, np.cos(yaw)]])
|
|
|
R_pitch = np.array([[1, 0, 0], [0, np.cos(pitch), -np.sin(pitch)], [0, np.sin(pitch), np.cos(pitch)]])
|
|
|
R = np.dot(R_pitch, R_yaw)
|
|
|
rotated_points = np.dot(points_3d, R.T)
|
|
|
|
|
|
projected_points = []
|
|
|
for p in rotated_points:
|
|
|
x, y, z = p
|
|
|
scale_factor = focal_length / (focal_length + z) if (focal_length + z) != 0 else focal_length
|
|
|
projected_points.append((x * scale_factor, y * scale_factor))
|
|
|
|
|
|
projected_points = np.float32(projected_points)
|
|
|
projected_points[:, 0] += w / 2
|
|
|
projected_points[:, 1] += h / 2
|
|
|
return projected_points
|
|
|
|
|
|
@classmethod
|
|
|
def INPUT_TYPES(cls):
|
|
|
|
|
|
return {
|
|
|
"required": {
|
|
|
"width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
|
|
|
"height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
|
|
|
"num_frames": ("INT", {"default": 73, "min": 1, "max": 1000, "label": "Duración (frames)"}),
|
|
|
"rows": ("INT", {"default": 1, "min": 1, "max": 50, "label": "Filas"}),
|
|
|
"columns": ("INT", {"default": 1, "min": 1, "max": 50, "label": "Columnas"}),
|
|
|
"square_size": ("INT", {"default": 200, "min": 10, "max": 1024, "label": "Tamaño del lado"}),
|
|
|
"spacing": ("INT", {"default": 24, "min": 0, "max": 1024, "step": 1, "label": "Espaciado"}),
|
|
|
"line_thickness": ("INT", {"default": 4, "min": 1, "max": 50, "label": "Grosor de línea"}),
|
|
|
"color": ("STRING", {"default": "#FF0033", "label": "Color (Hex)"}),
|
|
|
"focal_length": ("INT", {"default": 500, "min": 50, "max": 5000, "step": 10, "label": "Focal Length (Perspective)"}),
|
|
|
"start_yaw": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Inicio Yaw (grados)"}),
|
|
|
"end_yaw": ("FLOAT", {"default": 45.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Fin Yaw (grados)"}),
|
|
|
"start_pitch": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Inicio Pitch (grados)"}),
|
|
|
"end_pitch": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Fin Pitch (grados)"}),
|
|
|
"start_zoom": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.05}),
|
|
|
"end_zoom": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.05}),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
def generate_animation(self, width, height, num_frames, rows, columns, square_size, spacing, line_thickness, color, focal_length, start_yaw, end_yaw, start_pitch, end_pitch, start_zoom, end_zoom):
|
|
|
|
|
|
|
|
|
|
|
|
canvas_size = max(width, height) * 2
|
|
|
|
|
|
|
|
|
grid_w = (columns * square_size) + max(0, columns - 1) * spacing
|
|
|
grid_h = (rows * square_size) + max(0, rows - 1) * spacing
|
|
|
|
|
|
|
|
|
grid_canvas_pil = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
|
draw = ImageDraw.Draw(grid_canvas_pil)
|
|
|
start_x = (canvas_size - grid_w) // 2
|
|
|
start_y = (canvas_size - grid_h) // 2
|
|
|
|
|
|
for r in range(rows):
|
|
|
for c in range(columns):
|
|
|
x0, y0 = start_x + c * (square_size + spacing), start_y + r * (square_size + spacing)
|
|
|
draw.rectangle([x0, y0, x0 + square_size, y0 + square_size], outline=color, width=line_thickness)
|
|
|
|
|
|
grid_canvas_cv = cv2.cvtColor(np.array(grid_canvas_pil), cv2.COLOR_RGBA2BGRA)
|
|
|
|
|
|
|
|
|
|
|
|
points_3d = np.float32([[-grid_w/2, -grid_h/2, 0], [grid_w/2, -grid_h/2, 0], [grid_w/2, grid_h/2, 0], [-grid_w/2, grid_h/2, 0]])
|
|
|
|
|
|
src_pts = np.float32([[start_x, start_y], [start_x + grid_w, start_y], [start_x + grid_w, start_y + grid_h], [start_x, start_y + grid_h]])
|
|
|
|
|
|
output_frames = []
|
|
|
for i in range(num_frames):
|
|
|
progress = i / (num_frames - 1) if num_frames > 1 else 0.0
|
|
|
current_yaw = start_yaw + (end_yaw - start_yaw) * progress
|
|
|
current_pitch = start_pitch + (end_pitch - start_pitch) * progress
|
|
|
current_zoom = start_zoom + (end_zoom - start_zoom) * progress
|
|
|
|
|
|
|
|
|
dst_pts = self._project_3d_to_2d(points_3d, current_yaw, current_pitch, focal_length, (canvas_size, canvas_size))
|
|
|
|
|
|
|
|
|
matrix = cv2.getPerspectiveTransform(src_pts, dst_pts)
|
|
|
transformed_cv = cv2.warpPerspective(grid_canvas_cv, matrix, (canvas_size, canvas_size), flags=cv2.INTER_LANCZOS4)
|
|
|
|
|
|
|
|
|
|
|
|
alpha_channel = transformed_cv[:, :, 3]
|
|
|
contours, _ = cv2.findContours(alpha_channel, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
|
|
final_pil = None
|
|
|
if contours:
|
|
|
|
|
|
all_points = np.concatenate(contours)
|
|
|
x, y, w, h = cv2.boundingRect(all_points)
|
|
|
|
|
|
|
|
|
cropped_cv = transformed_cv[y:y+h, x:x+w]
|
|
|
|
|
|
|
|
|
zoomed_w, zoomed_h = int(w * current_zoom), int(h * current_zoom)
|
|
|
if zoomed_w > 0 and zoomed_h > 0:
|
|
|
zoomed_cv = cv2.resize(cropped_cv, (zoomed_w, zoomed_h), interpolation=cv2.INTER_LANCZOS4)
|
|
|
final_pil = Image.fromarray(cv2.cvtColor(zoomed_cv, cv2.COLOR_BGRA2RGBA))
|
|
|
|
|
|
|
|
|
final_frame = Image.new('RGB', (width, height), 'white')
|
|
|
if final_pil:
|
|
|
paste_x = (width - final_pil.width) // 2
|
|
|
paste_y = (height - final_pil.height) // 2
|
|
|
final_frame.paste(final_pil, (paste_x, paste_y), final_pil)
|
|
|
|
|
|
np_frame = np.array(final_frame).astype(np.float32) / 255.0
|
|
|
output_frames.append(np_frame)
|
|
|
|
|
|
frames_np = np.stack(output_frames, axis=0)
|
|
|
frames_tensor = torch.from_numpy(frames_np)
|
|
|
return (frames_tensor,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {"GridAnimator": GridAnimator}
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {"GridAnimator": "Grid Animator 3D 🔳"} |