Ray1ee01's picture
Upload folder using huggingface_hub
2cf467c verified
Raw
History Blame Contribute Delete
47.6 kB
"""Main optimization function for SDF-based layout optimization."""
import os
import shutil
from typing import List, Tuple, Optional, Dict
import numpy as np
import torch
import torch.nn.functional as F
from modules.infographics_generator.layout_system import parameters as params
from .core import (
load_binary_mask_from_rgba,
tight_bbox_ratio,
binary_to_sdf_norm,
dilate_mask,
make_container_grid,
sdf_to_softmask,
area_sum,
)
from .bbox import (
bbox_aspect_from_unconstrained,
unconstrained_from_bbox,
)
from .losses import (
compute_alignment_consistency_loss,
compute_alignment_similarity_loss,
compute_readability_loss,
compute_proximity_ratio_loss,
compute_visual_balance_loss,
compute_position_size_similarity_loss,
)
from .visualization import (
visualize_optimization_progress,
visualize_final_result,
save_composite_image,
)
def optimize(
png_list, # List of image paths for N nodes
original_png_list=None, # List of original image paths for saving composite
Wc=1000, Hc=1000,
opt_res_list=params.OPT_RES_LIST, # optimize on these resolutions
outer_rounds=params.OUTER_ROUNDS, # augmented-lagrangian outer updates per stage
inner_steps=params.INNER_STEPS, # gradient steps per outer round
tau_schedule=params.TAU_SCHEDULE,
rho_init=params.RHO_INIT, # initial penalty parameter
rho_mult=params.RHO_MULT, # penalty multiplier
lr=params.LEARNING_RATE,
size_min=params.SIZE_MIN, # Legacy parameter for backward compatibility
min_sizes=None, # List of (min_width, min_height) tuples for each element
pen_weight=params.PEN_WEIGHT, # weight for penetration penalty
pen_eta_px=params.PEN_ETA_PX,
reference_bboxes=None, # List of reference element bboxes (x, y, w, h) from Example layout
reference_parent_bbox=None, # Reference parent container bbox (x, y, w, h)
w_similarity=params.W_SIMILARITY, # weight for position/size similarity loss
size_rules=None, # List of tuples (source_idx, target_idx) for size hierarchy rules
w_readability=params.W_READABILITY, # weight for readability loss
w_alignment_consistency=params.W_ALIGNMENT_CONSISTENCY, # weight for alignment consistency loss
alignment_constraint=None, # Dictionary with alignment constraint from JSON (direction, value)
w_alignment_similarity=params.W_ALIGNMENT_SIMILARITY, # weight for alignment similarity loss
proximity_info=None, # Dictionary with container hierarchy info for proximity ratio calculation
w_proximity=params.W_PROXIMITY, # weight for proximity ratio loss
w_data_ink=params.W_DATA_INK, # weight for data ink loss (maximize union area)
w_visual_balance=params.W_VISUAL_BALANCE, # weight for visual balance loss
min_gap_px=20.0, # minimum gap between elements in pixels
device=None,
save_prefix=None, # Prefix for saving result images (None = use default names)
debug=False, # Enable debug mode: visualization and detailed logging
):
"""Main SDF-based layout optimization function.
Args:
png_list: List of image paths for N nodes
original_png_list: List of original image paths for saving composite
Wc, Hc: Container width and height
opt_res_list: Optimize on these resolutions
outer_rounds: Augmented-lagrangian outer updates per stage
inner_steps: Gradient steps per outer round
tau_schedule: Schedule for tau parameter
rho_init: Initial penalty parameter
rho_mult: Penalty multiplier
lr: Learning rate
size_min: Legacy minimum size parameter
min_sizes: List of (min_width, min_height) tuples for each element
pen_weight: Weight for penetration penalty
pen_eta_px: Eta parameter for penetration penalty
reference_bboxes: List of reference element bboxes from Example layout
reference_parent_bbox: Reference parent container bbox
w_similarity: Weight for position/size similarity loss
size_rules: List of tuples (source_idx, target_idx) for size hierarchy rules
w_readability: Weight for readability loss
w_alignment_consistency: Weight for alignment consistency loss
alignment_constraint: Dictionary with alignment constraint from JSON
w_alignment_similarity: Weight for alignment similarity loss
proximity_info: Dictionary with container hierarchy info
w_proximity: Weight for proximity ratio loss
w_data_ink: Weight for data ink loss
w_visual_balance: Weight for visual balance loss
min_gap_px: Minimum gap between elements in pixels
device: PyTorch device
save_prefix: Prefix for saving result images
debug: Enable debug mode (visualization and detailed logging)
Returns:
List of final bounding boxes [(x, y, w, h), ...]
"""
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Device:", device)
# Validate png_list
if png_list is None or len(png_list) < 1:
raise ValueError("png_list must be provided with at least one image path")
num_nodes = len(png_list)
# Handle original_png_list
if original_png_list is None:
original_png_list = png_list
# Set dilation_radii to 100 for all elements
dilation_radii = [20.0] * num_nodes
# print(f"Number of nodes: {num_nodes}")
# print(f"Dilation radii: {dilation_radii}")
# Validate reference bboxes
if reference_bboxes is None:
reference_bboxes = []
if reference_parent_bbox is None:
reference_parent_bbox = (0.0, 0.0, float(Wc), float(Hc))
# Normalize reference bboxes: subtract x_min and y_min to make coordinates start from 0
if reference_bboxes and len(reference_bboxes) > 0:
# Find minimum x and y across all reference bboxes
x_min = min(bbox[0] for bbox in reference_bboxes)
y_min = min(bbox[1] for bbox in reference_bboxes)
# Subtract x_min and y_min from all bboxes
normalized_reference_bboxes = []
for bbox in reference_bboxes:
if isinstance(bbox, (tuple, list)) and len(bbox) >= 4:
normalized_bbox = (bbox[0] - x_min, bbox[1] - y_min, bbox[2], bbox[3])
elif isinstance(bbox, dict):
normalized_bbox = {
'x': bbox.get('x', 0) - x_min,
'y': bbox.get('y', 0) - y_min,
'width': bbox.get('width', bbox.get('w', 100)),
'height': bbox.get('height', bbox.get('h', 100))
}
else:
normalized_bbox = bbox
normalized_reference_bboxes.append(normalized_bbox)
reference_bboxes = normalized_reference_bboxes
# Also normalize reference_parent_bbox
if reference_parent_bbox:
x_p, y_p, w_p, h_p = reference_parent_bbox
reference_parent_bbox = (x_p - x_min, y_p - y_min, w_p, h_p)
# print(f"Normalized reference bboxes: subtracted x_min={x_min:.1f}, y_min={y_min:.1f}")
# Validate size rules
if size_rules is None:
size_rules = []
# Validate and set up min_sizes
if min_sizes is None:
# Use default values for all elements
min_sizes = [(params.MIN_WIDTH_DEFAULT, params.MIN_HEIGHT_DEFAULT)] * num_nodes
elif len(min_sizes) < num_nodes:
# Extend with default values
default_size = (params.MIN_WIDTH_DEFAULT, params.MIN_HEIGHT_DEFAULT)
min_sizes = min_sizes + [default_size] * (num_nodes - len(min_sizes))
elif len(min_sizes) > num_nodes:
# Truncate to num_nodes
min_sizes = min_sizes[:num_nodes]
# print(f"Reference bboxes: {len(reference_bboxes)} elements")
# print(f"Reference parent bbox: {reference_parent_bbox}")
# print(f"Size rules: {len(size_rules)} rules")
# print(f"Similarity weight: {w_similarity}, Readability weight: {w_readability}")
# print(f"Min sizes: {min_sizes}")
# print(f"Debug mode: {debug}")
# Create visualization folder for this optimization run (only in debug mode)
viz_folder = None
if debug:
if save_prefix:
viz_folder = f"{save_prefix}_visualization"
else:
viz_folder = "optimization_visualization"
# Clear existing folder contents if it exists
if os.path.exists(viz_folder):
shutil.rmtree(viz_folder)
os.makedirs(viz_folder, exist_ok=True)
# print(f"Visualization folder created: {viz_folder}")
# load masks for all nodes
masks = []
ratios = []
for i, png_path in enumerate(png_list):
if png_path is None:
raise ValueError(f"Image path at index {i} is None")
mask = load_binary_mask_from_rgba(png_path)
# Apply dilation with radius 100 to all elements
mask = dilate_mask(mask, dilation_radii[i])
# print(f"Applied dilation to node {i} mask with radius {dilation_radii[i]:.1f} pixels")
masks.append(mask)
r, _ = tight_bbox_ratio(mask)
ratios.append(r)
# print(f"Node {i} aspect ratio (tight alpha bbox): r={r:.4f}")
# precompute SDF templates for all nodes
sdf_norms = []
sdf_tensors = []
for i, mask in enumerate(masks):
sdf_norm = binary_to_sdf_norm(mask, pad=16)
sdf_norms.append(sdf_norm)
sdf_t = torch.from_numpy(sdf_norm)[None, None].to(device)
sdf_tensors.append(sdf_t)
# learnable parameters: (tx,ty,ts) for each object
# Initialize from reference_bboxes if available, otherwise use zeros
opt_params = []
for i in range(num_nodes):
if reference_bboxes and i < len(reference_bboxes):
# Initialize from reference bbox
ref_bbox = reference_bboxes[i]
if isinstance(ref_bbox, (tuple, list)) and len(ref_bbox) >= 4:
ref_x, ref_y, ref_w, ref_h = ref_bbox[0], ref_bbox[1], ref_bbox[2], ref_bbox[3]
elif isinstance(ref_bbox, dict):
ref_x = ref_bbox.get("x", 0)
ref_y = ref_bbox.get("y", 0)
ref_w = ref_bbox.get("width", ref_bbox.get("w", 100))
ref_h = ref_bbox.get("height", ref_bbox.get("h", 100))
else:
ref_x, ref_y, ref_w, ref_h = 0, 0, 100, 100
# Convert reference bbox to unconstrained parameters
# Use material's aspect ratio (ratios[i]) instead of JSON's w/h
min_width, min_height = min_sizes[i]
tx_init, ty_init, ts_init = unconstrained_from_bbox(
ref_x, ref_y, ref_w, ref_h,
Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
# print(f"Node {i}: Initializing from reference bbox ({ref_x:.1f}, {ref_y:.1f}, {ref_w:.1f}, {ref_h:.1f}), "
# f"adjusted to aspect ratio {ratios[i]:.4f}")
else:
# Initialize with zeros (default)
tx_init, ty_init, ts_init = 0.0, 0.0, 0.0
tx = torch.nn.Parameter(torch.tensor(tx_init, device=device))
ty = torch.nn.Parameter(torch.tensor(ty_init, device=device))
ts = torch.nn.Parameter(torch.tensor(ts_init, device=device))
opt_params.extend([tx, ty, ts])
opt = torch.optim.Adam(opt_params, lr=lr)
# augmented lagrangian multipliers for constraint g = A_inter = 0
lam = torch.tensor(0.0, device=device)
rho = torch.tensor(rho_init, device=device)
tau_list = list(tau_schedule)
if len(tau_list) < outer_rounds:
tau_list += [tau_list[-1]] * (outer_rounds - len(tau_list))
# staged optimization over resolutions
for stage_idx, stage_res in enumerate(opt_res_list):
Hs = Ws = int(stage_res)
X, Y = make_container_grid(Hs, Ws, device=device)
# print(f"\n=== Stage optimize at {Ws}x{Hs} (container {Wc}x{Hc}) ===")
# Visualize initial state before optimization (only for first stage and only in debug mode)
if stage_idx == 0 and debug:
with torch.no_grad():
# Use full resolution for visualization
X_init, Y_init = make_container_grid(Hc, Wc, device=device)
initial_bboxes = []
initial_softmasks = []
for i in range(num_nodes):
tx_idx = i * 3
ty_idx = i * 3 + 1
ts_idx = i * 3 + 2
tx = opt_params[tx_idx]
ty = opt_params[ty_idx]
ts = opt_params[ts_idx]
min_width, min_height = min_sizes[i]
x, y, w, h = bbox_aspect_from_unconstrained(
tx, ty, ts, Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
# print(f"Node {i} initial bbox: ({x.item():.1f}, {y.item():.1f}, {w.item():.1f}, {h.item():.1f})")
initial_bboxes.append((x.item(), y.item(), w.item(), h.item()))
# Use smaller tau for initial visualization to show actual mask shape
m, _ = sdf_to_softmask(sdf_tensors[i], x, y, w, h, X_init, Y_init, tau_px=0.5)
initial_softmasks.append(m)
# Compute initial loss values for display
initial_union = torch.ones_like(initial_softmasks[0])
for m in initial_softmasks:
initial_union = initial_union * (1.0 - m)
initial_union = 1.0 - initial_union
initial_A_union = initial_union.sum().item() # Full resolution, da=1
initial_inter = torch.zeros_like(initial_softmasks[0])
for i in range(num_nodes):
for j in range(i + 1, num_nodes):
initial_inter = initial_inter + initial_softmasks[i] * initial_softmasks[j]
initial_A_inter = initial_inter.sum().item() # Full resolution, da=1
# Compute visual balance loss for initial state (using bboxes)
initial_bboxes_tensors = []
for i in range(num_nodes):
tx_idx = i * 3
ty_idx = i * 3 + 1
ts_idx = i * 3 + 2
tx = opt_params[tx_idx]
ty = opt_params[ty_idx]
ts = opt_params[ts_idx]
min_width, min_height = min_sizes[i]
x, y, w, h = bbox_aspect_from_unconstrained(
tx, ty, ts, Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
initial_bboxes_tensors.append((x, y, w, h))
L_visual_balance_init = compute_visual_balance_loss(
initial_bboxes_tensors, Wc, Hc, device=device
)
initial_loss_info = {
'A_union': initial_A_union,
'A_inter': initial_A_inter,
'visual_balance': w_visual_balance * L_visual_balance_init.item(),
}
initial_save_path = os.path.join(viz_folder, f"initial_stage{stage_idx}.png")
visualize_optimization_progress(
initial_softmasks, initial_bboxes, Wc, Hc,
epoch=-1, loss_info=initial_loss_info,
save_path=initial_save_path
)
for k in range(outer_rounds):
tau_px = float(tau_list[min(k, len(tau_list)-1)])
for t in range(inner_steps):
opt.zero_grad(set_to_none=True)
# Compute bboxes for all nodes
bboxes = []
softmasks = []
distances = []
for i in range(num_nodes):
tx_idx = i * 3
ty_idx = i * 3 + 1
ts_idx = i * 3 + 2
tx = opt_params[tx_idx]
ty = opt_params[ty_idx]
ts = opt_params[ts_idx]
min_width, min_height = min_sizes[i]
x, y, w, h = bbox_aspect_from_unconstrained(
tx, ty, ts, Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
bboxes.append((x, y, w, h))
m, d_px = sdf_to_softmask(sdf_tensors[i], x, y, w, h, X, Y, tau_px=tau_px)
softmasks.append(m)
distances.append(d_px)
# Compute union: 1 - product of (1 - mask_i)
union = torch.ones_like(softmasks[0])
for m in softmasks:
union = union * (1.0 - m)
union = 1.0 - union
# Compute intersection: sum of all pairwise intersections
inter = torch.zeros_like(softmasks[0])
for i in range(num_nodes):
for j in range(i + 1, num_nodes):
inter = inter + softmasks[i] * softmasks[j]
A_union = area_sum(union, Wc, Hc)
A_inter = area_sum(inter, Wc, Hc) # must go to 0
# Visual balance loss (using bboxes directly for differentiability)
L_visual_balance = compute_visual_balance_loss(
bboxes, Wc, Hc, device=device
)
# Penetration loss: based on actual bbox gap (not limited by SDF range)
# Penalize when gap < min_gap_px (ensures minimum gap between elements)
# Use ReLU for hard cutoff: no penalty when gap >= min_gap_px
# For "layer" container type, skip this loss (overlapping is allowed)
L_pen = torch.tensor(0.0, device=device)
# # Check if this is a layer container (overlapping allowed)
# is_layer = False
# if proximity_info:
# container_types = proximity_info.get("types", [])
# if len(container_types) > 0:
# is_layer = (container_types[0] == "layer")
# if not is_layer:
# # Only apply penetration penalty for row/column layouts
# for i in range(num_nodes):
# for j in range(i + 1, num_nodes):
# x_i, y_i, w_i, h_i = bboxes[i]
# x_j, y_j, w_j, h_j = bboxes[j]
# # Compute gap in each dimension (negative if overlapping)
# gap_x = torch.max(x_j - (x_i + w_i), x_i - (x_j + w_j))
# gap_y = torch.max(y_j - (y_i + h_i), y_i - (y_j + h_j))
# # Combined gap logic:
# # - If both separated (both positive): Euclidean distance
# # - If both overlapping (both negative): use MAX (smallest overlap, easiest to fix)
# # - If one separated, one overlapping: use the separated one (that's the actual gap)
# if gap_x >= 0 and gap_y >= 0:
# # Both separated: Euclidean distance
# gap = torch.sqrt(gap_x * gap_x + gap_y * gap_y)
# elif gap_x < 0 and gap_y < 0:
# # Both overlapping: use the smaller overlap (easier to separate)
# gap = torch.max(gap_x, gap_y)
# else:
# # One separated, one overlapping: they're aligned in overlapping dimension
# # Use the separated dimension's gap
# gap = torch.max(gap_x, gap_y)
# # Penalty if gap < min_gap_px
# L_pen = L_pen + F.relu(min_gap_px - gap)
# Position/Size similarity loss
L_similarity = torch.tensor(0.0, device=device)
if reference_bboxes and len(reference_bboxes) >= num_nodes:
# Current generated bboxes as tensors
generated_bboxes = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
# Generated parent container bbox (current container)
generated_parent_bbox = (0.0, 0.0, float(Wc), float(Hc))
L_similarity = compute_position_size_similarity_loss(
reference_bboxes[:num_nodes],
generated_bboxes,
reference_parent_bbox,
generated_parent_bbox,
device=device
)
# Readability loss (size hierarchy consistency)
L_readability = torch.tensor(0.0, device=device)
if size_rules and len(size_rules) > 0:
generated_bboxes_readability = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
L_readability = compute_readability_loss(
size_rules,
generated_bboxes_readability,
size_ratio_threshold=params.SIZE_RATIO_THRESHOLD,
device=device
)
# Alignment consistency loss (hierarchical alignment)
L_alignment_consistency = torch.tensor(0.0, device=device)
if reference_bboxes and len(reference_bboxes) >= num_nodes:
generated_bboxes_alignment = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
generated_parent_bbox_alignment = (0.0, 0.0, float(Wc), float(Hc))
L_alignment_consistency = compute_alignment_consistency_loss(
reference_bboxes[:num_nodes],
generated_bboxes_alignment,
reference_parent_bbox,
generated_parent_bbox_alignment,
device=device
)
# Alignment similarity loss (based on JSON constraint)
L_alignment_similarity = torch.tensor(0.0, device=device)
if alignment_constraint and w_alignment_similarity > 0:
generated_bboxes_alignment_sim = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
container_bbox_alignment = (0.0, 0.0, float(Wc), float(Hc))
L_alignment_similarity = compute_alignment_similarity_loss(
generated_bboxes_alignment_sim,
container_bbox_alignment,
alignment_constraint,
device=device
)
# Proximity ratio loss
L_proximity = torch.tensor(0.0, device=device)
if proximity_info and w_proximity > 0:
# Extract information from proximity_info
container_bboxes = proximity_info.get("containers", [])
child_bboxes_list = proximity_info.get("children", [])
grandchild_bboxes_list = proximity_info.get("grandchildren", [])
container_types = proximity_info.get("types", [])
container_weights = proximity_info.get("weights", None)
# Convert current generated bboxes to tuples for proximity calculation
# For N-element case: treat as single container with N children
if len(container_bboxes) == 0:
# Simplified N-element case: create a container with N children
# Note: container type should be provided in proximity_info
container_bbox = (0.0, 0.0, float(Wc), float(Hc))
# Keep bboxes as tensors for gradient computation
child_bboxes = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
# For N-element case, grandchildren would be empty (children are leaf nodes)
grandchild_bboxes = []
container_type = container_types[0] if container_types else "row" # Default to row
L_proximity = compute_proximity_ratio_loss(
[container_bbox],
[child_bboxes],
[grandchild_bboxes],
[container_type],
container_weights=[1.0] if container_weights is None else container_weights,
epsilon=params.PROXIMITY_EPSILON,
device=device
)
else:
# Use provided proximity_info
# Keep bboxes as tensors for gradient computation
generated_bboxes_proximity = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes]
# Update child_bboxes_list with current generated bboxes if needed
# This is a simplified approach - in full implementation, we'd need to map
# generated bboxes to the correct containers
if len(child_bboxes_list) > 0 and len(child_bboxes_list[0]) == num_nodes:
# Update first container's children with generated bboxes (as tensors)
updated_child_bboxes_list = [generated_bboxes_proximity] + child_bboxes_list[1:]
else:
updated_child_bboxes_list = child_bboxes_list
L_proximity = compute_proximity_ratio_loss(
container_bboxes,
updated_child_bboxes_list,
grandchild_bboxes_list,
container_types,
container_weights,
epsilon=params.PROXIMITY_EPSILON,
device=device
)
g = A_inter
# Data ink loss: maximize union area (minimize white space)
# Negative because we want to maximize A_union (minimize -A_union)
L_data_ink = -A_union
# Scale pen_weight by rho to prevent L_pen from being overwhelmed when rho is large
# When rho is large, AL constraint dominates, so we need to scale pen_weight accordingly
pen_weight_scaled = pen_weight * (1.0 + rho.item() / 1e4)
# AL constraint on overlap + penalty term + similarity loss + readability loss + alignment consistency loss + alignment similarity loss + proximity loss + data ink loss + visual balance loss
loss = (lam * g + 0.5 * rho * g * g + pen_weight_scaled * L_pen +
w_similarity * L_similarity + w_readability * L_readability +
w_alignment_consistency * L_alignment_consistency + w_alignment_similarity * L_alignment_similarity +
w_proximity * L_proximity + w_data_ink * L_data_ink + w_visual_balance * L_visual_balance)
loss.backward()
opt.step()
# outer AL update
with torch.no_grad():
# Compute bboxes for logging and AL update
bboxes_log = []
for i in range(num_nodes):
tx_idx = i * 3
ty_idx = i * 3 + 1
ts_idx = i * 3 + 2
tx = opt_params[tx_idx]
ty = opt_params[ty_idx]
ts = opt_params[ts_idx]
min_width, min_height = min_sizes[i]
x, y, w, h = bbox_aspect_from_unconstrained(
tx, ty, ts, Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
bboxes_log.append((x, y, w, h))
# Compute A_inter for AL update (always needed)
softmasks_log = []
for i in range(num_nodes):
x, y, w, h = bboxes_log[i]
m, d_px = sdf_to_softmask(sdf_tensors[i], x, y, w, h, X, Y, tau_px=tau_px)
softmasks_log.append(m)
inter_log = torch.zeros_like(softmasks_log[0])
for i in range(num_nodes):
for j in range(i + 1, num_nodes):
inter_log = inter_log + softmasks_log[i] * softmasks_log[j]
A_inter = area_sum(inter_log, Wc, Hc)
# Update Lagrangian multiplier
lam = lam + rho * A_inter
rho = rho * rho_mult
# Detailed logging and visualization (only in debug mode)
if debug:
# Compute all loss components for detailed logging
distances_log = []
for i in range(num_nodes):
x, y, w, h = bboxes_log[i]
m, d_px = sdf_to_softmask(sdf_tensors[i], x, y, w, h, X, Y, tau_px=tau_px)
distances_log.append(d_px)
union_log = torch.ones_like(softmasks_log[0])
for m in softmasks_log:
union_log = union_log * (1.0 - m)
union_log = 1.0 - union_log
A_union = area_sum(union_log, Wc, Hc)
A_union = area_sum(union_log, Wc, Hc)
# Recompute penalty for logging (based on actual bbox gap)
# Skip for layer containers (overlapping allowed)
L_pen_val = torch.tensor(0.0, device=device)
# Check if this is a layer container
is_layer = False
if proximity_info:
container_types = proximity_info.get("types", [])
if len(container_types) > 0:
is_layer = (container_types[0] == "layer")
if not is_layer:
for i in range(num_nodes):
for j in range(i + 1, num_nodes):
x_i, y_i, w_i, h_i = bboxes_log[i]
x_j, y_j, w_j, h_j = bboxes_log[j]
# Compute gap in each dimension
gap_x = torch.max(x_j - (x_i + w_i), x_i - (x_j + w_j))
gap_y = torch.max(y_j - (y_i + h_i), y_i - (y_j + h_j))
# Combined gap logic
if gap_x >= 0 and gap_y >= 0:
# Both separated: Euclidean distance
gap = torch.sqrt(gap_x * gap_x + gap_y * gap_y)
elif gap_x < 0 and gap_y < 0:
# Both overlapping: use smaller overlap
gap = torch.max(gap_x, gap_y)
else:
# One separated, one overlapping
gap = torch.max(gap_x, gap_y)
# Penalty if gap < min_gap_px
L_pen_val = L_pen_val + F.relu(min_gap_px - gap)
# Recompute similarity loss for logging
L_similarity_val = torch.tensor(0.0, device=device)
if reference_bboxes and len(reference_bboxes) >= num_nodes:
generated_bboxes_log = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes_log]
generated_parent_bbox_log = (0.0, 0.0, float(Wc), float(Hc))
L_similarity_val = compute_position_size_similarity_loss(
reference_bboxes[:num_nodes],
generated_bboxes_log,
reference_parent_bbox,
generated_parent_bbox_log,
device=device
)
# Recompute readability loss for logging
L_readability_val = torch.tensor(0.0, device=device)
if size_rules and len(size_rules) > 0:
generated_bboxes_readability_log = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes_log]
L_readability_val = compute_readability_loss(
size_rules,
generated_bboxes_readability_log,
size_ratio_threshold=params.SIZE_RATIO_THRESHOLD,
device=device
)
# Recompute alignment consistency loss for logging
L_alignment_consistency_val = torch.tensor(0.0, device=device)
if reference_bboxes and len(reference_bboxes) >= num_nodes:
generated_bboxes_alignment_log = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes_log]
generated_parent_bbox_alignment_log = (0.0, 0.0, float(Wc), float(Hc))
L_alignment_consistency_val = compute_alignment_consistency_loss(
reference_bboxes[:num_nodes],
generated_bboxes_alignment_log,
reference_parent_bbox,
generated_parent_bbox_alignment_log,
device=device
)
# Recompute alignment similarity loss for logging
L_alignment_similarity_val = torch.tensor(0.0, device=device)
if alignment_constraint and w_alignment_similarity > 0:
generated_bboxes_alignment_sim_log = [torch.stack([bbox[0], bbox[1], bbox[2], bbox[3]]) for bbox in bboxes_log]
container_bbox_alignment_log = (0.0, 0.0, float(Wc), float(Hc))
L_alignment_similarity_val = compute_alignment_similarity_loss(
generated_bboxes_alignment_sim_log,
container_bbox_alignment_log,
alignment_constraint,
device=device
)
# Recompute proximity ratio loss for logging
L_proximity_val = torch.tensor(0.0, device=device)
if proximity_info and w_proximity > 0:
container_bboxes = proximity_info.get("containers", [])
child_bboxes_list = proximity_info.get("children", [])
grandchild_bboxes_list = proximity_info.get("grandchildren", [])
container_types = proximity_info.get("types", [])
container_weights = proximity_info.get("weights", None)
if len(container_bboxes) == 0:
container_bbox = (0.0, 0.0, float(Wc), float(Hc))
child_bboxes = [(bbox[0].item(), bbox[1].item(), bbox[2].item(), bbox[3].item()) for bbox in bboxes_log]
grandchild_bboxes = []
container_type = container_types[0] if container_types else "row" # Default to row
L_proximity_val = compute_proximity_ratio_loss(
[container_bbox],
[child_bboxes],
[grandchild_bboxes],
[container_type],
container_weights=[1.0] if container_weights is None else container_weights,
epsilon=params.PROXIMITY_EPSILON,
device=device
)
else:
generated_bboxes_proximity_log = [(bbox[0].item(), bbox[1].item(), bbox[2].item(), bbox[3].item()) for bbox in bboxes_log]
if len(child_bboxes_list) > 0 and len(child_bboxes_list[0]) == num_nodes:
updated_child_bboxes_list_log = [generated_bboxes_proximity_log] + child_bboxes_list[1:]
else:
updated_child_bboxes_list_log = child_bboxes_list
L_proximity_val = compute_proximity_ratio_loss(
container_bboxes,
updated_child_bboxes_list_log,
grandchild_bboxes_list,
container_types,
container_weights,
epsilon=params.PROXIMITY_EPSILON,
device=device
)
# Compute data ink loss for logging
L_data_ink_val = -A_union
# Compute visual balance loss for logging (using bboxes_log)
L_visual_balance_val = compute_visual_balance_loss(
bboxes_log, Wc, Hc, device=device
)
# Compute total loss for visualization
g_val = A_inter
total_loss_val = (lam.item() * g_val.item() + 0.5 * rho.item() * g_val.item() * g_val.item() +
pen_weight * L_pen_val.item() +
w_similarity * L_similarity_val.item() +
w_readability * L_readability_val.item() +
w_alignment_consistency * L_alignment_consistency_val.item() +
w_alignment_similarity * L_alignment_similarity_val.item() +
w_proximity * L_proximity_val.item() +
w_data_ink * L_data_ink_val.item() +
w_visual_balance * L_visual_balance_val.item())
print(f"[outer {k:02d}] tau={tau_px:.3f} A_union={A_union.item():.2f} A_inter={A_inter.item():.6f} "
f"L_pen={pen_weight*L_pen_val.item():.4f} L_sim={w_similarity*L_similarity_val.item():.4f} "
f"L_read={w_readability*L_readability_val.item():.4f} "
f"L_align_cons={w_alignment_consistency*L_alignment_consistency_val.item():.4f} "
f"L_align_sim={w_alignment_similarity*L_alignment_similarity_val.item():.4f} "
f"L_prox={w_proximity*L_proximity_val.item():.4f} "
f"L_data_ink={w_data_ink*L_data_ink_val.item():.4f} "
f"L_balance={w_visual_balance*L_visual_balance_val.item():.4f} "
f"lam={lam.item():.3e} rho={rho.item():.3e}")
# Visualize optimization progress at end of each outer epoch
# Use full resolution for visualization
X_viz, Y_viz = make_container_grid(Hc, Wc, device=device)
epoch_bboxes = []
epoch_softmasks = []
for i, bbox_log in enumerate(bboxes_log):
x, y, w, h = bbox_log
epoch_bboxes.append((x.item(), y.item(), w.item(), h.item()))
m_viz, _ = sdf_to_softmask(sdf_tensors[i], x, y, w, h, X_viz, Y_viz, tau_px=tau_px)
epoch_softmasks.append(m_viz)
epoch_loss_info = {
'total': total_loss_val,
'A_union': A_union.item(),
'A_inter': A_inter.item(),
'pen': L_pen_val.item(),
'similarity': w_similarity * L_similarity_val.item(),
'readability': w_readability * L_readability_val.item(),
'alignment': w_alignment_consistency * L_alignment_consistency_val.item() + w_alignment_similarity * L_alignment_similarity_val.item(),
'alignment_consistency': w_alignment_consistency * L_alignment_consistency_val.item(),
'alignment_similarity': w_alignment_similarity * L_alignment_similarity_val.item(),
'proximity': w_proximity * L_proximity_val.item(),
'data_ink': w_data_ink * L_data_ink_val.item(),
}
epoch_save_path = os.path.join(viz_folder, f"stage{stage_idx}_epoch{k:02d}.png")
visualize_optimization_progress(
epoch_softmasks, epoch_bboxes, Wc, Hc,
epoch=k, loss_info=epoch_loss_info,
save_path=epoch_save_path
)
else:
# Non-debug mode: simple logging
print(f"[outer {k:02d}] tau={tau_px:.3f} A_inter={A_inter.item():.6f} lam={lam.item():.3e} rho={rho.item():.3e}")
# final bbox (continuous)
final_bboxes = []
with torch.no_grad():
for i in range(num_nodes):
tx_idx = i * 3
ty_idx = i * 3 + 1
ts_idx = i * 3 + 2
tx = opt_params[tx_idx]
ty = opt_params[ty_idx]
ts = opt_params[ts_idx]
min_width, min_height = min_sizes[i]
x, y, w, h = bbox_aspect_from_unconstrained(
tx, ty, ts, Wc, Hc, ratios[i],
min_width=min_width, min_height=min_height,
size_min=size_min
)
final_bboxes.append((x.item(), y.item(), w.item(), h.item()))
# hard evaluation at full 1000x1000: overlap using (SDF<0) AND
with torch.no_grad():
Xf, Yf = make_container_grid(Hc, Wc, device=device)
# use a small tau for union display (not needed for hard overlap)
softmasks_f = []
distances_f = []
for i in range(num_nodes):
x, y, w, h = final_bboxes[i]
m, d_px = sdf_to_softmask(sdf_tensors[i],
torch.tensor(x, device=device),
torch.tensor(y, device=device),
torch.tensor(w, device=device),
torch.tensor(h, device=device),
Xf, Yf, tau_px=0.2)
softmasks_f.append(m)
distances_f.append(d_px)
# Compute union
union_f = torch.ones_like(softmasks_f[0])
for m in softmasks_f:
union_f = union_f * (1.0 - m)
union_f = 1.0 - union_f
A_union_f = union_f.sum().item() # da=1 at full res
# hard inside test: d_px < 0 for all pairs
hard_overlap = torch.zeros_like(softmasks_f[0])
for i in range(num_nodes):
for j in range(i + 1, num_nodes):
overlap_ij = ((distances_f[i] < 0.0) & (distances_f[j] < 0.0)).float()
hard_overlap = hard_overlap + overlap_ij
A_overlap_hard = hard_overlap.sum().item()
# print("\n=== Final Results ===")
# for i, bbox in enumerate(final_bboxes):
# print(f"bbox{i+1} (x,y,w,h) = {bbox}")
# print(f"Union area (approx, {Wc}x{Hc}) = {A_union_f:.2f} -> ratio {A_union_f/(Wc*Hc):.4f}")
# print(f"Hard overlap area (SDF<0) = {A_overlap_hard:.0f} pixels")
# Save visualization and composite images (only in debug mode)
if debug:
# Determine save paths based on prefix (save to visualization folder)
if save_prefix:
final_result_path = os.path.join(viz_folder, f"{save_prefix}_final_result.png")
composite_result_path = os.path.join(viz_folder, f"{save_prefix}_composite_result.png")
else:
final_result_path = os.path.join(viz_folder, "final_result.png")
composite_result_path = os.path.join(viz_folder, "composite_result.png")
# Visualize final result (only for 2 nodes, skip for N nodes)
# TODO: Extend visualize_final_result to support N nodes
if num_nodes == 2:
mask1_orig = load_binary_mask_from_rgba(png_list[0])
mask2_orig = load_binary_mask_from_rgba(png_list[1])
if mask1_orig is not None and mask2_orig is not None:
visualize_final_result(softmasks_f[0], softmasks_f[1], distances_f[0], distances_f[1],
final_bboxes[0], final_bboxes[1],
mask1_orig, mask2_orig, Wc, Hc,
save_path=final_result_path)
# Save composite image with original images
# Use original paths if provided, otherwise use the paths passed to optimize
save_png_list = []
for i in range(num_nodes):
orig_png = original_png_list[i] if i < len(original_png_list) and original_png_list[i] is not None else png_list[i]
save_png_list.append(orig_png)
if all(png is not None for png in save_png_list):
save_composite_image(png_list=save_png_list, bbox_list=final_bboxes, Wc=Wc, Hc=Hc,
save_path=composite_result_path)
else:
print(f"Warning: Skipping composite image save - some image paths are None")
return final_bboxes