diff --git a/random_integer_node_efficient_advanced.py b/random_integer_node_efficient_advanced.py new file mode 100644 index 0000000..7df3192 --- /dev/null +++ b/random_integer_node_efficient_advanced.py @@ -0,0 +1,308 @@ +import random +import math + +class RandomIntegerNodeEfficientAdvanced: + # Constant for max attempts in Gaussian sampling + MAX_GAUSSIAN_ATTEMPTS = 100 + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # Width Parameters + "min_width": ("INT", {"default": 256}), + "max_width": ("INT", {"default": 1024}), + "width_divisors": ("STRING", {"default": "64", "description": "Comma-separated divisors for width"}), + + # Height Parameters + "min_height": ("INT", {"default": 256}), + "max_height": ("INT", {"default": 1024}), + "height_divisors": ("STRING", {"default": "64", "description": "Comma-separated divisors for height"}), + + # Randomization Toggles + "randomize_width": ("BOOLEAN", {"default": True}), + "randomize_height": ("BOOLEAN", {"default": True}), + + # Aspect Ratio Maintenance + "maintain_aspect_ratio": ("BOOLEAN", {"default": False}), + "aspect_ratio": ("FLOAT", {"default": 1.0, "visible": {"on": "maintain_aspect_ratio", "value": True}}), # e.g., 16:9 -> 1.7778 + "aspect_ratio_basis": (["width", "height"], {"default": "width", "visible": {"on": "maintain_aspect_ratio", "value": True}}), + "max_aspect_ratio_deviation": ("FLOAT", {"default": 10.0, "visible": {"on": "maintain_aspect_ratio", "value": True}}), + + # Randomization Type + "randomization_type": (["Uniform", "Gaussian"], {"default": "Uniform"}), + "gaussian_mean_width": ("INT", {"default": 512}), + "gaussian_std_width": ("INT", {"default": 128}), + "gaussian_mean_height": ("INT", {"default": 512}), + "gaussian_std_height": ("INT", {"default": 128}), + + # Exclusion Zones + "exclude_widths": ("STRING", {"default": "", "description": "Comma-separated widths to exclude"}), + "exclude_heights": ("STRING", {"default": "", "description": "Comma-separated heights to exclude"}), + + # Cross-Dimensional Constraints + "max_total_megapixels": ("FLOAT", {"default": 1.0, "description": "Maximum total megapixels"}), + # New Parameter + "max_aspect_ratio_any_direction": ("FLOAT", {"default": 4.0, "description": "Maximum allowed aspect ratio in any direction"}), + } + } + + RETURN_TYPES = ("INT", "INT") + RETURN_NAMES = ("Width", "Height") + FUNCTION = "generate_random_dimensions" + CATEGORY = "Custom/Random" + + def generate_random_dimensions( + self, + min_width, + max_width, + width_divisors, + min_height, + max_height, + height_divisors, + randomize_width, + randomize_height, + maintain_aspect_ratio, + # (when MAR is on) + aspect_ratio, + # (when MAR is on) + aspect_ratio_basis, + # (when MAR is on) + max_aspect_ratio_deviation, + randomization_type, + gaussian_mean_width, + gaussian_std_width, + gaussian_mean_height, + gaussian_std_height, + exclude_widths, + exclude_heights, + max_total_megapixels, + max_aspect_ratio_any_direction, + ): + # Convert megapixels to pixels + max_total_pixels = int(max_total_megapixels * 1_000_000) if max_total_megapixels else None + + # ---------- Helper Functions ---------- + def parse_divisors(divisors_str): + """Parse and validate divisor strings into a list of integers.""" + try: + divisors = [int(d.strip()) for d in divisors_str.split(',') if d.strip()] + if not divisors: + raise ValueError("No valid divisors provided.") + if any(divisor == 0 for divisor in divisors): + raise ValueError("Divisors cannot be zero.") + return divisors + except ValueError: + raise ValueError("Divisors must be integers separated by commas.") + + def parse_exclusions(exclusions_str): + """Parse exclusion strings into a list of integers.""" + try: + return [int(e.strip()) for e in exclusions_str.split(',') if e.strip()] + except ValueError: + raise ValueError("Exclusions must be integers separated by commas.") + + def calculate_valid_multiples(min_val, max_val, divisors, exclusions): + """Calculate valid multiples within a range based on divisors and exclusions.""" + valid = set() + for divisor in divisors: + # Calculate the smallest multiple of divisor >= min_val + start = min_val if min_val % divisor == 0 else min_val + (divisor - (min_val % divisor)) + # Generate multiples within range + for val in range(start, max_val + 1, divisor): + if val not in exclusions and min_val <= val <= max_val: + valid.add(val) + return sorted(valid) + + def sample_uniform(valid_values): + """Sample a value uniformly from valid values.""" + if not valid_values: + return None + return random.choice(valid_values) + + def sample_gaussian(mean, std, valid_values, min_val, max_val): + """Sample a value using Gaussian distribution from valid values.""" + if not valid_values: + return None + for _ in range(self.MAX_GAUSSIAN_ATTEMPTS): + sample = int(random.gauss(mean, std)) + # Clamp sample within range + sample = max(min_val, min(sample, max_val)) + # Find nearest multiple + nearest = min_valid_multiple(sample, valid_values) + if nearest is not None: + return nearest + # Fallback if no valid sample found + return min(valid_values, key=lambda x: abs(x - mean)) if valid_values else None + + def min_valid_multiple(target, valid_values): + """Find the closest value in valid_values to target.""" + return min(valid_values, key=lambda x: abs(x - target), default=None) + + def calculate_aspect_ratio(width, height): + """Calculate the aspect ratio of width to height.""" + if height == 0: + return float('inf') + return width / height + + # ---------- Input Validation ---------- + if min_width > max_width: + raise ValueError("Minimum width cannot be greater than maximum width.") + if min_height > max_height: + raise ValueError("Minimum height cannot be greater than maximum height.") + if aspect_ratio <= 0: + raise ValueError("Aspect ratio must be a positive number.") + if max_aspect_ratio_any_direction <= 0: + raise ValueError("Max aspect ratio in any direction must be positive.") + + # ---------- Parse Divisors and Exclusions ---------- + width_divisors_list = parse_divisors(width_divisors) + height_divisors_list = parse_divisors(height_divisors) + exclude_widths_list = parse_exclusions(exclude_widths) + exclude_heights_list = parse_exclusions(exclude_heights) + + # ---------- Prepare Valid Values ---------- + valid_widths = calculate_valid_multiples(min_width, max_width, width_divisors_list, exclude_widths_list) + valid_heights = calculate_valid_multiples(min_height, max_height, height_divisors_list, exclude_heights_list) + + # Fallback defaults + default_width = min_width if min_width in valid_widths else valid_widths[0] if valid_widths else min_width + default_height = min_height if min_height in valid_heights else valid_heights[0] if valid_heights else min_height + + # ---------- Sampling Functions ---------- + def sample_width(): + """Sample a width value based on the randomization type.""" + if randomization_type == "Uniform": + return sample_uniform(valid_widths) + elif randomization_type == "Gaussian": + return sample_gaussian(gaussian_mean_width, gaussian_std_width, valid_widths, min_width, max_width) + return None + + def sample_height(): + """Sample a height value based on the randomization type.""" + if randomization_type == "Uniform": + return sample_uniform(valid_heights) + elif randomization_type == "Gaussian": + return sample_gaussian(gaussian_mean_height, gaussian_std_height, valid_heights, min_height, max_height) + return None + + # ---------- Generate Dimensions ---------- + width = height = None + + try: + if maintain_aspect_ratio: + if aspect_ratio_basis == "width": + # Randomize or set width + width = sample_width() if randomize_width else default_width + width = width or default_width # Ensure width is set + + # Calculate height based on aspect ratio + calculated_height = width / aspect_ratio + # Round and snap to valid height + calculated_height = int(round(calculated_height)) + height = min_valid_multiple(calculated_height, valid_heights) + if height is None: + raise ValueError("No valid height found to maintain aspect ratio.") + elif aspect_ratio_basis == "height": + # Randomize or set height + height = sample_height() if randomize_height else default_height + height = height or default_height # Ensure height is set + + # Calculate width based on aspect ratio + calculated_width = height * aspect_ratio + # Round and snap to valid width + calculated_width = int(round(calculated_width)) + width = min_valid_multiple(calculated_width, valid_widths) + if width is None: + raise ValueError("No valid width found to maintain aspect ratio.") + else: + raise ValueError("Invalid aspect_ratio_basis value.") + + # Validate dimensions within min/max ranges + if not (min_width <= width <= max_width) or not (min_height <= height <= max_height): + raise ValueError("Dimensions out of bounds after applying aspect ratio.") + + else: + # Independently randomize width and height + width = sample_width() if randomize_width else default_width + height = sample_height() if randomize_height else default_height + width = width or default_width # Ensure width is set + height = height or default_height # Ensure height is set + + # Apply Cross-Dimensional Constraints + # Constraint 1: Total Pixel Area + if max_total_pixels: + total_pixels = width * height + if total_pixels > max_total_pixels: + scaling_factor = math.sqrt(max_total_pixels / total_pixels) + scaled_width = int(width * scaling_factor) + scaled_height = int(height * scaling_factor) + + # Snap to nearest valid multiples + scaled_width = min_valid_multiple(scaled_width, valid_widths) or default_width + scaled_height = min_valid_multiple(scaled_height, valid_heights) or default_height + + width, height = scaled_width, scaled_height + + # Constraint 2: Aspect Ratio Deviation (when MAR is on) + if maintain_aspect_ratio and max_aspect_ratio_deviation: + current_aspect = calculate_aspect_ratio(width, height) + deviation = abs((current_aspect - aspect_ratio) / aspect_ratio) * 100 + if deviation > max_aspect_ratio_deviation: + # Adjust dimensions to reduce deviation + if aspect_ratio_basis == "width": + # Adjust height + calculated_height = width / aspect_ratio + calculated_height = int(round(calculated_height)) + height = min_valid_multiple(calculated_height, valid_heights) or height + else: + # Adjust width + calculated_width = height * aspect_ratio + calculated_width = int(round(calculated_width)) + width = min_valid_multiple(calculated_width, valid_widths) or width + + # Constraint 3: Max Aspect Ratio in Any Direction + if max_aspect_ratio_any_direction: + aspect_ratio_wh = width / height if width >= height else height / width + if aspect_ratio_wh > max_aspect_ratio_any_direction: + # Adjust dimensions to meet the aspect ratio constraint + if width >= height: + # Reduce width + adjusted_width = height * max_aspect_ratio_any_direction + adjusted_width = int(round(adjusted_width)) + adjusted_width = min_valid_multiple(adjusted_width, valid_widths) + if adjusted_width and min_width <= adjusted_width <= max_width: + width = adjusted_width + else: + # As a fallback, set to max allowed width based on aspect ratio + width = min_valid_multiple(int(height * max_aspect_ratio_any_direction), valid_widths) or width + else: + # Reduce height + adjusted_height = width * max_aspect_ratio_any_direction + adjusted_height = int(round(adjusted_height)) + adjusted_height = min_valid_multiple(adjusted_height, valid_heights) + if adjusted_height and min_height <= adjusted_height <= max_height: + height = adjusted_height + else: + # As a fallback, set to max allowed height based on aspect ratio + height = min_valid_multiple(int(width * max_aspect_ratio_any_direction), valid_heights) or height + + # Final Validation + if width not in valid_widths or height not in valid_heights: + raise ValueError("Final dimensions are invalid.") + + except ValueError as e: + # Handle specific exceptions and provide feedback + print(f"Warning: {e}. Using default dimensions.") + width = width or default_width + height = height or default_height + + # Ensure fallback values adhere to constraints + width = min_valid_multiple(width, valid_widths) or default_width + height = min_valid_multiple(height, valid_heights) or default_height + + return (width, height) + + @classmethod + def IS_CHANGED(cls, *args, **kwargs): + return float('nan')