Stkzzzz222 commited on
Commit
eeaca5a
verified
1 Parent(s): 9cac472

Upload gridanimator.py

Browse files
Files changed (1) hide show
  1. gridanimator.py +145 -0
gridanimator.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from PIL import Image, ImageDraw
4
+ import cv2
5
+
6
+ # ====================================================================================================
7
+ # --- Grid Animator Node ---
8
+ # By empoweringtheuser @ civitai
9
+ #
10
+ # Versi贸n 11: Anti-Clipping.
11
+ # Se implementa un sistema de lienzo de trabajo de gran tama帽o para evitar que la
12
+ # transformaci贸n de perspectiva recorte partes de la imagen. Esto soluciona el problema
13
+ # de las "l铆neas que desaparecen" de forma definitiva.
14
+ # ====================================================================================================
15
+
16
+ class GridAnimator:
17
+ CATEGORY = "animation/generators"
18
+ RETURN_TYPES = ("IMAGE",)
19
+ RETURN_NAMES = ("images",)
20
+ FUNCTION = "generate_animation"
21
+ OUTPUT_NODE = False
22
+
23
+ @staticmethod
24
+ def _project_3d_to_2d(points_3d, rotation_yaw, rotation_pitch, focal_length, canvas_size):
25
+ w, h = canvas_size
26
+ yaw, pitch = np.deg2rad(rotation_yaw), np.deg2rad(rotation_pitch)
27
+ R_yaw = np.array([[np.cos(yaw), 0, np.sin(yaw)], [0, 1, 0], [-np.sin(yaw), 0, np.cos(yaw)]])
28
+ R_pitch = np.array([[1, 0, 0], [0, np.cos(pitch), -np.sin(pitch)], [0, np.sin(pitch), np.cos(pitch)]])
29
+ R = np.dot(R_pitch, R_yaw)
30
+ rotated_points = np.dot(points_3d, R.T)
31
+
32
+ projected_points = []
33
+ for p in rotated_points:
34
+ x, y, z = p
35
+ scale_factor = focal_length / (focal_length + z) if (focal_length + z) != 0 else focal_length
36
+ projected_points.append((x * scale_factor, y * scale_factor))
37
+
38
+ projected_points = np.float32(projected_points)
39
+ projected_points[:, 0] += w / 2
40
+ projected_points[:, 1] += h / 2
41
+ return projected_points
42
+
43
+ @classmethod
44
+ def INPUT_TYPES(cls):
45
+ # Los inputs no cambian.
46
+ return {
47
+ "required": {
48
+ "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
49
+ "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
50
+ "num_frames": ("INT", {"default": 73, "min": 1, "max": 1000, "label": "Duraci贸n (frames)"}),
51
+ "rows": ("INT", {"default": 1, "min": 1, "max": 50, "label": "Filas"}),
52
+ "columns": ("INT", {"default": 1, "min": 1, "max": 50, "label": "Columnas"}),
53
+ "square_size": ("INT", {"default": 200, "min": 10, "max": 1024, "label": "Tama帽o del lado"}),
54
+ "spacing": ("INT", {"default": 24, "min": 0, "max": 1024, "step": 1, "label": "Espaciado"}),
55
+ "line_thickness": ("INT", {"default": 4, "min": 1, "max": 50, "label": "Grosor de l铆nea"}),
56
+ "color": ("STRING", {"default": "#FF0033", "label": "Color (Hex)"}),
57
+ "focal_length": ("INT", {"default": 500, "min": 50, "max": 5000, "step": 10, "label": "Focal Length (Perspective)"}),
58
+ "start_yaw": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Inicio Yaw (grados)"}),
59
+ "end_yaw": ("FLOAT", {"default": 45.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Fin Yaw (grados)"}),
60
+ "start_pitch": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Inicio Pitch (grados)"}),
61
+ "end_pitch": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0, "label": "Fin Pitch (grados)"}),
62
+ "start_zoom": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.05}),
63
+ "end_zoom": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.05}),
64
+ }
65
+ }
66
+
67
+ 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):
68
+
69
+ # --- 1. Crear un GRAN Lienzo de Trabajo para evitar el recorte ---
70
+ # Usamos un factor de 2, que deber铆a ser suficiente para rotaciones extremas.
71
+ canvas_size = max(width, height) * 2
72
+
73
+ # Calculamos el tama帽o de la cuadr铆cula que dibujaremos
74
+ grid_w = (columns * square_size) + max(0, columns - 1) * spacing
75
+ grid_h = (rows * square_size) + max(0, rows - 1) * spacing
76
+
77
+ # Dibujamos la cuadr铆cula EN EL CENTRO del gran lienzo de trabajo
78
+ grid_canvas_pil = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
79
+ draw = ImageDraw.Draw(grid_canvas_pil)
80
+ start_x = (canvas_size - grid_w) // 2
81
+ start_y = (canvas_size - grid_h) // 2
82
+
83
+ for r in range(rows):
84
+ for c in range(columns):
85
+ x0, y0 = start_x + c * (square_size + spacing), start_y + r * (square_size + spacing)
86
+ draw.rectangle([x0, y0, x0 + square_size, y0 + square_size], outline=color, width=line_thickness)
87
+
88
+ grid_canvas_cv = cv2.cvtColor(np.array(grid_canvas_pil), cv2.COLOR_RGBA2BGRA)
89
+
90
+ # --- 2. Preparar Puntos para la Transformaci贸n 3D ---
91
+ # Los puntos 3D siguen siendo del tama帽o de la cuadr铆cula, no del lienzo
92
+ 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]])
93
+ # Los puntos de origen son las esquinas del DIBUJO en el lienzo grande
94
+ 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]])
95
+
96
+ output_frames = []
97
+ for i in range(num_frames):
98
+ progress = i / (num_frames - 1) if num_frames > 1 else 0.0
99
+ current_yaw = start_yaw + (end_yaw - start_yaw) * progress
100
+ current_pitch = start_pitch + (end_pitch - start_pitch) * progress
101
+ current_zoom = start_zoom + (end_zoom - start_zoom) * progress
102
+
103
+ # Proyectamos los puntos 3D en nuestro lienzo grande
104
+ dst_pts = self._project_3d_to_2d(points_3d, current_yaw, current_pitch, focal_length, (canvas_size, canvas_size))
105
+
106
+ # Realizamos la transformaci贸n DENTRO del lienzo grande
107
+ matrix = cv2.getPerspectiveTransform(src_pts, dst_pts)
108
+ transformed_cv = cv2.warpPerspective(grid_canvas_cv, matrix, (canvas_size, canvas_size), flags=cv2.INTER_LANCZOS4)
109
+
110
+ # --- 3. Recorte Autom谩tico, Zoom y Composici贸n Final ---
111
+ # Encontramos el contenido no transparente en el lienzo grande
112
+ alpha_channel = transformed_cv[:, :, 3]
113
+ contours, _ = cv2.findContours(alpha_channel, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
114
+
115
+ final_pil = None
116
+ if contours:
117
+ # Unimos todos los contornos para obtener un bounding box general
118
+ all_points = np.concatenate(contours)
119
+ x, y, w, h = cv2.boundingRect(all_points)
120
+
121
+ # Recortamos la imagen transformada al contenido exacto
122
+ cropped_cv = transformed_cv[y:y+h, x:x+w]
123
+
124
+ # Aplicamos el zoom
125
+ zoomed_w, zoomed_h = int(w * current_zoom), int(h * current_zoom)
126
+ if zoomed_w > 0 and zoomed_h > 0:
127
+ zoomed_cv = cv2.resize(cropped_cv, (zoomed_w, zoomed_h), interpolation=cv2.INTER_LANCZOS4)
128
+ final_pil = Image.fromarray(cv2.cvtColor(zoomed_cv, cv2.COLOR_BGRA2RGBA))
129
+
130
+ # Pegamos la imagen final (si existe) en el lienzo de salida del usuario
131
+ final_frame = Image.new('RGB', (width, height), 'white')
132
+ if final_pil:
133
+ paste_x = (width - final_pil.width) // 2
134
+ paste_y = (height - final_pil.height) // 2
135
+ final_frame.paste(final_pil, (paste_x, paste_y), final_pil)
136
+
137
+ np_frame = np.array(final_frame).astype(np.float32) / 255.0
138
+ output_frames.append(np_frame)
139
+
140
+ frames_np = np.stack(output_frames, axis=0)
141
+ frames_tensor = torch.from_numpy(frames_np)
142
+ return (frames_tensor,)
143
+
144
+ NODE_CLASS_MAPPINGS = {"GridAnimator": GridAnimator}
145
+ NODE_DISPLAY_NAME_MAPPINGS = {"GridAnimator": "Grid Animator 3D 馃敵"}