Flux Regional Control & Res Cal Update

This commit is contained in:
ControlAltAI
2024-12-08 05:46:49 +05:30
parent bf57aa0c81
commit 2e362fb850
9 changed files with 1541 additions and 4 deletions
+22 -1
View File
@@ -7,6 +7,13 @@ from .get_image_size_ratio_node import GetImageSizeRatio
from .noise_plus_blend_node import NoisePlusBlend
from .integer_settings_node import IntegerSettings
from .choose_upscale_model_node import ChooseUpscaleModel
from .region_mask_generator_node import RegionMaskGenerator
from .region_mask_validator_node import RegionMaskValidator
from .region_mask_processor_node import RegionMaskProcessor
from .region_mask_conditioning_node import RegionMaskConditioning
from .flux_attention_control_node import FluxAttentionControl
from .region_overlay_visualizer_node import RegionOverlayVisualizer
from .flux_attention_cleanup_node import FluxAttentionCleanup
print("Initializing ControlAltAI Nodes")
@@ -20,6 +27,13 @@ NODE_CLASS_MAPPINGS = {
"NoisePlusBlend": NoisePlusBlend,
"IntegerSettings": IntegerSettings,
"ChooseUpscaleModel": ChooseUpscaleModel,
"RegionMaskGenerator": RegionMaskGenerator,
"RegionMaskValidator": RegionMaskValidator,
"RegionMaskProcessor": RegionMaskProcessor,
"RegionMaskConditioning": RegionMaskConditioning,
"FluxAttentionControl": FluxAttentionControl,
"RegionOverlayVisualizer": RegionOverlayVisualizer,
"FluxAttentionCleanup": FluxAttentionCleanup,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -32,4 +46,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"NoisePlusBlend": "Noise Plus Blend",
"IntegerSettings": "Integer Settings",
"ChooseUpscaleModel": "Choose Upscale Model",
}
"RegionMaskGenerator": "Region Mask Generator",
"RegionMaskValidator": "Region Mask Validator",
"RegionMaskProcessor": "Region Mask Processor",
"RegionMaskConditioning": "Region Mask Conditioning",
"FluxAttentionControl": "Flux Attention Control",
"RegionOverlayVisualizer": "Region Overlay Visualizer",
"FluxAttentionCleanup": "Flux Attention Cleanup",
}
+75
View File
@@ -0,0 +1,75 @@
import torch
from comfy.ldm.modules import attention as comfy_attention
from comfy.ldm.flux import math as flux_math
from comfy.ldm.flux import layers as flux_layers
class AnyType(str):
"""A special class that is always equal in not equal comparisons"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
class FluxAttentionCleanup:
def __init__(self):
self.original_attention = comfy_attention.optimized_attention
self.original_flux_attention = flux_math.attention
self.original_flux_layers_attention = flux_layers.attention
self.current_attn_mask = None
print("FluxAttentionCleanup initialized")
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any_input": (any_type, {}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("message",)
FUNCTION = "cleanup_attention"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def cleanup_attention(self, any_input):
"""Skip cleanup during normal operation, but clean on workflow switch"""
message = "Attention preserved for current workflow. Will clean on workflow switch."
print("\n" + message)
return (message,)
def __del__(self):
"""Clean up attention when switching workflows"""
try:
print("\nStarting attention cleanup for workflow switch...")
# Reset attention functions to original state
flux_math.attention = self.original_flux_attention
flux_layers.attention = self.original_flux_layers_attention
# Clear attention mask
if hasattr(flux_math.attention, 'keywords'):
if 'attn_mask' in flux_math.attention.keywords:
flux_math.attention.keywords['attn_mask'] = None
# Clear stored mask
if self.current_attn_mask is not None:
del self.current_attn_mask
self.current_attn_mask = None
# Force CUDA cleanup
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
print("Workflow switch: Region Attention Cleanup Successful")
except:
pass
# Node class mappings
NODE_CLASS_MAPPINGS = {
"FluxAttentionCleanup": FluxAttentionCleanup
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FluxAttentionCleanup": "Flux Attention Cleanup"
}
+304
View File
@@ -0,0 +1,304 @@
import torch
from torch import Tensor
import torch.nn.functional as F
from typing import List, Dict, Optional, Tuple
from einops import rearrange
import comfy.model_management as model_management
from comfy.ldm.modules import attention as comfy_attention
from comfy.ldm.flux import math as flux_math
from comfy.ldm.flux import layers as flux_layers
from xformers.ops import memory_efficient_attention as xattention
import numpy as np
from PIL import Image, ImageFilter, ImageDraw
from functools import partial
class FluxAttentionControl:
def __init__(self):
self.original_attention = comfy_attention.optimized_attention
self.original_flux_attention = flux_math.attention
self.original_flux_layers_attention = flux_layers.attention
print("FluxAttentionControl initialized")
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"condition": ("CONDITIONING",),
"latent_dimensions": ("LATENT",),
"region1": ("REGION",),
"number_of_regions": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"step": 1,
"display": "Number of Regions"
}),
"enabled": ("BOOLEAN", {
"default": True,
"display": "Enable Regional Control"
}),
"feather_radius1": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 100.0,
"step": 1.0,
"display": "Feather Radius for Region 1"
}),
},
"optional": {
"region2": ("REGION",),
"feather_radius2": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 100.0,
"step": 1.0,
"display": "Feather Radius for Region 2"
}),
"region3": ("REGION",),
"feather_radius3": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 100.0,
"step": 1.0,
"display": "Feather Radius for Region 3"
}),
}
}
RETURN_TYPES = ("MODEL", "CONDITIONING",)
RETURN_NAMES = ("model", "conditioning",)
FUNCTION = "apply_attention_control"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def generate_region_mask(self, region: Dict, width: int, height: int, feather_radius: float) -> Image.Image:
if region.get('bbox') is not None:
x1, y1, x2, y2 = region['bbox']
x1_px = int(x1 * width)
y1_px = int(y1 * height)
x2_px = int(x2 * width)
y2_px = int(y2 * height)
mask = Image.new('L', (width, height), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rectangle([x1_px, y1_px, x2_px, y2_px], fill=255)
if feather_radius > 0:
mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_radius))
print(f'Generating masks with {width}x{height} and [{x1}, {y1}, {x2}, {y2}], feather_radius={feather_radius}')
return mask
elif region.get('mask') is not None:
mask = region['mask'][0].cpu().numpy()
mask = (mask * 255).astype(np.uint8)
mask = Image.fromarray(mask)
mask = mask.resize((width, height))
if feather_radius > 0:
mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_radius))
return mask
else:
raise Exception('Unknown region type')
def generate_test_mask(self, masks: List[Image.Image], height: int, width: int):
hH, hW = int(height) // 16, int(width) // 16
print(f'{width} {height} -> {hW} {hH}')
lin_masks = []
for mask in masks:
mask = mask.convert('L')
mask = torch.tensor(np.array(mask)).unsqueeze(0).unsqueeze(0) / 255.0 # Normalize to 0-1
mask = F.interpolate(mask, (hH, hW), mode='bilinear', align_corners=False).flatten()
lin_masks.append(mask)
return lin_masks, hH, hW
def prepare_attention_mask(self, lin_masks: List[torch.Tensor], region_strengths: List[float], Nx: int, emb_size: int, emb_len: int):
"""Prepare attention mask for three regions with per-region strengths."""
total_len = emb_len + Nx
n_regs = len(lin_masks)
# Initialize attention mask and scales
cross_mask = torch.zeros(total_len, total_len)
q_scale = torch.ones(total_len)
k_scale = torch.ones(total_len)
# Indices for embeddings
main_prompt_start = 0
main_prompt_end = emb_size
# Subprompt indices
subprompt_starts = [emb_size * (i + 1) for i in range(n_regs)]
subprompt_ends = [emb_size * (i + 2) for i in range(n_regs)]
# Initialize position masks
position_masks = torch.stack(lin_masks) # Shape: [n_regs, Nx]
# Normalize masks so that overlapping areas sum to 1
position_masks_sum = position_masks.sum(dim=0)
position_masks_normalized = position_masks / (position_masks_sum + 1e-8)
# Build attention masks and scales
for i in range(n_regs):
sp_start = subprompt_starts[i]
sp_end = subprompt_ends[i]
mask_i = position_masks_normalized[i]
# Scale embeddings based on mask and per-region strength
strength = region_strengths[i]
q_scale[sp_start:sp_end] = mask_i.mean() * strength
k_scale[sp_start:sp_end] = mask_i.mean() * strength
# Create mask including tokens and positions
m_with_tokens = torch.cat([torch.ones(emb_len), mask_i])
mb = m_with_tokens > 0.0 # Include positions where mask > 0
# Block attention between positions not in mask and subprompt
cross_mask[~mb, sp_start:sp_end] = 1
cross_mask[sp_start:sp_end, ~mb] = 1
# Block attention between positions in region and main prompt
positions_idx = (mask_i > 0.0).nonzero(as_tuple=True)[0] + emb_len
cross_mask[positions_idx[:, None], main_prompt_start:main_prompt_end] = 1
cross_mask[main_prompt_start:main_prompt_end, positions_idx[None, :]] = 1
# Block attention between subprompts
for j in range(n_regs):
if i != j:
other_sp_start = subprompt_starts[j]
other_sp_end = subprompt_ends[j]
cross_mask[sp_start:sp_end, other_sp_start:other_sp_end] = 1
cross_mask[other_sp_start:other_sp_end, sp_start:sp_end] = 1
# Ensure self-attention is allowed
cross_mask.fill_diagonal_(0)
# Prepare scales for GPU
q_scale = q_scale.reshape(1, 1, -1, 1).cuda()
k_scale = k_scale.reshape(1, 1, -1, 1).cuda()
return cross_mask, q_scale, k_scale
def xformers_attention(self, q: Tensor, k: Tensor, v: Tensor, pe: Tensor,
attn_mask: Optional[Tensor] = None) -> Tensor:
q, k = flux_math.apply_rope(q, k, pe)
q = rearrange(q, "B H L D -> B L H D")
k = rearrange(k, "B H L D -> B L H D")
v = rearrange(v, "B H L D -> B L H D")
if attn_mask is not None:
x = xattention(q, k, v, attn_bias=attn_mask)
else:
x = xattention(q, k, v)
x = rearrange(x, "B L H D -> B L (H D)")
return x
def apply_attention_control(self,
model: object,
condition: List,
latent_dimensions: Dict,
region1: Dict,
number_of_regions: int,
enabled: bool,
feather_radius1: float = 0.0,
region2: Optional[Dict] = None,
feather_radius2: Optional[float] = 0.0,
region3: Optional[Dict] = None,
feather_radius3: Optional[float] = 0.0):
# Extract dimensions and embeddings first (moved before enabled check)
latent = latent_dimensions["samples"]
bs_l, n_ch, lH, lW = latent.shape
text_emb = condition[0][0].clone()
clip_emb = condition[0][1]['pooled_output'].clone()
bs, emb_size, emb_dim = text_emb.shape
iH, iW = lH * 8, lW * 8
if not enabled:
# Restore original attention functions
flux_math.attention = self.original_flux_attention
flux_layers.attention = self.original_flux_layers_attention
print("Regional control disabled. Restored original attention functions.")
return (model, condition) # Return original condition when disabled
print(f'Region attention Node enabled: {enabled}, regions: {number_of_regions}')
# Extract dimensions and embeddings
latent = latent_dimensions["samples"]
bs_l, n_ch, lH, lW = latent.shape
text_emb = condition[0][0].clone()
clip_emb = condition[0][1]['pooled_output'].clone()
bs, emb_size, emb_dim = text_emb.shape
iH, iW = lH * 8, lW * 8
# Process active regions
subprompts_embeds = []
masks = []
region_strengths = []
# Collect regions and feather radii
regions = [region1, region2, region3]
feather_radii = [feather_radius1, feather_radius2, feather_radius3]
for idx, region in enumerate(regions[:number_of_regions]):
if region is not None and region.get('conditioning') is not None:
# Get 'strength' from region or default to 1.0
strength = region.get('strength', 1.0)
region_strengths.append(strength)
subprompt_emb = region['conditioning'][0][0]
subprompts_embeds.append(subprompt_emb)
# Use per-region feather_radius
feather_radius = feather_radii[idx] if feather_radii[idx] is not None else 0.0
masks.append(self.generate_region_mask(region, iW, iH, feather_radius))
else:
print(f"Region {idx+1} is None or has no conditioning")
if not subprompts_embeds:
print("No active regions with conditioning found.")
# Restore original attention functions
flux_math.attention = self.original_flux_attention
flux_layers.attention = self.original_flux_layers_attention
return (model, condition)
n_regs = len(subprompts_embeds)
# Generate attention components
lin_masks, hH, hW = self.generate_test_mask(masks, iH, iW)
Nx = int(hH * hW)
emb_len = emb_size * (n_regs + 1) # +1 for main prompt
# Create attention mask
attn_mask, q_scale, k_scale = self.prepare_attention_mask(
lin_masks, region_strengths, Nx, emb_size, emb_len)
# Format for xFormers
device = torch.device('cuda')
attn_dtype = torch.bfloat16 if model_management.should_use_bf16(device=device) else torch.float16
if attn_mask is not None:
print(f'Applying attention masks: torch.Size([{attn_mask.shape[0]}, {attn_mask.shape[1]}])')
L = attn_mask.shape[0]
H = 24 # Number of heads in FLUX model
pad = (8 - L % 8) % 8 # Ensure pad is between 0 and 7
pad_L = L + pad
mask_out = torch.zeros([bs, H, pad_L, pad_L], dtype=attn_dtype, device=device)
mask_out[:, :, :L, :L] = attn_mask.to(device, dtype=attn_dtype)
attn_mask = mask_out[:, :, :pad_L, :pad_L]
# Prepare final mask
attn_mask_bool = attn_mask > 0.5
attn_mask.masked_fill_(attn_mask_bool, float('-inf'))
# Override attention
attn_mask_arg = attn_mask if enabled else None
override_attention = partial(self.xformers_attention, attn_mask=attn_mask_arg)
flux_math.attention = override_attention
flux_layers.attention = override_attention
# Create extended conditioning
extended_condition = torch.cat([text_emb] + subprompts_embeds, dim=1)
return (model, [[extended_condition, {'pooled_output': clip_emb}]])
# Node class mappings
NODE_CLASS_MAPPINGS = {
"FluxAttentionControl": FluxAttentionControl
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FluxAttentionControl": "Flux Attention Control"
}
+85 -3
View File
@@ -1,3 +1,11 @@
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import torch
def pil2tensor(image):
"""Convert PIL image to tensor in the correct format"""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class FluxResolutionNode:
@classmethod
def INPUT_TYPES(cls):
@@ -18,19 +26,90 @@ class FluxResolutionNode:
}
}
RETURN_TYPES = ("INT", "INT", "STRING")
RETURN_NAMES = ("width", "height", "resolution")
RETURN_TYPES = ("INT", "INT", "STRING", "IMAGE")
RETURN_NAMES = ("width", "height", "resolution", "preview")
FUNCTION = "calculate_dimensions"
CATEGORY = "ControlAltAI Nodes/Flux"
OUTPUT_NODE = True
def create_preview_image(self, width, height, resolution, ratio_display):
# 1024x1024 preview size
preview_size = (1024, 1024)
image = Image.new('RGB', preview_size, (0, 0, 0)) # Black background
draw = ImageDraw.Draw(image)
# Draw grid with grey lines
grid_color = '#333333' # Dark grey for grid
grid_spacing = 50 # Adjusted grid spacing
for x in range(0, preview_size[0], grid_spacing):
draw.line([(x, 0), (x, preview_size[1])], fill=grid_color)
for y in range(0, preview_size[1], grid_spacing):
draw.line([(0, y), (preview_size[0], y)], fill=grid_color)
# Calculate preview box dimensions
preview_width = 800 # Increased size
preview_height = int(preview_width * (height / width))
# Adjust if height is too tall
if preview_height > 800: # Adjusted for larger preview
preview_height = 800
preview_width = int(preview_height * (width / height))
# Calculate center position
x_offset = (preview_size[0] - preview_width) // 2
y_offset = (preview_size[1] - preview_height) // 2
# Draw the aspect ratio box with thicker outline
draw.rectangle(
[(x_offset, y_offset), (x_offset + preview_width, y_offset + preview_height)],
outline='red',
width=4 # Thicker outline
)
# Add text with larger font sizes
try:
# Draw text (centered)
text_y = y_offset + preview_height//2
# Resolution text in red
draw.text((preview_size[0]//2, text_y),
f"{width}x{height}",
fill='red',
anchor="mm",
font=ImageFont.truetype("arial.ttf", 48))
# Aspect ratio text in red
draw.text((preview_size[0]//2, text_y + 60),
f"({ratio_display})",
fill='red',
anchor="mm",
font=ImageFont.truetype("arial.ttf", 36))
# Resolution text at bottom in white
draw.text((preview_size[0]//2, y_offset + preview_height + 60),
f"Resolution: {resolution}",
fill='white', # Changed to white
anchor="mm",
font=ImageFont.truetype("arial.ttf", 32))
except:
# Fallback if font loading fails
draw.text((preview_size[0]//2, text_y), f"{width}x{height}", fill='red', anchor="mm")
draw.text((preview_size[0]//2, text_y + 60), f"({ratio_display})", fill='red', anchor="mm")
draw.text((preview_size[0]//2, y_offset + preview_height + 60), f"Resolution: {resolution}", fill='white', anchor="mm")
# Convert to tensor using the helper function
return pil2tensor(image)
def calculate_dimensions(self, megapixel, aspect_ratio, custom_ratio, custom_aspect_ratio=None):
megapixel = float(megapixel)
if custom_ratio and custom_aspect_ratio:
numeric_ratio = custom_aspect_ratio
ratio_display = custom_aspect_ratio # Keep original format for display
else:
numeric_ratio = aspect_ratio.split(' ')[0]
ratio_display = numeric_ratio # Keep original format for display
width_ratio, height_ratio = map(int, numeric_ratio.split(':'))
@@ -52,7 +131,10 @@ class FluxResolutionNode:
resolution = f"{width} x {height}"
return width, height, resolution
# Generate preview image with original ratio format
preview = self.create_preview_image(width, height, resolution, ratio_display)
return width, height, resolution, preview
NODE_CLASS_MAPPINGS = {
"FluxResolutionNode": FluxResolutionNode,
+301
View File
@@ -0,0 +1,301 @@
import torch
import numpy as np
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class RegionMaskConditioning:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask1": ("MASK",),
"bbox1": ("BBOX",),
"conditioning1": ("CONDITIONING",),
"number_of_regions": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"step": 1,
"display": "Number of Regions"
}),
"strength1": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"display": "Strength for Region 1"
}),
},
"optional": {
"mask2": ("MASK",),
"bbox2": ("BBOX",),
"conditioning2": ("CONDITIONING",),
"strength2": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"display": "Strength for Region 2"
}),
"mask3": ("MASK",),
"bbox3": ("BBOX",),
"conditioning3": ("CONDITIONING",),
"strength3": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"display": "Strength for Region 3"
}),
}
}
RETURN_TYPES = ("REGION", "REGION", "REGION", "INT", "IMAGE")
RETURN_NAMES = ("region1", "region2", "region3",
"region_count", "preview_image")
FUNCTION = "create_conditioned_regions"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def validate_bbox(self, bbox: Dict) -> bool:
"""Validate bbox coordinates and structure"""
print(f"\n=== Validating BBox ===")
print(f"Input bbox: {bbox}")
if bbox is None or not isinstance(bbox, dict):
print("Failed: Invalid bbox type")
return False
required_keys = ["x1", "y1", "x2", "y2"]
if not all(k in bbox for k in required_keys):
print(f"Failed: Missing keys. Required: {required_keys}")
return False
# Validate coordinate values
if not all(isinstance(bbox[k], (int, float)) for k in required_keys):
print("Failed: Invalid coordinate types")
return False
# Validate coordinate ranges
if not all(0 <= bbox[k] <= 1.0 for k in required_keys):
print("Failed: Coordinates out of range [0,1]")
return False
# Validate proper ordering
if bbox["x1"] >= bbox["x2"] or bbox["y1"] >= bbox["y2"]:
print("Failed: Invalid coordinate ordering")
return False
print("Passed: BBox validation successful")
return True
def scale_conditioning(self, conditioning: List, strength: float) -> List:
"""Scale conditioning tensors by strength"""
print(f"\n=== Scaling Conditioning ===")
print(f"Strength: {strength}")
try:
if not conditioning or not isinstance(conditioning, list):
print("Failed: Invalid conditioning format")
raise ValueError("Invalid conditioning format")
# Get the conditioning tensors and dict
cond_tensors = conditioning[0][0]
cond_dict = conditioning[0][1]
print(f"Input tensor shape: {cond_tensors.shape}")
print(f"Conditioning keys: {list(cond_dict.keys())}")
print(f"Input tensor stats: min={cond_tensors.min():.3f}, max={cond_tensors.max():.3f}, mean={cond_tensors.mean():.3f}")
# Scale the tensors
scaled_tensors = cond_tensors.clone() * strength
print(f"Scaled tensor stats: min={scaled_tensors.min():.3f}, max={scaled_tensors.max():.3f}, mean={scaled_tensors.mean():.3f}")
return [[scaled_tensors, cond_dict]]
except Exception as e:
print(f"Error in scale_conditioning: {str(e)}")
import traceback
traceback.print_exc()
return conditioning
def create_region(self, mask: Optional[torch.Tensor], bbox: Optional[Dict],
conditioning: Optional[List], strength: float, region_idx: int) -> Dict:
"""Create a single region with its conditioning"""
print(f"\n=== Creating Region {region_idx} ===")
# Debug inputs
print("Input validation:")
print(f"- Mask: {type(mask)}, shape={mask.shape if mask is not None else None}")
print(f"- BBox: {bbox}")
print(f"- Conditioning type: {type(conditioning)}")
print(f"- Strength: {strength}")
# Default empty region
empty_region = {
"conditioning": None,
"bbox": [0.0, 0.0, 0.0, 0.0], # Array format for empty
"is_active": False,
"strength": 1.0
}
try:
# Validate inputs
if mask is None or bbox is None or conditioning is None:
print(f"Region {region_idx}: Missing components")
return empty_region
if not self.validate_bbox(bbox):
print(f"Region {region_idx}: Invalid bbox")
return empty_region
# Scale conditioning
scaled_conditioning = self.scale_conditioning(conditioning, strength)
# Create region output - bbox array, conditioning, and strength
region = {
"conditioning": scaled_conditioning,
"bbox": [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]], # Array format
"is_active": True,
"strength": strength
}
print(f"\nSuccessfully created region {region_idx}")
return region
except Exception as e:
print(f"Error creating region {region_idx}: {str(e)}")
import traceback
traceback.print_exc()
return empty_region
def create_preview(self, masks: List[torch.Tensor], bboxes: List[Dict],
number_of_regions: int) -> torch.Tensor:
"""Create preview of conditioned regions"""
print("\n=== Creating Preview ===")
if not masks:
print("No masks provided")
return torch.zeros((3, 64, 64), dtype=torch.float32)
height, width = masks[0].shape
print(f"Preview dimensions: {width}x{height}")
# Create PIL Image for preview
preview = Image.new("RGB", (width, height), (0, 0, 0))
draw = ImageDraw.Draw(preview)
# Define colors for 3 regions
colors = [
(255, 0, 0), # Red
(0, 255, 0), # Green
(255, 255, 0), # Yellow
]
# Draw each region
for i, (mask, bbox) in enumerate(zip(masks[:number_of_regions], bboxes[:number_of_regions])):
validation_result = self.validate_bbox(bbox)
if validation_result and mask is not None:
print(f"\nDrawing region {i+1}:")
# Get pixel coordinates
x1 = int(bbox["x1"] * width)
y1 = int(bbox["y1"] * height)
x2 = int(bbox["x2"] * width)
y2 = int(bbox["y2"] * height)
print(f"Region {i+1} coordinates: ({x1},{y1}) to ({x2},{y2})")
# Draw region outline
draw.rectangle([x1, y1, x2, y2], outline=colors[i], width=4)
return pil2tensor(preview)
def create_conditioned_regions(self,
mask1: torch.Tensor,
bbox1: Dict,
conditioning1: List,
number_of_regions: int,
strength1: float,
mask2: Optional[torch.Tensor] = None,
bbox2: Optional[Dict] = None,
conditioning2: Optional[List] = None,
strength2: Optional[float] = 1.0,
mask3: Optional[torch.Tensor] = None,
bbox3: Optional[Dict] = None,
conditioning3: Optional[List] = None,
strength3: Optional[float] = 1.0) -> Tuple:
print("\n=== Creating Conditioned Regions ===")
print(f"Number of regions: {number_of_regions}")
try:
# Create regions
regions = []
active_count = 0
# Process required number of regions
inputs = [
(mask1, bbox1, conditioning1, strength1),
(mask2, bbox2, conditioning2, strength2),
(mask3, bbox3, conditioning3, strength3)
]
# Store masks and bboxes for preview only
preview_masks = []
preview_bboxes = []
for i, (mask, bbox, conditioning, strength) in enumerate(inputs[:number_of_regions]):
# Create region with per-region strength
region = self.create_region(mask, bbox, conditioning, strength, i+1)
if region["is_active"]:
active_count += 1
regions.append(region)
print(f"Processed region {i+1}: active={region['is_active']}")
# Store for preview
preview_masks.append(mask)
preview_bboxes.append(bbox)
# Fill remaining slots with empty regions
empty_region = {
"conditioning": None,
"bbox": [0.0, 0.0, 0.0, 0.0], # Array format
"is_active": False,
"strength": 1.0
}
while len(regions) < 3:
idx = len(regions) + 1
print(f"Adding empty region {idx}")
regions.append(empty_region)
print(f"\nCreated {active_count} active regions out of {number_of_regions} requested")
# Create preview using stored masks and bboxes
preview = self.create_preview(preview_masks, preview_bboxes, number_of_regions)
return (*regions, active_count, preview)
except Exception as e:
print(f"Error in create_conditioned_regions: {str(e)}")
import traceback
traceback.print_exc()
empty_region = {
"conditioning": None,
"bbox": [0.0, 0.0, 0.0, 0.0], # Array format
"is_active": False,
"strength": 1.0
}
empty_preview = torch.zeros((3, mask1.shape[0], mask1.shape[1]), dtype=torch.float32)
return (empty_region, empty_region, empty_region, 0, empty_preview)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RegionMaskConditioning": RegionMaskConditioning
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RegionMaskConditioning": "Region Mask Conditioning"
}
+164
View File
@@ -0,0 +1,164 @@
import torch
import numpy as np
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class RegionMaskGenerator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024}),
"height": ("INT", {"default": 1024}),
"number_of_regions": ("INT", {"default": 1, "min": 1, "max": 3}),
# Region 1
"region1_x1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"region1_y1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"region1_x2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"region1_y2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
# Region 2
"region2_x1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"region2_y1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"region2_x2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"region2_y2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
# Region 3
"region3_x1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"region3_y1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"region3_x2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"region3_y2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE", "MASK", "MASK", "MASK", "INT", "BBOX", "BBOX", "BBOX")
RETURN_NAMES = ("colored_regions_image", "bbox_preview",
"mask1", "mask2", "mask3",
"number_of_regions",
"bbox1", "bbox2", "bbox3")
FUNCTION = "generate_regions"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def create_bbox(self, x1: float, y1: float, x2: float, y2: float) -> Dict:
"""Create bbox with debug output"""
print(f"Creating BBOX: x1={x1:.3f}, y1={y1:.3f}, x2={x2:.3f}, y2={y2:.3f}")
return {
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
"active": True
}
def create_mask_from_bbox(self, bbox: Dict, width: int, height: int) -> torch.Tensor:
"""Create mask from bbox with debug output"""
mask = torch.zeros((height, width), dtype=torch.float32)
if bbox["active"]:
x1 = int(bbox["x1"] * width)
y1 = int(bbox["y1"] * height)
x2 = int(bbox["x2"] * width)
y2 = int(bbox["y2"] * height)
print(f"Creating mask at pixels: x1={x1}, y1={y1}, x2={x2}, y2={y2}")
mask[y1:y2, x1:x2] = 1.0
return mask
def create_preview(self, masks: List[torch.Tensor], bboxes: List[Dict],
number_of_regions: int) -> Tuple[torch.Tensor, torch.Tensor]:
"""Create preview images with debug info"""
if not masks:
return torch.zeros((3, 64, 64), dtype=torch.float32), torch.zeros((3, 64, 64), dtype=torch.float32)
height, width = masks[0].shape
# Create both preview images
region_preview = Image.new("RGB", (width, height), (0, 0, 0))
bbox_preview = Image.new("RGB", (width, height), (0, 0, 0))
region_draw = ImageDraw.Draw(region_preview)
bbox_draw = ImageDraw.Draw(bbox_preview)
colors = [
(255, 0, 0), # Red - Region 1
(0, 255, 0), # Green - Region 2
(255, 255, 0), # Yellow - Region 3
]
# Store regions for ordered preview
preview_regions = []
for i in range(number_of_regions):
if bboxes[i]["active"]:
mask_np = masks[i].cpu().numpy() > 0.5
if mask_np.any():
preview_regions.append((i, mask_np, bboxes[i]))
# Draw regions in reverse order (Region 3 first, Region 1 last)
for i, mask_np, bbox in sorted(preview_regions, reverse=True):
# Draw on region preview
color_array = np.zeros((height, width, 3), dtype=np.uint8)
for c in range(3):
color_array[mask_np, c] = colors[i][c]
preview_np = np.array(region_preview)
preview_np[mask_np] = color_array[mask_np]
region_preview = Image.fromarray(preview_np)
# Draw on bbox preview - maintaining original bbox drawing order
x1 = int(bbox["x1"] * width)
y1 = int(bbox["y1"] * height)
x2 = int(bbox["x2"] * width)
y2 = int(bbox["y2"] * height)
print(f"Drawing preview for region {i}: x1={x1}, y1={y1}, x2={x2}, y2={y2}")
bbox_draw.rectangle([x1, y1, x2, y2], outline=colors[i], width=2)
return pil2tensor(region_preview), pil2tensor(bbox_preview)
def generate_regions(self,
width: int,
height: int,
number_of_regions: int,
**kwargs) -> Tuple:
try:
print(f"\nGenerating {number_of_regions} regions for {width}x{height} image")
bboxes = []
masks = []
# Create regions
for i in range(3):
if i < number_of_regions:
print(f"\nProcessing region {i+1}:")
bbox = self.create_bbox(
kwargs[f"region{i+1}_x1"],
kwargs[f"region{i+1}_y1"],
kwargs[f"region{i+1}_x2"],
kwargs[f"region{i+1}_y2"]
)
mask = self.create_mask_from_bbox(bbox, width, height)
bboxes.append(bbox)
masks.append(mask)
else:
print(f"Creating empty region {i+1}")
empty_bbox = {"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
bboxes.append(empty_bbox)
masks.append(torch.zeros((height, width), dtype=torch.float32))
# Create previews
region_preview, bbox_preview = self.create_preview(masks, bboxes, number_of_regions)
return (region_preview, bbox_preview, *masks, number_of_regions, *bboxes)
except Exception as e:
print(f"Error in generate_regions: {str(e)}")
empty_mask = torch.zeros((height, width), dtype=torch.float32)
empty_bbox = {"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
empty_preview = torch.zeros((3, height, width), dtype=torch.float32)
return (empty_preview, empty_preview,
empty_mask, empty_mask, empty_mask,
0, empty_bbox, empty_bbox, empty_bbox)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RegionMaskGenerator": RegionMaskGenerator
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RegionMaskGenerator": "Region Mask Generator"
}
+230
View File
@@ -0,0 +1,230 @@
import torch
import torch.nn.functional as F
from typing import Tuple, Dict, Optional, List
import numpy as np
from PIL import Image, ImageDraw
def pil2tensor(image):
"""Convert a PIL image to a PyTorch tensor in the expected format."""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class RegionMaskProcessor:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask1": ("MASK",),
"bbox1": ("BBOX",),
"blur_radius": ("INT", {
"default": 5,
"min": 0,
"max": 32,
"step": 1,
"display": "Blur Radius"
}),
"threshold": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "Mask Threshold"
}),
"feather_edges": ("BOOLEAN", {
"default": True,
"display": "Feather Edges"
}),
"number_of_regions": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"display": "Number of Regions"
}),
},
"optional": {
"mask2": ("MASK",),
"bbox2": ("BBOX",),
"mask3": ("MASK",),
"bbox3": ("BBOX",),
}
}
RETURN_TYPES = ("MASK", "BBOX", "MASK", "BBOX", "MASK", "BBOX", "IMAGE", "INT")
RETURN_NAMES = ("processed_mask1", "processed_bbox1",
"processed_mask2", "processed_bbox2",
"processed_mask3", "processed_bbox3",
"preview_image", "region_count")
FUNCTION = "process_regions"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def apply_gaussian_blur(self, mask: torch.Tensor, radius: int) -> torch.Tensor:
"""Apply gaussian blur to mask edges"""
if radius <= 0:
return mask
kernel_size = 2 * radius + 1
sigma = radius / 3.0
if len(mask.shape) == 2:
mask = mask.unsqueeze(0).unsqueeze(0)
kernel_1d = torch.exp(torch.linspace(-radius, radius, kernel_size).pow(2) / (-2 * sigma ** 2))
kernel_1d = kernel_1d / kernel_1d.sum()
padding = radius
kernel_h = kernel_1d.unsqueeze(0).unsqueeze(0).unsqueeze(0).to(mask.device)
kernel_v = kernel_1d.unsqueeze(0).unsqueeze(0).unsqueeze(-1).to(mask.device)
mask = F.pad(mask, (padding, padding, 0, 0), mode='reflect')
mask = F.conv2d(mask, kernel_h)
mask = F.pad(mask, (0, 0, padding, padding), mode='reflect')
mask = F.conv2d(mask, kernel_v)
return mask.squeeze()
def apply_feathering(self, mask: torch.Tensor, bbox: Dict, radius: int) -> Tuple[torch.Tensor, Dict]:
"""Apply feathering to mask edges while preserving bbox boundaries"""
if radius <= 0 or not bbox["active"]:
return mask, bbox
height, width = mask.shape
x1 = int(bbox["x1"] * width)
y1 = int(bbox["y1"] * height)
x2 = int(bbox["x2"] * width)
y2 = int(bbox["y2"] * height)
inner_mask = torch.zeros_like(mask)
inner_mask[y1+radius:y2-radius, x1+radius:x2-radius] = 1.0
edge_mask = mask - inner_mask
if edge_mask.any():
blurred = self.apply_gaussian_blur(mask, radius)
result = mask.clone()
result[edge_mask > 0] = blurred[edge_mask > 0]
else:
result = mask
return result, bbox
def process_single_region(self,
mask: torch.Tensor,
bbox: Dict,
blur_radius: int,
threshold: float,
feather_edges: bool) -> Tuple[torch.Tensor, Dict]:
"""Process a single mask-bbox pair"""
if mask is None or not bbox["active"]:
return mask, bbox
try:
processed = (mask > threshold).float()
if feather_edges and blur_radius > 0:
processed, bbox = self.apply_feathering(processed, bbox, blur_radius)
elif blur_radius > 0:
processed = self.apply_gaussian_blur(processed, blur_radius)
return processed, bbox
except Exception as e:
print(f"Error processing region: {str(e)}")
return mask, bbox
def create_preview(self, masks: List[torch.Tensor], bboxes: List[Dict],
number_of_regions: int) -> torch.Tensor:
"""Create preview of processed regions with PIL for consistent coloring"""
if not masks:
return torch.zeros((3, 64, 64), dtype=torch.float32)
height, width = masks[0].shape
# Create PIL Image for preview
preview = Image.new("RGB", (width, height), (0, 0, 0))
colors = [
(255, 0, 0), # Red - Region 1
(0, 255, 0), # Green - Region 2
(255, 255, 0), # Yellow - Region 3
]
# Store regions for ordered preview
preview_regions = []
for i in range(number_of_regions):
if bboxes[i]["active"] and masks[i] is not None:
mask_np = masks[i].cpu().numpy() > 0.5
preview_regions.append((i, mask_np))
# Draw regions in reverse order (Region 3 first, Region 1 last)
for i, mask_np in sorted(preview_regions, reverse=True):
color_array = np.zeros((height, width, 3), dtype=np.uint8)
color_array[mask_np] = colors[i]
# Convert to PIL and composite
region_img = Image.fromarray(color_array, 'RGB')
preview = Image.alpha_composite(
preview.convert('RGBA'),
Image.merge('RGBA', (*region_img.split(), Image.fromarray((mask_np * 255).astype(np.uint8))))
)
return pil2tensor(preview.convert('RGB'))
def process_regions(self,
mask1: torch.Tensor,
bbox1: Dict,
blur_radius: int,
threshold: float,
feather_edges: bool,
number_of_regions: int,
mask2: Optional[torch.Tensor] = None,
bbox2: Optional[Dict] = None,
mask3: Optional[torch.Tensor] = None,
bbox3: Optional[Dict] = None) -> Tuple:
try:
# Process each mask-bbox pair
mask_bbox_pairs = [
(mask1, bbox1),
(mask2, bbox2) if mask2 is not None else (None, None),
(mask3, bbox3) if mask3 is not None else (None, None),
]
processed_masks = []
processed_bboxes = []
active_count = 0
for i, (mask, bbox) in enumerate(mask_bbox_pairs):
if i < number_of_regions and mask is not None and bbox is not None:
proc_mask, proc_bbox = self.process_single_region(
mask, bbox, blur_radius, threshold, feather_edges
)
if proc_bbox["active"]:
active_count += 1
processed_masks.append(proc_mask)
processed_bboxes.append(proc_bbox)
else:
empty_mask = torch.zeros_like(mask1)
empty_bbox = {"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
processed_masks.append(empty_mask)
processed_bboxes.append(empty_bbox)
# Create preview
preview = self.create_preview(processed_masks, processed_bboxes, number_of_regions)
return (*[item for pair in zip(processed_masks, processed_bboxes) for item in pair],
preview, active_count)
except Exception as e:
print(f"Error processing regions: {str(e)}")
empty_mask = torch.zeros_like(mask1)
empty_bbox = {"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
empty_preview = torch.zeros((3, mask1.shape[0], mask1.shape[1]), dtype=torch.float32)
return (empty_mask, empty_bbox, empty_mask, empty_bbox,
empty_mask, empty_bbox,
empty_preview, 0)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RegionMaskProcessor": RegionMaskProcessor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RegionMaskProcessor": "Region Mask Processor"
}
+270
View File
@@ -0,0 +1,270 @@
import torch
from typing import Tuple, Dict, Optional, List
import numpy as np
from PIL import Image, ImageDraw
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class RegionMaskValidator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask1": ("MASK",),
"bbox1": ("BBOX",),
"number_of_regions": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"step": 1
}),
"min_region_size": ("INT", {
"default": 64,
"min": 32,
"max": 512,
"step": 32,
"display": "Minimum Region Size (px)"
}),
"max_overlap": ("FLOAT", {
"default": 0.1,
"min": 0.0,
"max": 0.5,
"step": 0.01,
"display": "Maximum Region Overlap"
}),
},
"optional": {
"mask2": ("MASK",),
"bbox2": ("BBOX",),
"mask3": ("MASK",),
"bbox3": ("BBOX",),
}
}
RETURN_TYPES = ("MASK", "BBOX", "MASK", "BBOX", "MASK", "BBOX",
"INT", "BOOLEAN", "STRING", "IMAGE")
RETURN_NAMES = ("valid_mask1", "valid_bbox1",
"valid_mask2", "valid_bbox2",
"valid_mask3", "valid_bbox3",
"valid_region_count", "is_valid", "validation_message",
"validation_preview")
FUNCTION = "validate_regions"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def get_region_dimensions(self, bbox: Dict, width: int, height: int) -> Tuple[int, int, Tuple[int, int]]:
"""Calculate region dimensions in pixels"""
if not bbox["active"]:
return 0, (0, 0)
x1 = int(bbox["x1"] * width)
y1 = int(bbox["y1"] * height)
x2 = int(bbox["x2"] * width)
y2 = int(bbox["y2"] * height)
w = x2 - x1
h = y2 - y1
area = w * h
print(f"Region dimensions: {w}x{h} pixels")
return area, (w, h)
def calculate_overlap(self, bbox1: Dict, bbox2: Dict, width: int, height: int) -> Tuple[Tuple[int, int], float]:
"""Calculate overlap dimensions and ratio"""
if not (bbox1["active"] and bbox2["active"]):
return (0, 0), 0.0
# Convert to pixel coordinates
x1_1 = int(bbox1["x1"] * width)
y1_1 = int(bbox1["y1"] * height)
x2_1 = int(bbox1["x2"] * width)
y2_1 = int(bbox1["y2"] * height)
x1_2 = int(bbox2["x1"] * width)
y1_2 = int(bbox2["y1"] * height)
x2_2 = int(bbox2["x2"] * width)
y2_2 = int(bbox2["y2"] * height)
# Calculate intersection
x_left = max(x1_1, x1_2)
y_top = max(y1_1, y1_2)
x_right = min(x2_1, x2_2)
y_bottom = min(y2_1, y2_2)
if x_right > x_left and y_bottom > y_top:
overlap_width = x_right - x_left
overlap_height = y_bottom - y_top
overlap_area = overlap_width * overlap_height
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
smaller_area = min(area1, area2)
overlap_ratio = overlap_area / smaller_area
print(f"Overlap dimensions: {overlap_width}x{overlap_height} pixels ({overlap_ratio:.1%})")
return (overlap_width, overlap_height), overlap_ratio
return (0, 0), 0.0
def create_validation_preview(self, masks: List[torch.Tensor], bboxes: List[Dict],
number_of_regions: int, is_valid: bool,
messages: List[str], img_width: int, img_height: int) -> torch.Tensor:
"""Create visual validation feedback with improved text rendering"""
if not masks:
return torch.zeros((3, 64, 64), dtype=torch.float32)
preview = Image.new("RGB", (img_width, img_height), (0, 0, 0))
draw = ImageDraw.Draw(preview)
# Colors for valid/invalid regions
colors = {
'valid': [(0, 255, 0), (0, 200, 0), (0, 150, 0)], # Green shades
'invalid': [(255, 0, 0), (200, 0, 0), (150, 0, 0)] # Red shades
}
# Draw regions with validation status and improved text
for i, (mask, bbox) in enumerate(zip(masks[:number_of_regions], bboxes[:number_of_regions])):
if bbox["active"]:
x1 = int(bbox["x1"] * img_width)
y1 = int(bbox["y1"] * img_height)
x2 = int(bbox["x2"] * img_width)
y2 = int(bbox["y2"] * img_height)
w = x2 - x1
h = y2 - y1
color = colors['valid' if is_valid else 'invalid'][i]
# Draw thicker rectangle outline
draw.rectangle([x1, y1, x2, y2], outline=color, width=4)
# Improved region label with dimensions
label = f"R{i+1}: {w}x{h}"
# Position text with offset from corner and draw twice for better visibility
text_x = x1 + 10
text_y = y1 + 10
# Draw text shadow/outline for better contrast
shadow_offset = 2
shadow_color = (0, 0, 0)
for dx in [-shadow_offset, shadow_offset]:
for dy in [-shadow_offset, shadow_offset]:
draw.text((text_x + dx, text_y + dy), label, fill=shadow_color, font=None, size=64)
# Draw main text
draw.text((text_x, text_y), label, fill=color, font=None, size=64)
# If region is invalid, add error message below the label
if not is_valid and i < len(messages):
error_y = text_y + 30 # Position error message below label
# Draw error message with shadow for contrast
for dx in [-shadow_offset, shadow_offset]:
for dy in [-shadow_offset, shadow_offset]:
draw.text((text_x + dx, error_y + dy), messages[i], fill=shadow_color, font=None, size=20)
draw.text((text_x, error_y), messages[i], fill=color, font=None, size=20)
return pil2tensor(preview)
def validate_regions(self,
mask1: torch.Tensor,
bbox1: Dict,
number_of_regions: int,
min_region_size: int,
max_overlap: float,
mask2: Optional[torch.Tensor] = None,
bbox2: Optional[Dict] = None,
mask3: Optional[torch.Tensor] = None,
bbox3: Optional[Dict] = None) -> Tuple:
try:
print(f"\nValidating {number_of_regions} regions:")
messages = []
is_valid = True
height, width = mask1.shape
print(f"Canvas size: {width}x{height} pixels")
# Collect regions
regions = [
(mask1, bbox1),
(mask2, bbox2) if mask2 is not None else (None, None),
(mask3, bbox3) if mask3 is not None else (None, None),
]
# Validate each region
valid_regions = []
valid_count = 0
for i, (mask, bbox) in enumerate(regions):
if i < number_of_regions and mask is not None and bbox is not None:
print(f"\nValidating Region {i+1}:")
# Check region size
_, (w, h) = self.get_region_dimensions(bbox, width, height)
if w < min_region_size or h < min_region_size:
message = f"Region {i+1} too small: {w}x{h} pixels (minimum: {min_region_size}x{min_region_size})"
print(f"Failed: {message}")
messages.append(message)
is_valid = False
bbox = bbox.copy()
bbox["active"] = False
else:
print(f"Passed: Region {i+1} size check ({w}x{h} pixels)")
valid_count += 1
valid_regions.append((mask, bbox))
else:
valid_regions.append((
torch.zeros_like(mask1),
{"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
))
# Check overlaps
if valid_count > 1:
print("\nChecking region overlaps:")
for i in range(len(valid_regions)):
for j in range(i + 1, len(valid_regions)):
mask_i, bbox_i = valid_regions[i]
mask_j, bbox_j = valid_regions[j]
if bbox_i["active"] and bbox_j["active"]:
print(f"Checking overlap between regions {i+1} and {j+1}:")
(ow, oh), overlap_ratio = self.calculate_overlap(bbox_i, bbox_j, width, height)
if overlap_ratio > max_overlap:
message = f"Excessive overlap ({ow}x{oh} pixels, {overlap_ratio:.1%}) between regions {i+1} and {j+1}"
print(f"Failed: {message}")
messages.append(message)
is_valid = False
# Create validation message
validation_message = "All regions valid" if is_valid else "\n".join(messages)
print(f"\nValidation {'passed' if is_valid else 'failed'}:")
print(validation_message)
# Create validation preview
preview = self.create_validation_preview(
[r[0] for r in valid_regions],
[r[1] for r in valid_regions],
number_of_regions,
is_valid,
messages,
width,
height
)
return (*[item for region in valid_regions for item in region],
valid_count, is_valid, validation_message, preview)
except Exception as e:
print(f"Validation error: {str(e)}")
empty_mask = torch.zeros_like(mask1)
empty_bbox = {"x1": 0.0, "y1": 0.0, "x2": 0.0, "y2": 0.0, "active": False}
empty_preview = torch.zeros((3, height, width), dtype=torch.float32)
return (empty_mask, empty_bbox, empty_mask, empty_bbox,
empty_mask, empty_bbox,
0, False, f"Validation error: {str(e)}", empty_preview)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"RegionMaskValidator": RegionMaskValidator
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RegionMaskValidator": "Region Mask Validator"
}
+90
View File
@@ -0,0 +1,90 @@
import torch
import numpy as np
from typing import Tuple
class RegionOverlayVisualizer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"region_preview": ("IMAGE",),
"opacity": ("FLOAT", {
"default": 0.3,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "Overlay Opacity"
}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "visualize_regions"
CATEGORY = "ControlAltAI Nodes/Flux Region"
def visualize_regions(
self,
image: torch.Tensor,
region_preview: torch.Tensor,
opacity: float,
) -> Tuple[torch.Tensor]:
try:
print("\n=== Starting Region Overlay Visualization ===")
print(f"Initial shapes - Image: {image.shape}, Preview: {region_preview.shape}")
# Ensure input tensors are in [B, H, W, C] format
if len(image.shape) == 3:
image = image.unsqueeze(0)
if len(region_preview.shape) == 3:
region_preview = region_preview.unsqueeze(0)
# Get working copies
base_image = image.clone()
preview = region_preview.clone()
# Convert to numpy for mask creation (keeping batch and HWC format)
preview_np = (preview * 255).byte().cpu().numpy()
# Create mask based on preview content (operating on the last dimension - channels)
color_sum = np.sum(preview_np, axis=-1) # Sum across color channels
max_channel = np.max(preview_np, axis=-1)
min_channel = np.min(preview_np, axis=-1)
# Create binary mask where content exists
mask = (
(color_sum > 50) &
(max_channel > 30) &
((max_channel - min_channel) > 10)
)
# Expand mask to match input dimensions
mask = mask[..., None] # Add channel dimension back
mask = torch.from_numpy(mask).to(image.device)
print(f"Mask shape: {mask.shape}")
print(f"Masked pixels: {mask.sum().item()}/{mask.numel()} ({mask.sum().item()/mask.numel()*100:.2f}%)")
# Apply blending only where mask is True
result = torch.where(
mask.bool(),
(1 - opacity) * base_image + opacity * preview,
base_image
)
print(f"Final shape: {result.shape}")
return (result,)
except Exception as e:
print(f"Error in visualization: {str(e)}")
import traceback
traceback.print_exc()
return (image,)
NODE_CLASS_MAPPINGS = {
"RegionOverlayVisualizer": RegionOverlayVisualizer
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RegionOverlayVisualizer": "Region Overlay Visualizer"
}