import numpy as np import torch from PIL import Image, ImageDraw, ImageColor import random class PatternGeneratorNode: PATTERN_TYPES = ["Stripes", "Checkerboard", "Random Dots", "Solid Color", "Gradient", "Noise"] @classmethod def INPUT_TYPES(cls): return { "required": { "width": ("INT", {"default": 128, "min": 16, "max": 4096, "step": 8}), "height": ("INT", {"default": 128, "min": 16, "max": 4096, "step": 8}), "pattern_type": (cls.PATTERN_TYPES, {"default": "Noise"}), "color1_hex": ("STRING", {"default": "#000000", "multiline": False}), "color2_hex": ("STRING", {"default": "#FFFFFF", "multiline": False}), "parameter1": ("INT", {"default": 1, "min": 0, "max": 256, "step": 1, "tooltip":"Stripes:width; Dots:density%; Gradient:direction; Noise:0=Color/1=Grayscale"}), "parameter2": ("INT", {"default": 1, "min": 1, "max": 64, "step": 1, "tooltip":"Dots:max_radius; Noise:block_scale"}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "generate_pattern" CATEGORY = "illusion" def _hex_to_rgb(self, hex_color_string): try: return ImageColor.getrgb(hex_color_string) except ValueError: print(f"PatternGeneratorNode Warning: Invalid color string '{hex_color_string}'. Defaulting to black.") return (0, 0, 0) def generate_pattern(self, width, height, pattern_type, color1_hex, color2_hex, parameter1, parameter2, seed): np.random.seed(seed) random.seed(seed) c1 = self._hex_to_rgb(color1_hex) c2 = self._hex_to_rgb(color2_hex) image_np = np.zeros((height, width, 3), dtype=np.uint8) if pattern_type == "Stripes": stripe_width = max(1, parameter1) # Stripe width orientation = "Vertical" # Could be an input later for y_coord in range(height): for x_coord in range(width): if orientation == "Vertical": if (x_coord // stripe_width) % 2 == 0: image_np[y_coord, x_coord] = c1 else: image_np[y_coord, x_coord] = c2 else: # Horizontal if (y_coord // stripe_width) % 2 == 0: image_np[y_coord, x_coord] = c1 else: image_np[y_coord, x_coord] = c2 elif pattern_type == "Checkerboard": square_size = max(1, parameter1) # Square size for y_coord in range(height): for x_coord in range(width): if ((x_coord // square_size) % 2 == (y_coord // square_size) % 2): image_np[y_coord, x_coord] = c1 else: image_np[y_coord, x_coord] = c2 elif pattern_type == "Random Dots": density_percent = np.clip(parameter1, 1, 100) # Density percentage dot_radius_min = 1 dot_radius_max = np.clip(parameter2, 1, min(width, height)//4) # Max dot radius avg_radius = (dot_radius_min + dot_radius_max) / 2.0 if avg_radius < 1: avg_radius = 1 num_dots = int((width * height * (density_percent / 100.0)) / (np.pi * avg_radius**2)) num_dots = max(10, num_dots) num_dots = min(num_dots, width * height // 2) # Prevent extreme overdraw pil_image = Image.fromarray(image_np) pil_image = pil_image.convert("RGB") pil_image.paste(c1, (0,0,width,height)) draw = ImageDraw.Draw(pil_image) for _ in range(num_dots): dot_x = random.randint(0, width - 1) dot_y = random.randint(0, height - 1) dot_radius = random.randint(dot_radius_min, dot_radius_max) color_choice = c2 bbox = (dot_x - dot_radius, dot_y - dot_radius, dot_x + dot_radius, dot_y + dot_radius) try: # Add try-except for rare issues with ellipse on tiny images/radii draw.ellipse(bbox, fill=color_choice) except ValueError: pass # Skip dot if it causes an issue image_np = np.array(pil_image) elif pattern_type == "Solid Color": image_np[:, :] = c1 elif pattern_type == "Gradient": direction = parameter1 % 4 # Gradient direction for y_coord in range(height): for x_coord in range(width): if direction == 0: # Left to Right ratio = x_coord / (width -1) if width > 1 else 0 elif direction == 1: # Top to Bottom ratio = y_coord / (height -1) if height > 1 else 0 elif direction == 2: # Right to Left ratio = (width - 1 - x_coord) / (width - 1) if width > 1 else 0 else: # Bottom to Top (direction == 3) ratio = (height - 1 - y_coord) / (height - 1) if height > 1 else 0 r_val = int(c1[0] * (1 - ratio) + c2[0] * ratio) g_val = int(c1[1] * (1 - ratio) + c2[1] * ratio) b_val = int(c1[2] * (1 - ratio) + c2[2] * ratio) image_np[y_coord, x_coord] = (r_val, g_val, b_val) elif pattern_type == "Noise": is_grayscale_noise = parameter1 == 1 # 0 for color, 1 for grayscale block_scale = max(1, parameter2) # Scale of noise blocks, 1 for pixel noise for y_base in range(0, height, block_scale): for x_base in range(0, width, block_scale): if is_grayscale_noise: val = random.randint(0, 255) chosen_color = (val, val, val) else: chosen_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) for y_offset in range(block_scale): for x_offset in range(block_scale): y = y_base + y_offset x = x_base + x_offset if y < height and x < width: image_np[y, x] = chosen_color image_tensor = torch.from_numpy(image_np.astype(np.float32) / 255.0).unsqueeze(0) return (image_tensor,) NODE_CLASS_MAPPINGS = { "PatternGeneratorNode": PatternGeneratorNode, } NODE_DISPLAY_NAME_MAPPINGS = { "PatternGeneratorNode": "Pattern Generator" }