Complete package with Core, Creative, Vintage, Deformation, Light Effects, and Geometric categories
169 lines
6.9 KiB
Python
169 lines
6.9 KiB
Python
import numpy as np
|
|
import torch
|
|
import cv2
|
|
from scipy.spatial import Delaunay
|
|
|
|
class TriangulateNode:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"num_points": ("INT", {"default": 500, "min": 50, "max": 2000, "step": 50}),
|
|
"edge_threshold": ("FLOAT", {"default": 0.3, "min": 0.1, "max": 1.0, "step": 0.05}),
|
|
"color_mode": (["average", "dominant", "gradient"], {"default": "average"}),
|
|
},
|
|
"optional": {
|
|
"point_distribution": (["random", "edge_based", "grid"], {"default": "edge_based"}),
|
|
"triangle_outline": ("BOOLEAN", {"default": False}),
|
|
"outline_thickness": ("INT", {"default": 1, "min": 1, "max": 5, "step": 1}),
|
|
"smoothing": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "apply_triangulate"
|
|
CATEGORY = "Image Effects"
|
|
|
|
def apply_triangulate(self, image, num_points, edge_threshold, color_mode,
|
|
point_distribution="edge_based", triangle_outline=False,
|
|
outline_thickness=1, smoothing=0.0):
|
|
if len(image.shape) == 4:
|
|
img_tensor = image[0]
|
|
else:
|
|
img_tensor = image
|
|
|
|
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
|
|
h, w, c = img_np.shape
|
|
|
|
# Générer les points selon la distribution
|
|
points = self._generate_points(img_np, num_points, point_distribution, edge_threshold)
|
|
|
|
# Triangulation de Delaunay
|
|
tri = Delaunay(points)
|
|
|
|
# Créer l'image triangulée
|
|
result = self._create_triangulated_image(img_np, points, tri.simplices,
|
|
color_mode, triangle_outline, outline_thickness)
|
|
|
|
# Appliquer le lissage si demandé
|
|
if smoothing > 0:
|
|
result = self._apply_smoothing(result, smoothing)
|
|
|
|
result_tensor = torch.from_numpy(result.astype(np.float32) / 255.0).unsqueeze(0)
|
|
return (result_tensor,)
|
|
|
|
def _generate_points(self, image, num_points, distribution, edge_threshold):
|
|
"""Générer les points pour la triangulation"""
|
|
h, w = image.shape[:2]
|
|
points = []
|
|
|
|
# Ajouter les coins
|
|
points.extend([[0, 0], [w-1, 0], [w-1, h-1], [0, h-1]])
|
|
|
|
if distribution == "random":
|
|
# Distribution aléatoire
|
|
for _ in range(num_points - 4):
|
|
x = np.random.randint(0, w)
|
|
y = np.random.randint(0, h)
|
|
points.append([x, y])
|
|
|
|
elif distribution == "edge_based":
|
|
# Basé sur les contours
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
|
edges = cv2.Canny(gray, int(edge_threshold * 100), int(edge_threshold * 200))
|
|
|
|
# Points sur les contours
|
|
edge_points = np.column_stack(np.where(edges > 0))
|
|
if len(edge_points) > 0:
|
|
# Échantillonner les points de contour
|
|
indices = np.random.choice(len(edge_points),
|
|
min(num_points // 2, len(edge_points)),
|
|
replace=False)
|
|
for idx in indices:
|
|
y, x = edge_points[idx]
|
|
points.append([x, y])
|
|
|
|
# Points aléatoires pour compléter
|
|
remaining = num_points - len(points)
|
|
for _ in range(remaining):
|
|
x = np.random.randint(0, w)
|
|
y = np.random.randint(0, h)
|
|
points.append([x, y])
|
|
|
|
elif distribution == "grid":
|
|
# Distribution en grille avec variation
|
|
grid_size = int(np.sqrt(num_points))
|
|
for i in range(grid_size):
|
|
for j in range(grid_size):
|
|
if len(points) >= num_points:
|
|
break
|
|
x = int((j + 0.5) * w / grid_size) + np.random.randint(-w//20, w//20)
|
|
y = int((i + 0.5) * h / grid_size) + np.random.randint(-h//20, h//20)
|
|
x = np.clip(x, 0, w-1)
|
|
y = np.clip(y, 0, h-1)
|
|
points.append([x, y])
|
|
|
|
return np.array(points)
|
|
|
|
def _create_triangulated_image(self, image, points, triangles, color_mode, outline, thickness):
|
|
"""Créer l'image triangulée"""
|
|
h, w, c = image.shape
|
|
result = np.zeros_like(image)
|
|
|
|
for triangle in triangles:
|
|
# Points du triangle
|
|
pts = points[triangle].astype(np.int32)
|
|
|
|
# Calculer la couleur du triangle
|
|
color = self._get_triangle_color(image, pts, color_mode)
|
|
|
|
# Dessiner le triangle
|
|
cv2.fillPoly(result, [pts], color.tolist())
|
|
|
|
# Dessiner le contour si demandé
|
|
if outline:
|
|
cv2.polylines(result, [pts], True, (0, 0, 0), thickness)
|
|
|
|
return result
|
|
|
|
def _get_triangle_color(self, image, triangle_points, mode):
|
|
"""Calculer la couleur d'un triangle"""
|
|
# Créer un masque pour le triangle
|
|
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
|
cv2.fillPoly(mask, [triangle_points], 255)
|
|
|
|
if mode == "average":
|
|
# Couleur moyenne
|
|
masked_pixels = image[mask > 0]
|
|
if len(masked_pixels) > 0:
|
|
return np.mean(masked_pixels, axis=0)
|
|
else:
|
|
return np.array([128, 128, 128])
|
|
|
|
elif mode == "dominant":
|
|
# Couleur dominante (approximation)
|
|
masked_pixels = image[mask > 0]
|
|
if len(masked_pixels) > 0:
|
|
# Quantifier les couleurs et prendre la plus fréquente
|
|
pixels_reshaped = masked_pixels.reshape(-1, 3)
|
|
unique_colors, counts = np.unique(pixels_reshaped, axis=0, return_counts=True)
|
|
dominant_color = unique_colors[np.argmax(counts)]
|
|
return dominant_color
|
|
else:
|
|
return np.array([128, 128, 128])
|
|
|
|
elif mode == "gradient":
|
|
# Gradient basé sur la position
|
|
center = np.mean(triangle_points, axis=0)
|
|
h, w = image.shape[:2]
|
|
gradient_factor = center[1] / h # Gradient vertical
|
|
base_color = np.mean(image[mask > 0], axis=0) if np.any(mask > 0) else np.array([128, 128, 128])
|
|
return base_color * (0.5 + 0.5 * gradient_factor)
|
|
|
|
def _apply_smoothing(self, image, smoothing):
|
|
"""Appliquer un lissage à l'image"""
|
|
kernel_size = int(smoothing * 10) * 2 + 1
|
|
smoothed = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
|
|
return image * (1 - smoothing) + smoothed * smoothing
|