bchao1's picture
Upload foveated_diffusion Gradio demo
606581d verified
Raw
History Blame Contribute Delete
59.7 kB
"""
FLUX2 Foveated Image Generation Pipeline for DiffSynth.
This pipeline integrates foveation logic from pipeline_flux2_klein_foveation.py
into the DiffSynth FLUX2 pipeline architecture.
Key features:
- Mixed-resolution latent processing for efficient foveated rendering
- X0 (clean image) prediction for better upsampling quality
- Phase-aligned RoPE for consistent position encoding across resolutions
- CRPA attention for efficient mixed-resolution attention
"""
import os
import torch
import matplotlib
matplotlib.use("Agg")
import math
import torchvision
import numpy as np
import torch.nn.functional as F
from PIL import Image
from typing import Union, List, Optional, Tuple
from tqdm import tqdm
from einops import rearrange
from torchvision.utils import save_image
from diffsynth.core.device.npu_compatible_device import get_device_type
from diffsynth.diffusion import FlowMatchScheduler
from diffsynth.core import ModelConfig, gradient_checkpoint_forward
from diffsynth.diffusion.base_pipeline import BasePipeline, PipelineUnit, ControlNetInput
from transformers import AutoProcessor, AutoTokenizer
from diffsynth.models.flux2_text_encoder import Flux2TextEncoder
from .dit import Flux2DiTFoveated
from diffsynth.models.flux2_vae import Flux2VAE
from diffsynth.models.z_image_text_encoder import ZImageTextEncoder
from scipy.ndimage import zoom, gaussian_filter
import time
# Optional saliency-detection dependencies: only required when
# foveated_training_mode == "saliency". Import lazily so a default
# install does not need them.
try:
from scipy.datasets import face as scipy_face
except Exception:
scipy_face = None
try:
import deepgaze_pytorch
except Exception:
deepgaze_pytorch = None
def _gaussian_blur_mask_2d(
mask: torch.Tensor,
sigma_pixels: float,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
"""Apply separable Gaussian blur to a [1, 1, H, W] mask. sigma_pixels controls falloff width."""
# Kernel size odd, ~3 sigma on each side
k = max(3, int(math.ceil(3 * sigma_pixels)) * 2 + 1)
if k % 2 == 0:
k += 1
x = torch.arange(k, device=device, dtype=dtype) - (k - 1) / 2.0
g = torch.exp(-(x ** 2) / (2 * sigma_pixels ** 2 + 1e-6))
g = g / g.sum()
# [1, 1, k, 1] and [1, 1, 1, k] for separable conv
kernel_h = g.view(1, 1, k, 1)
kernel_w = g.view(1, 1, 1, k)
pad = k // 2
out = F.conv2d(mask, kernel_h, padding=(pad, 0))
out = F.conv2d(out, kernel_w, padding=(0, pad))
return out
class Flux2FoveatedImagePipeline(BasePipeline):
"""
FLUX2 Foveated Image Generation Pipeline.
This pipeline extends the standard FLUX2 pipeline with support for:
- Foveated rendering with mixed-resolution latents
- X0 prediction upsampling for better quality
- CRPA attention for efficient attention computation
"""
def __init__(self, device=get_device_type(), torch_dtype=torch.bfloat16):
super().__init__(
device=device, torch_dtype=torch_dtype,
height_division_factor=16, width_division_factor=16,
)
self.scheduler = FlowMatchScheduler("FLUX.2")
self.text_encoder: Flux2TextEncoder = None
self.text_encoder_qwen3: ZImageTextEncoder = None
self.dit: Flux2DiTFoveated = None
self.vae: Flux2VAE = None
self.tokenizer: AutoProcessor = None
self.in_iteration_models = ("dit",)
self.units = [
Flux2FoveatedUnit_ShapeChecker(),
Flux2FoveatedUnit_PromptEmbedder(),
Flux2FoveatedUnit_Qwen3PromptEmbedder(),
Flux2FoveatedUnit_NoiseInitializer(),
Flux2FoveatedUnit_InputImageEmbedder(),
Flux2FoveatedUnit_ImageIDs(),
]
# fixed latent for tracking validation
self.fixed_clean_latent = None
self.detach_z_mix = False
self.model_fn = model_fn_flux2_foveated
@staticmethod
def from_pretrained(
torch_dtype: torch.dtype = torch.bfloat16,
device: Union[str, torch.device] = get_device_type(),
model_configs: list[ModelConfig] = [],
tokenizer_config: ModelConfig = ModelConfig(model_id="black-forest-labs/FLUX.2-dev", origin_file_pattern="tokenizer/"),
vram_limit: float = None,
):
"""
Load a FLUX2 Foveated Image Pipeline from pretrained weights.
The pipeline will first try to fetch a registered "flux2_dit_foveated" model.
If not found, it will fetch a standard "flux2_dit" model and convert it to
the foveated version (same weights, different attention processing).
"""
# Initialize pipeline
pipe = Flux2FoveatedImagePipeline(device=device, torch_dtype=torch_dtype)
model_pool = pipe.download_and_load_models(model_configs, vram_limit)
# Fetch models
pipe.text_encoder = model_pool.fetch_model("flux2_text_encoder")
pipe.text_encoder_qwen3 = model_pool.fetch_model("z_image_text_encoder")
# Try to fetch foveated model first, fall back to base model
pipe.dit = model_pool.fetch_model("flux2_dit_foveated")
if pipe.dit is None:
# Load base FLUX2 model and convert to foveated version
base_dit = model_pool.fetch_model("flux2_dit")
if base_dit is not None:
pipe.dit = Flux2FoveatedImagePipeline._convert_to_foveated_dit(base_dit)
print("Converted base FLUX2 DiT to foveated version.")
pipe.vae = model_pool.fetch_model("flux2_vae")
if tokenizer_config is not None:
tokenizer_config.download_if_necessary()
pipe.tokenizer = AutoTokenizer.from_pretrained(tokenizer_config.path)
# VRAM Management
pipe.vram_management_enabled = pipe.check_vram_management_state()
return pipe
@staticmethod
def _convert_to_foveated_dit(base_dit):
"""
Convert a base FLUX2 DiT to a foveated version.
The foveated model has the exact same architecture as the base model,
just with CRPA attention processors. We infer config from state dict.
Args:
base_dit: A loaded Flux2DiT model instance.
Returns:
A Flux2DiTFoveated model with the same weights.
"""
# Get the state dict from the base model
state_dict = base_dit.state_dict()
# Infer configuration from state dict shapes
# inner_dim from x_embedder.weight: [inner_dim, in_channels]
inner_dim = state_dict['x_embedder.weight'].shape[0]
in_channels = state_dict['x_embedder.weight'].shape[1]
# joint_attention_dim from context_embedder.weight: [inner_dim, joint_attention_dim]
joint_attention_dim = state_dict['context_embedder.weight'].shape[1]
# Count transformer blocks
num_layers = 0
num_single_layers = 0
for key in state_dict.keys():
if key.startswith('transformer_blocks.') and key.endswith('.attn.to_q.weight'):
idx = int(key.split('.')[1])
num_layers = max(num_layers, idx + 1)
if key.startswith('single_transformer_blocks.') and key.endswith('.attn.to_qkv_mlp_proj.weight'):
idx = int(key.split('.')[1])
num_single_layers = max(num_single_layers, idx + 1)
# Infer attention config from to_q weight: [inner_dim, inner_dim]
# inner_dim = num_heads * head_dim
# Default head_dim is 128, so num_heads = inner_dim / 128
attention_head_dim = 128
num_attention_heads = inner_dim // attention_head_dim
# Check for guidance embeddings
guidance_embeds = 'time_guidance_embed.guidance_embedder.linear_1.weight' in state_dict
print(f"Inferred config: inner_dim={inner_dim}, in_channels={in_channels}, "
f"joint_attention_dim={joint_attention_dim}, num_layers={num_layers}, "
f"num_single_layers={num_single_layers}, num_attention_heads={num_attention_heads}, "
f"attention_head_dim={attention_head_dim}, guidance_embeds={guidance_embeds}")
# Create foveated model with inferred parameters
foveated_dit = Flux2DiTFoveated(
in_channels=in_channels,
num_layers=num_layers,
num_single_layers=num_single_layers,
attention_head_dim=attention_head_dim,
num_attention_heads=num_attention_heads,
joint_attention_dim=joint_attention_dim,
guidance_embeds=guidance_embeds,
)
# Load state dict directly - architectures are identical
foveated_dit.load_state_dict(state_dict)
# Move to same device and dtype as base model
device = next(base_dit.parameters()).device
dtype = next(base_dit.parameters()).dtype
foveated_dit = foveated_dit.to(device=device, dtype=dtype)
return foveated_dit
# =====================================================================
# Patchify / Unpatchify Operations (FLUX2 2x2 patches)
# =====================================================================
@staticmethod
def _patchify_latents(latents):
"""Convert [B, C, H, W] to 2x2 patched format [B, C*4, H/2, W/2]."""
batch_size, num_channels, height, width = latents.shape
latents = latents.view(batch_size, num_channels, height // 2, 2, width // 2, 2)
latents = latents.permute(0, 1, 3, 5, 2, 4)
latents = latents.reshape(batch_size, num_channels * 4, height // 2, width // 2)
return latents
@staticmethod
def _unpatchify_latents(latents):
"""Convert [B, C*4, H/2, W/2] back to [B, C, H, W]."""
batch_size, num_channels, height, width = latents.shape
latents = latents.reshape(batch_size, num_channels // 4, 2, 2, height, width)
latents = latents.permute(0, 1, 4, 2, 5, 3)
latents = latents.reshape(batch_size, num_channels // 4, height * 2, width * 2)
return latents
# =====================================================================
# Foveation-Specific Latent Operations
# =====================================================================
@staticmethod
def _prepare_latent_image_ids_foveation(
batch_size: int,
height: int,
width: int,
device: torch.device,
dtype: torch.dtype,
foveation_mask: Optional[torch.Tensor] = None,
lr_factor: int = 2,
):
"""
Prepare 4D position IDs for image latents with foveation support.
For FLUX2, position IDs have format (T, H, W, L) where:
- T: Time coordinate (always 0 for image latents)
- H: Height coordinate
- W: Width coordinate
- L: Layer coordinate (always 0 for image latents)
With foveation:
- High-res blocks keep all 4 tokens
- Low-res blocks keep only top-left token
Returns:
latent_image_ids: [Seq_Len, 4] position IDs
resolution_mask: [Seq_Len] mask (1=HR, 0=LR)
resolution_mask_top_left: [Seq_Len] mask for key subsampling
"""
# Create base coordinate grid (all must have same dtype for cartesian_prod)
t = torch.zeros(1, dtype=dtype)
h = torch.arange(height, dtype=dtype)
w = torch.arange(width, dtype=dtype)
l = torch.zeros(1, dtype=dtype)
latent_image_ids = torch.cartesian_prod(t, h, w, l).to(device=device)
resolution_mask_grid = torch.ones(height, width, device=device, dtype=dtype)
if foveation_mask is not None:
if height % lr_factor != 0 or width % lr_factor != 0:
raise ValueError(f"Height and width must be divisible by lr_factor ({lr_factor}) for mixed-resolution.")
n_per_block = lr_factor * lr_factor
h_d, w_d = height // lr_factor, width // lr_factor
grid_2d = latent_image_ids.view(height, width, 4)
grid_blocks = grid_2d.view(h_d, lr_factor, w_d, lr_factor, 4).permute(0, 2, 1, 3, 4).reshape(h_d, w_d, n_per_block, 4)
mask_blocks = foveation_mask.view(h_d, lr_factor, w_d, lr_factor).permute(0, 2, 1, 3).reshape(h_d, w_d, n_per_block)
is_high_res_block = (mask_blocks.sum(dim=-1) > 0)
is_low_res_block = ~is_high_res_block
low_res_coords = grid_blocks[..., 0, :]
output_grid = grid_blocks.clone()
# Flatten before mask indexing β€” torch 2.5.1 mishandles 2D bool mask + int/slice.
output_grid.view(h_d * w_d, n_per_block, 4)[is_low_res_block.view(-1), 0, :] = low_res_coords[is_low_res_block]
res_mask_blocks = resolution_mask_grid.view(h_d, lr_factor, w_d, lr_factor).permute(0, 2, 1, 3).reshape(h_d, w_d, n_per_block)
output_res_mask = res_mask_blocks.clone()
output_res_mask[is_low_res_block, :] = 0.0
valid_tokens = torch.ones(h_d, w_d, n_per_block, device=device, dtype=torch.bool)
valid_tokens.view(h_d * w_d, n_per_block)[is_low_res_block.view(-1), 1:] = False
output_grid = output_grid.reshape(-1, 4)
output_res_mask = output_res_mask.reshape(-1)
valid_tokens = valid_tokens.reshape(-1)
tl_mask_blocks = torch.zeros_like(res_mask_blocks)
tl_mask_blocks[..., 0] = 1.0
tl_mask_blocks[is_low_res_block, :] = 1.0
resolution_mask_top_left = tl_mask_blocks.reshape(-1)[valid_tokens]
latent_image_ids = output_grid[valid_tokens]
resolution_mask = output_res_mask[valid_tokens]
return (
latent_image_ids.to(device=device, dtype=dtype),
resolution_mask.to(device=device, dtype=dtype),
resolution_mask_top_left.to(device=device, dtype=dtype)
)
else:
return (
latent_image_ids.to(device=device, dtype=dtype),
None,
None
)
@staticmethod
def _downsample_latents(
latents: torch.Tensor,
height: int,
width: int,
foveation_mask: Optional[torch.Tensor] = None,
sample_mode: str = "nearest",
latents_downsampled: Optional[torch.Tensor] = None,
lr_factor: int = 2,
):
"""
Downsample latents spatially based on foveation mask.
For low-res blocks, averages/samples lr_factor x lr_factor regions to single tokens.
High-res blocks keep all lr_factor*lr_factor tokens.
"""
if foveation_mask is None:
return latents
mask = foveation_mask.to(latents.device)
batch_size, seq_len, channels = latents.shape
if height * width != seq_len:
raise ValueError(f"Height ({height}) * Width ({width}) != seq_len ({seq_len})")
output_latents = latents.clone()
# Unpack to spatial format for downsampling
latents_spatial = latents.view(batch_size, height, width, channels // 4, 2, 2)
latents_spatial = latents_spatial.permute(0, 3, 1, 4, 2, 5)
latents_spatial = latents_spatial.reshape(batch_size, channels // 4, height * 2, width * 2)
# Downsample (naive interpolation when latents_downsampled isn't precomputed).
if latents_downsampled is None:
if sample_mode == "average":
latents_down = F.interpolate(latents_spatial, scale_factor=1.0 / lr_factor, mode="bilinear") * lr_factor
elif sample_mode == "top_left":
latents_down = F.interpolate(latents_spatial, scale_factor=1.0 / lr_factor, mode="nearest")
else:
raise ValueError(f"Unknown sample_mode: {sample_mode}")
latents_down = latents_down.reshape(batch_size, channels // 4, height // lr_factor, 2, width // lr_factor, 2)
latents_down = latents_down.permute(0, 2, 4, 1, 3, 5)
low_res_packed = latents_down.reshape(batch_size, height // lr_factor, width // lr_factor, channels)
else:
low_res_packed = latents_downsampled.reshape(batch_size, height // lr_factor, width // lr_factor, channels)
n_per_block = lr_factor * lr_factor
h_d, w_d = height // lr_factor, width // lr_factor
output_latents = output_latents.view(batch_size, height, width, channels)
output_latents = output_latents.view(batch_size, h_d, lr_factor, w_d, lr_factor, channels)
output_latents = output_latents.permute(0, 1, 3, 2, 4, 5).reshape(batch_size, h_d, w_d, n_per_block, channels)
mask_blocks = mask.view(h_d, lr_factor, w_d, lr_factor).permute(0, 2, 1, 3).reshape(h_d, w_d, n_per_block)
is_high_res_block = (mask_blocks.sum(dim=-1) > 0)
is_low_res_block = ~is_high_res_block
# Flatten the (h_d, w_d) dims before mask indexing β€” torch 2.5.1 mishandles
# mixing a 2D bool mask with an integer index in adjacent slots.
output_latents_flat = output_latents.view(batch_size, h_d * w_d, n_per_block, channels)
low_res_packed_flat = low_res_packed.view(batch_size, h_d * w_d, channels)
is_low_res_block_flat = is_low_res_block.view(-1)
output_latents_flat[:, is_low_res_block_flat, 0, :] = low_res_packed_flat[:, is_low_res_block_flat, :].to(output_latents.dtype)
valid_tokens = torch.ones(h_d, w_d, n_per_block, device=latents.device, dtype=torch.bool)
# Flatten before mask indexing β€” torch 2.5.1 mishandles 2D bool mask + slice.
valid_tokens_flat = valid_tokens.view(h_d * w_d, n_per_block)
valid_tokens_flat[is_low_res_block.view(-1), 1:] = False
output_latents = output_latents.view(batch_size, -1, channels)
valid_tokens = valid_tokens.view(-1)
return output_latents[:, valid_tokens, :]
@staticmethod
def _downsample_remaining_high_res(
latents: torch.Tensor,
height: int,
width: int,
foveation_mask: torch.Tensor,
downsample_mode: str = "nearest",
lr_factor: int = 2,
):
"""
Downsample all blocks to uniform low-res (single token per block).
"""
batch_size, reduced_len, channels = latents.shape
device = latents.device
dtype = latents.dtype
n_per_block = lr_factor * lr_factor
h_d, w_d = height // lr_factor, width // lr_factor
mask = foveation_mask.to(device)
mask_blocks = mask.view(h_d, lr_factor, w_d, lr_factor).permute(0, 2, 1, 3).reshape(h_d, w_d, n_per_block)
is_high_res_block = (mask_blocks.sum(dim=-1) > 0)
is_low_res_block = ~is_high_res_block
layout_mask = torch.ones(h_d, w_d, n_per_block, device=device, dtype=torch.bool)
# Flatten before mask indexing β€” torch 2.5.1 mishandles 2D bool mask + slice.
layout_mask.view(h_d * w_d, n_per_block)[is_low_res_block.view(-1), 1:] = False
if layout_mask.sum() != reduced_len:
raise ValueError(f"Latent length {reduced_len} != mask topology {layout_mask.sum()}")
reconstructed_blocks = torch.zeros(batch_size, h_d * w_d * n_per_block, channels, device=device, dtype=dtype)
reconstructed_blocks[:, layout_mask.view(-1), :] = latents
reconstructed_blocks = reconstructed_blocks.view(batch_size, h_d, w_d, n_per_block, channels)
if is_high_res_block.any():
high_res_tokens = reconstructed_blocks[:, is_high_res_block, :, :]
if downsample_mode == "average":
pooled_val = high_res_tokens.mean(dim=2)
# Flatten spatial dims before mask indexing β€” torch 2.5.1 mishandles
# mixing a 2D bool mask with an integer index in adjacent slots.
reconstructed_blocks_flat = reconstructed_blocks.view(batch_size, h_d * w_d, n_per_block, channels)
reconstructed_blocks_flat[:, is_high_res_block.view(-1), 0, :] = pooled_val
final_grid = reconstructed_blocks[:, :, :, 0, :]
return final_grid.reshape(batch_size, h_d * w_d, channels)
@staticmethod
def _upsample_latents(
latents: torch.Tensor,
height: int,
width: int,
foveation_mask: Optional[torch.Tensor] = None,
upsample_mode: str = "nearest",
lr_factor: int = 2,
):
"""
Upsamples reduced latents back to the original spatial grid size.
"""
if foveation_mask is None:
return latents
batch_size, reduced_len, channels = latents.shape
device = latents.device
dtype = latents.dtype
n_per_block = lr_factor * lr_factor
h_d, w_d = height // lr_factor, width // lr_factor
mask = foveation_mask.to(device)
mask_blocks = mask.view(h_d, lr_factor, w_d, lr_factor).permute(0, 2, 1, 3).reshape(h_d, w_d, n_per_block)
is_high_res_block = (mask_blocks.sum(dim=-1) > 0)
is_low_res_block = ~is_high_res_block
layout_mask = torch.ones(h_d, w_d, n_per_block, device=device, dtype=torch.bool)
# Flatten before mask indexing β€” torch 2.5.1 mishandles 2D bool mask + slice.
layout_mask.view(h_d * w_d, n_per_block)[is_low_res_block.view(-1), 1:] = False
if layout_mask.sum() != reduced_len:
raise ValueError(f"Latent length {reduced_len} does not match mask topology {layout_mask.sum()}.")
reconstructed_blocks = torch.zeros(batch_size, h_d * w_d * n_per_block, channels, device=device, dtype=dtype)
reconstructed_blocks[:, layout_mask.view(-1), :] = latents
reconstructed_blocks = reconstructed_blocks.view(batch_size, h_d, w_d, n_per_block, channels)
if upsample_mode == "nearest":
if is_low_res_block.any():
# Flatten before mask indexing β€” torch 2.5.1 mishandles 2D bool mask + slice.
rb_flat = reconstructed_blocks.view(batch_size, h_d * w_d, n_per_block, channels)
low_res_block_flat = is_low_res_block.view(-1)
low_res_vals = rb_flat[:, low_res_block_flat, 0:1, :]
rb_flat[:, low_res_block_flat, :, :] = low_res_vals.expand(-1, -1, n_per_block, -1)
final_output = reconstructed_blocks.view(batch_size, h_d, w_d, lr_factor, lr_factor, channels)
final_output = final_output.permute(0, 1, 3, 2, 4, 5).reshape(batch_size, height, width, channels)
else:
coarse_grid = reconstructed_blocks[..., 0, :].clone()
if is_high_res_block.any():
high_res_means = reconstructed_blocks[:, is_high_res_block, :, :].mean(dim=2)
coarse_grid[:, is_high_res_block, :] = high_res_means
coarse_grid = coarse_grid.permute(0, 3, 1, 2)
fine_grid = F.interpolate(
coarse_grid,
size=(height, width),
mode=upsample_mode,
align_corners=False,
antialias=True if upsample_mode != 'nearest' else False
)
final_output = fine_grid.permute(0, 2, 3, 1)
high_res_raster = reconstructed_blocks.view(batch_size, h_d, w_d, lr_factor, lr_factor, channels)
high_res_raster = high_res_raster.permute(0, 1, 3, 2, 4, 5).reshape(batch_size, height, width, channels)
pixel_mask = is_high_res_block.repeat_interleave(lr_factor, dim=0).repeat_interleave(lr_factor, dim=1)
final_output[:, pixel_mask, :] = high_res_raster[:, pixel_mask, :]
return final_output.reshape(batch_size, height * width, channels)
def foveated_training_forward(
self,
inputs: dict,
timestep: torch.Tensor,
timestep_id: int,
prediction_type: str,
lr_downsample_factor: int = 2,
):
"""Single-step foveated forward for training. Used by FoveatedFlowMatchSFTLoss.
Downsamples latents with foveation, runs the DiT, and returns the foveated
noise prediction [B, L, C] matching the mixed-resolution training target.
"""
foveation_mask = inputs.get("foveation_mask")
if foveation_mask is None:
models = {name: getattr(self, name) for name in self.in_iteration_models}
return self.model_fn(**models, **inputs, timestep=timestep)
# fixed clean latent for tracking validation
if self.fixed_clean_latent is None:
self.fixed_clean_latent = inputs["input_latents"]
latents = inputs["latents"]
height = inputs["height"]
width = inputs["width"]
latent_height, latent_width = height // 16, width // 16
batch_size = latents.shape[0]
foveation_mask = foveation_mask.to(latents.device)
latents_input = self._downsample_latents(
latents, latent_height, latent_width,
foveation_mask=foveation_mask,
latents_downsampled=inputs["latents_downsampled"],
lr_factor=lr_downsample_factor,
)
latent_ids, resolution_mask, resolution_mask_top_left = self._prepare_latent_image_ids_foveation(
batch_size, latent_height, latent_width, self.device, latents.dtype,
foveation_mask, lr_factor=lr_downsample_factor,
)
if latent_ids.ndim == 2:
latent_ids = latent_ids.unsqueeze(0).expand(batch_size, -1, -1)
inputs["latents"] = latents_input
inputs["image_ids"] = latent_ids
inputs["resolution_mask"] = resolution_mask
inputs["resolution_mask_top_left"] = resolution_mask_top_left
models = {name: getattr(self, name) for name in self.in_iteration_models}
return self.model_fn(**models, **inputs, timestep=timestep)
@torch.no_grad()
def __call__(
self,
# Prompt
prompt: str,
negative_prompt: str = "",
cfg_scale: float = 1.0,
embedded_guidance: float = 4.0,
# Image
input_image: Image.Image = None,
denoising_strength: float = 1.0,
# Shape
height: int = 1024,
width: int = 1024,
# Randomness
seed: int = None,
rand_device: str = "cpu",
# Steps
num_inference_steps: int = 30,
progress_bar_cmd=tqdm,
# Foveation
foveation_mask: Optional[torch.Tensor] = None,
decode_mode: str = "direct",
prediction_type: str = "clean",
soft_foveation_blend: bool = False,
full_res_foveation_mask: Optional[torch.Tensor] = None,
lr_downsample_factor: int = 2,
):
"""Generate images with optional foveated rendering.
Args:
prompt: Text prompt for generation.
foveation_mask: [H, W] binary mask (1 = high-res, 0 = low-res).
Shape should match the token grid (height//16, width//16).
decode_mode: "direct" (HR-only decode) or "merge" (blend HR + LR decodes).
soft_foveation_blend: If True, Gaussian-blur the mask boundary before
blending in merge decode.
"""
self.scheduler.set_timesteps(
num_inference_steps,
denoising_strength=denoising_strength,
dynamic_shift_len=height // 16 * width // 16,
)
latent_height = height // 16
latent_width = width // 16
inputs_posi = {"prompt": prompt}
inputs_nega = {"negative_prompt": negative_prompt}
inputs_shared = {
"cfg_scale": cfg_scale, "embedded_guidance": embedded_guidance,
"input_image": input_image, "denoising_strength": denoising_strength,
"height": height, "width": width,
"seed": seed, "rand_device": rand_device,
"num_inference_steps": num_inference_steps,
"foveation_mask": foveation_mask,
"soft_foveation_blend": soft_foveation_blend,
"lr_downsample_factor": lr_downsample_factor,
}
for unit in self.units:
inputs_shared, inputs_posi, inputs_nega = self.unit_runner(
unit, self, inputs_shared, inputs_posi, inputs_nega,
)
# Denoise with foveation support
self.load_models_to_device(self.in_iteration_models)
models = {name: getattr(self, name) for name in self.in_iteration_models}
latents = inputs_shared["latents"]
# All downsampling happens once up front; the DiT operates on the mixed-resolution
# token sequence for every denoising step.
latents = self._downsample_latents(
latents, latent_height, latent_width,
foveation_mask=foveation_mask,
latents_downsampled=inputs_shared["latents_downsampled"],
lr_factor=lr_downsample_factor,
)
print('decode mode', decode_mode)
start_t = time.time()
for progress_id, timestep in enumerate(progress_bar_cmd(self.scheduler.timesteps)):
timestep_val = timestep.unsqueeze(0).to(dtype=self.torch_dtype, device=self.device)
batch_size = latents.shape[0]
if foveation_mask is not None:
latents_input = latents
latent_ids, resolution_mask, resolution_mask_top_left = self._prepare_latent_image_ids_foveation(
batch_size, latent_height, latent_width,
self.device, latents.dtype, foveation_mask,
lr_factor=lr_downsample_factor,
)
if latent_ids.ndim == 2:
latent_ids = latent_ids.unsqueeze(0).expand(batch_size, -1, -1)
else:
latents_input = latents
resolution_mask = None
resolution_mask_top_left = None
latent_ids = inputs_shared["image_ids"]
inputs_shared["latents"] = latents_input
inputs_shared["image_ids"] = latent_ids
inputs_shared["resolution_mask"] = resolution_mask
inputs_shared["resolution_mask_top_left"] = resolution_mask_top_left
noise_pred = self.cfg_guided_model_fn(
self.model_fn, cfg_scale,
inputs_shared, inputs_posi, inputs_nega,
**models,
timestep=timestep_val,
progress_id=progress_id,
)
if foveation_mask is None:
noise_pred = noise_pred[:, :latents.size(1), :]
latents = self.step(
self.scheduler, progress_id=progress_id,
noise_pred=noise_pred, latents=latents,
)
vit_end_t = time.time()
inputs_shared["latents"] = latents
# Decode
self.load_models_to_device(['vae'])
if decode_mode == "direct":
latents_spatial = rearrange(latents, "B (H W) C -> B C H W", H=latent_height, W=latent_width)
image = self.vae.decode(latents_spatial)
image = self.vae_output_to_image(image)
elif decode_mode == "merge" and foveation_mask is not None:
# Merge decoding: decode low-res and high-res separately, then blend.
# Note: DiffSynth VAE expects patched latents [B, 128, H/2, W/2].
mixed_res_latents = latents # already mixed-resolution
# Step 2: Low-res path - downsample mixed-res to uniform low-res grid
low_res_latents = self._downsample_remaining_high_res(
mixed_res_latents, latent_height, latent_width, foveation_mask=foveation_mask,
lr_factor=lr_downsample_factor
)
h_d, w_d = latent_height // lr_downsample_factor, latent_width // lr_downsample_factor
# VAE expects patched format [B, 128, H/2, W/2] - just reshape, don't unpatchify
low_res_spatial = low_res_latents.view(batch_size, h_d, w_d, -1).permute(0, 3, 1, 2)
low_res_image = self.vae.decode(low_res_spatial)
# Step 3: High-res path - upsample mixed-res to uniform high-res grid
high_res_latents = self._upsample_latents(
mixed_res_latents, latent_height, latent_width, foveation_mask=foveation_mask,
lr_factor=lr_downsample_factor
)
# VAE expects patched format [B, 128, H/2, W/2] - just reshape, don't unpatchify
high_res_spatial = high_res_latents.view(
batch_size, latent_height, latent_width, -1
).permute(0, 3, 1, 2)
high_res_image = self.vae.decode(high_res_spatial)
# Step 4: Upsample low-res image and merge
low_res_image = F.interpolate(low_res_image, size=(height, width), mode='bicubic', align_corners=False)
if full_res_foveation_mask is not None:
high_res_foveation_mask = full_res_foveation_mask[None, None, :, :].float()
else:
high_res_foveation_mask = F.interpolate(
foveation_mask[None, None, :, :].float(),
size=(height, width),
mode='bicubic'
)
high_res_foveation_mask = high_res_foveation_mask.to(latents.dtype).to(latents.device)
if soft_foveation_blend:
# Soft edge: Gaussian falloff over ~upscale_factor pixels at high-res
upscale = height / latent_height
high_res_foveation_mask = _gaussian_blur_mask_2d(
high_res_foveation_mask, sigma_pixels=upscale, device=latents.device, dtype=latents.dtype
)
high_res_foveation_mask = high_res_foveation_mask.clamp(0.0, 1.0)
merged_image = low_res_image * (1 - high_res_foveation_mask) + high_res_image * high_res_foveation_mask
image = self.vae_output_to_image(merged_image)
else:
# Fallback to direct
latents_spatial = rearrange(latents, "B (H W) C -> B C H W", H=latent_height, W=latent_width)
image = self.vae.decode(latents_spatial)
image = self.vae_output_to_image(image)
vae_end_t = time.time()
# individually track time
self.vit_timing = vit_end_t - start_t
self.vae_timing = vae_end_t - vit_end_t
self.load_models_to_device([])
return image
# =====================================================================
# Pipeline Units (adapted from flux2_image.py)
# =====================================================================
class Flux2FoveatedUnit_ShapeChecker(PipelineUnit):
def __init__(self):
super().__init__(
input_params=("height", "width"),
output_params=("height", "width"),
)
def process(self, pipe: Flux2FoveatedImagePipeline, height, width):
height, width = pipe.check_resize_height_width(height, width)
return {"height": height, "width": width}
class Flux2FoveatedUnit_PromptEmbedder(PipelineUnit):
def __init__(self):
super().__init__(
seperate_cfg=True,
input_params_posi={"prompt": "prompt"},
input_params_nega={"prompt": "negative_prompt"},
output_params=("prompt_emb", "prompt_emb_mask"),
onload_model_names=("text_encoder",)
)
self.system_message = "You are an AI that reasons about image descriptions. You give structured responses focusing on object relationships, object attribution and actions without speculation."
def format_text_input(self, prompts: List[str], system_message: str = None):
cleaned_txt = [prompt.replace("[IMG]", "") for prompt in prompts]
return [
[
{"role": "system", "content": [{"type": "text", "text": system_message}]},
{"role": "user", "content": [{"type": "text", "text": prompt}]},
]
for prompt in cleaned_txt
]
def get_mistral_3_small_prompt_embeds(
self,
text_encoder,
tokenizer,
prompt: Union[str, List[str]],
dtype: Optional[torch.dtype] = None,
device: Optional[torch.device] = None,
max_sequence_length: int = 512,
system_message: str = None,
hidden_states_layers: List[int] = (10, 20, 30),
):
dtype = text_encoder.dtype if dtype is None else dtype
device = text_encoder.device if device is None else device
prompt = [prompt] if isinstance(prompt, str) else prompt
messages_batch = self.format_text_input(prompts=prompt, system_message=system_message)
inputs = tokenizer.apply_chat_template(
messages_batch,
add_generation_prompt=False,
tokenize=True,
return_dict=True,
return_tensors="pt",
padding="max_length",
truncation=True,
max_length=max_sequence_length,
)
input_ids = inputs["input_ids"].to(device)
attention_mask = inputs["attention_mask"].to(device)
output = text_encoder(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
use_cache=False,
)
out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
out = out.to(dtype=dtype, device=device)
batch_size, num_channels, seq_len, hidden_dim = out.shape
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
return prompt_embeds
def prepare_text_ids(self, x: torch.Tensor, t_coord: Optional[torch.Tensor] = None):
B, L, _ = x.shape
out_ids = []
for i in range(B):
t = torch.arange(1) if t_coord is None else t_coord[i]
h = torch.arange(1)
w = torch.arange(1)
l = torch.arange(L)
coords = torch.cartesian_prod(t, h, w, l)
out_ids.append(coords)
return torch.stack(out_ids)
def encode_prompt(
self,
text_encoder,
tokenizer,
prompt: Union[str, List[str]],
dtype = None,
device: Optional[torch.device] = None,
num_images_per_prompt: int = 1,
prompt_embeds: Optional[torch.Tensor] = None,
max_sequence_length: int = 512,
text_encoder_out_layers: Tuple[int] = (10, 20, 30),
):
prompt = [prompt] if isinstance(prompt, str) else prompt
if prompt_embeds is None:
prompt_embeds = self.get_mistral_3_small_prompt_embeds(
text_encoder=text_encoder,
tokenizer=tokenizer,
prompt=prompt,
dtype=dtype,
device=device,
max_sequence_length=max_sequence_length,
system_message=self.system_message,
hidden_states_layers=text_encoder_out_layers,
)
batch_size, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
text_ids = self.prepare_text_ids(prompt_embeds)
text_ids = text_ids.to(device)
return prompt_embeds, text_ids
def process(self, pipe: Flux2FoveatedImagePipeline, prompt):
if pipe.text_encoder_qwen3 is not None:
return {}
pipe.load_models_to_device(self.onload_model_names)
prompt_embeds, text_ids = self.encode_prompt(
pipe.text_encoder, pipe.tokenizer, prompt,
dtype=pipe.torch_dtype, device=pipe.device,
)
return {"prompt_embeds": prompt_embeds, "text_ids": text_ids}
class Flux2FoveatedUnit_Qwen3PromptEmbedder(PipelineUnit):
def __init__(self):
super().__init__(
seperate_cfg=True,
input_params_posi={"prompt": "prompt"},
input_params_nega={"prompt": "negative_prompt"},
output_params=("prompt_emb", "prompt_emb_mask"),
onload_model_names=("text_encoder_qwen3",)
)
self.hidden_states_layers = (9, 18, 27)
def get_qwen3_prompt_embeds(
self,
text_encoder,
tokenizer,
prompt: Union[str, List[str]],
dtype: Optional[torch.dtype] = None,
device: Optional[torch.device] = None,
max_sequence_length: int = 512,
):
dtype = text_encoder.dtype if dtype is None else dtype
device = text_encoder.device if device is None else device
prompt = [prompt] if isinstance(prompt, str) else prompt
all_input_ids = []
all_attention_masks = []
for single_prompt in prompt:
messages = [{"role": "user", "content": single_prompt}]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
inputs = tokenizer(
text,
return_tensors="pt",
padding="max_length",
truncation=True,
max_length=max_sequence_length,
)
all_input_ids.append(inputs["input_ids"])
all_attention_masks.append(inputs["attention_mask"])
input_ids = torch.cat(all_input_ids, dim=0).to(device)
attention_mask = torch.cat(all_attention_masks, dim=0).to(device)
with torch.inference_mode():
output = text_encoder(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
use_cache=False,
)
out = torch.stack([output.hidden_states[k] for k in self.hidden_states_layers], dim=1)
out = out.to(dtype=dtype, device=device)
batch_size, num_channels, seq_len, hidden_dim = out.shape
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
return prompt_embeds
def prepare_text_ids(self, x: torch.Tensor, t_coord: Optional[torch.Tensor] = None):
B, L, _ = x.shape
out_ids = []
for i in range(B):
t = torch.arange(1) if t_coord is None else t_coord[i]
h = torch.arange(1)
w = torch.arange(1)
l = torch.arange(L)
coords = torch.cartesian_prod(t, h, w, l)
out_ids.append(coords)
return torch.stack(out_ids)
def encode_prompt(
self,
text_encoder,
tokenizer,
prompt: Union[str, List[str]],
dtype = None,
device: Optional[torch.device] = None,
num_images_per_prompt: int = 1,
prompt_embeds: Optional[torch.Tensor] = None,
max_sequence_length: int = 512,
):
prompt = [prompt] if isinstance(prompt, str) else prompt
if prompt_embeds is None:
prompt_embeds = self.get_qwen3_prompt_embeds(
text_encoder=text_encoder,
tokenizer=tokenizer,
prompt=prompt,
dtype=dtype,
device=device,
max_sequence_length=max_sequence_length,
)
batch_size, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
text_ids = self.prepare_text_ids(prompt_embeds)
text_ids = text_ids.to(device)
return prompt_embeds, text_ids
def process(self, pipe: Flux2FoveatedImagePipeline, prompt):
if pipe.text_encoder_qwen3 is None:
return {}
pipe.load_models_to_device(self.onload_model_names)
prompt_embeds, text_ids = self.encode_prompt(
pipe.text_encoder_qwen3, pipe.tokenizer, prompt,
dtype=pipe.torch_dtype, device=pipe.device,
)
return {"prompt_embeds": prompt_embeds, "text_ids": text_ids}
class Flux2FoveatedUnit_NoiseInitializer(PipelineUnit):
def __init__(self):
super().__init__(
input_params=("height", "width", "seed", "rand_device", "lr_downsample_factor"),
output_params=("noise", "downsampled_noise"),
)
def process(self, pipe: Flux2FoveatedImagePipeline, height, width, seed, rand_device, lr_downsample_factor=2):
noise = pipe.generate_noise((1, 128, height//16, width//16), seed=seed, rand_device=rand_device, rand_torch_dtype=pipe.torch_dtype)
noise_downsampled = pipe.generate_noise((1, 128, height//16//lr_downsample_factor, width//16//lr_downsample_factor), seed=seed, rand_device=rand_device, rand_torch_dtype=pipe.torch_dtype)
noise = noise.reshape(1, 128, height//16 * width//16).permute(0, 2, 1)
noise_downsampled = noise_downsampled.reshape(1, 128, height//16//lr_downsample_factor * width//16//lr_downsample_factor).permute(0, 2, 1)
return {"noise": noise, "noise_downsampled": noise_downsampled}
# Saliency-to-mask constants (same as run_saliency_detection.py / fovetaion_masks_from_video.py)
_INFERENCE_SCALE = 0.5
_SPATIAL_SIGMA = 20.0
_R_MIN, _R_MAX = 0.2, 0.5
_COVERAGE = 0.9
_DENSE_THRESHOLD = 0.3
def bbox_to_foveation_mask(
height: int,
width: int,
bboxes_xyxy,
sigma: float = 10.0,
):
"""
Create a full-resolution foveation mask (H, W) in [0, 1] from a list of
bounding boxes in absolute pixel coordinates (x1, y1, x2, y2).
This function mirrors bbox_to_foveation_mask in misc/run_bbox_detection.py:
the mask is 1 inside any bbox, 0 outside, followed by Gaussian smoothing
and normalization. This yields a smooth "high-acuity" region covering all
detected objects.
"""
mask = np.zeros((height, width), dtype=np.float32)
for box in bboxes_xyxy:
x1, y1, x2, y2 = box
x1_i = max(0, min(width - 1, int(np.floor(x1))))
y1_i = max(0, min(height - 1, int(np.floor(y1))))
x2_i = max(0, min(width, int(np.ceil(x2))))
y2_i = max(0, min(height, int(np.ceil(y2))))
if x2_i <= x1_i or y2_i <= y1_i:
continue
mask[y1_i:y2_i, x1_i:x2_i] = 1.0
if sigma > 0:
mask = gaussian_filter(mask, sigma=sigma)
max_val = mask.max()
if max_val > 0:
mask = mask / max_val
return mask
class Flux2FoveatedUnit_InputImageEmbedder(PipelineUnit):
def __init__(self):
super().__init__(
input_params=("input_image", "noise", "noise_downsampled", "lr_downsample_factor"),
output_params=("latents", "latents_downsampled", "input_latents", "input_latents_downsampled", "foveation_mask"),
onload_model_names=("vae",)
)
try:
self.saliency_detector = deepgaze_pytorch.DeepGazeIIE(pretrained=True)
self.saliency_detector.eval()
print("Saliency detector loaded")
except Exception as e:
print(e)
self.saliency_detector = None
print("Saliency detector not loaded")
self.bbox_detector = None
def get_foveation_mask_from_image(
self,
pipe: "Flux2FoveatedImagePipeline",
image: torch.Tensor,
) -> Optional[torch.Tensor]:
"""
Create a foveation mask from the input image, depending on the
foveated_training_mode on the pipeline:
- "saliency": current DeepGaze-based saliency-to-mask logic.
- "bbox": use bbox_to_foveation_mask (same logic as run_bbox_detection.py)
on provided bounding boxes, then downsample to latent grid size.
image: (1, 3, H, W) in [-1, 1]. Returns (H//16, W//16) mask.
"""
mode = getattr(pipe, "foveated_training_mode", "saliency")
if mode == "bbox":
# Run YOLO detector directly on the input image to obtain bboxes,
# then convert them to a foveation mask using the exact same logic
# as misc/run_bbox_detection.py.
if image.dim() == 3:
image = image.unsqueeze(0)
device = image.device
h_orig, w_orig = int(image.shape[2]), int(image.shape[3])
# Lazy-load YOLO detector with same defaults as run_bbox_detection.py
if self.bbox_detector is None:
try:
from ultralytics import YOLO
except ImportError as exc:
print(
"ultralytics not installed; bbox-based foveation requires "
"`pip install ultralytics`. Falling back to no foveation mask."
)
return None
# Use same default model and keep device selection at predict-time
self.bbox_detector = YOLO("yolov8n.pt")
self.bbox_detector.model.float()
# Convert input tensor ([-1, 1]) to uint8 RGB numpy array, same scaling
# convention as saliency path (0-255).
img_01 = (image[0].float() + 1.0) / 2.0
img_255 = (img_01.clamp(0, 1) * 255.0).permute(1, 2, 0).cpu().numpy().astype(np.uint8)
# Device string for YOLO; follow run_bbox_detection default conf params.
device_str = str(pipe.device) if hasattr(pipe, "device") else "cpu"
results = self.bbox_detector.predict(
img_255,
conf=0.25,
device=device_str,
verbose=False,
)
if not results:
# No detections result: use full high-res mask
mask_np = np.ones((h_orig, w_orig), dtype=np.float32)
else:
res = results[0]
if res.boxes is None or res.boxes.xyxy is None or len(res.boxes) == 0:
# No boxes: use full high-res mask
mask_np = np.ones((h_orig, w_orig), dtype=np.float32)
else:
boxes_np = res.boxes.xyxy.cpu().numpy()
# Full-resolution [H, W] mask using the exact bbox->mask logic.
mask_np = bbox_to_foveation_mask(h_orig, w_orig, boxes_np)
mask_hr = torch.from_numpy(mask_np).to(device=device, dtype=torch.float32)
# Downsample by 32, binarize, then upsample 2x so effective factor is 16
mask_hr_4d = mask_hr.unsqueeze(0).unsqueeze(0)
h_32, w_32 = h_orig // 32, w_orig // 32
mask_32 = F.interpolate(mask_hr_4d, size=(h_32, w_32), mode="bilinear", align_corners=False)
mask_binary_32 = (mask_32.squeeze(0).squeeze(0) > 0.5).to(torch.float32).unsqueeze(0).unsqueeze(0)
latent_h, latent_w = h_orig // 16, w_orig // 16
mask_binary = F.interpolate(mask_binary_32, size=(latent_h, latent_w), mode="nearest")
return mask_binary
# Default: saliency-based foveation mask
if self.saliency_detector is None:
return None
if image.dim() == 3:
image = image.unsqueeze(0)
device = image.device
self.saliency_detector = self.saliency_detector.to(device)
h_orig, w_orig = int(image.shape[2]), int(image.shape[3])
# DeepGaze input: [0, 255] float, same as run_saliency_detection _frame_to_tensor
img_01 = (image.float() + 1.0) / 2.0
img_255 = img_01.clamp(0, 1) * 255.0
h_inf = int(h_orig * _INFERENCE_SCALE)
w_inf = int(w_orig * _INFERENCE_SCALE)
img_inf = F.interpolate(img_255, size=(h_inf, w_inf), mode="bilinear", align_corners=False)
centerbias = torch.zeros(1, h_inf, w_inf, device=device, dtype=torch.float32)
with torch.no_grad():
log_density = self.saliency_detector(img_inf, centerbias)
smap = torch.exp(log_density[0, 0]).cpu().numpy()
smax = float(smap.max())
if smax > 0:
smap = smap / smax
smoothed = gaussian_filter(smap, sigma=_SPATIAL_SIGMA)
smax = float(smoothed.max())
if smax > 0:
smoothed = smoothed / smax
# _saliency_to_params (same as run_saliency_detection)
h, w = smoothed.shape
eps = 1e-8
peak = float(smoothed.max())
if peak < eps:
cx_norm, cy_norm, r = 0.0, 0.0, _R_MIN
else:
thresh = _DENSE_THRESHOLD * peak
dense_mask = smoothed >= thresh
dense_smap = smoothed * dense_mask
dense_total = dense_smap.sum() + eps
y_coords = np.arange(h, dtype=np.float64)
x_coords = np.arange(w, dtype=np.float64)
x_grid, y_grid = np.meshgrid(x_coords, y_coords)
cx_px = (dense_smap * x_grid).sum() / dense_total
cy_px = (dense_smap * y_grid).sum() / dense_total
cx_norm = (cx_px / w) - 0.5
cy_norm = (cy_px / h) - 0.5
dist = np.sqrt((x_grid - cx_px) ** 2 + (y_grid - cy_px) ** 2)
dense_pixels = dense_mask.ravel()
flat_dist = dist.ravel()[dense_pixels]
flat_sal = dense_smap.ravel()[dense_pixels]
sort_idx = np.argsort(flat_dist)
cumsum = np.cumsum(flat_sal[sort_idx])
target = _COVERAGE * dense_total
hit = min(np.searchsorted(cumsum, target), len(flat_dist) - 1)
radius_px = flat_dist[sort_idx[hit]]
half_diag = 0.5 * np.sqrt(h ** 2 + w ** 2)
r = float(np.clip(radius_px / half_diag, _R_MIN, _R_MAX))
# Binarized circular mask at full image res
cx = (cx_norm + 0.5) * w_orig
cy = (cy_norm + 0.5) * h_orig
diagonal = (h_orig ** 2 + w_orig ** 2) ** 0.5
radius_px = r * (diagonal / 2.0)
y = np.arange(h_orig, dtype=np.float64)
x = np.arange(w_orig, dtype=np.float64)
xx, yy = np.meshgrid(x, y)
dist_sq = (xx - cx) ** 2 + (yy - cy) ** 2
mask_hr = (dist_sq <= radius_px ** 2).astype(np.float32)
mask_hr = torch.from_numpy(mask_hr).to(device=device, dtype=torch.float32)
# Downsample by 32, binarize, then upsample 2x so effective factor is 16
mask_hr_4d = mask_hr.unsqueeze(0).unsqueeze(0)
h_32, w_32 = h_orig // 32, w_orig // 32
mask_32 = F.interpolate(mask_hr_4d, size=(h_32, w_32), mode="bilinear", align_corners=False)
mask_binary_32 = (mask_32.squeeze(0).squeeze(0) > 0.5).to(torch.float32).unsqueeze(0).unsqueeze(0)
latent_h, latent_w = h_orig // 16, w_orig // 16
mask_binary = F.interpolate(mask_binary_32, size=(latent_h, latent_w), mode="nearest")
return mask_binary
def process(self, pipe: Flux2FoveatedImagePipeline, input_image, noise, noise_downsampled, lr_downsample_factor=2):
if input_image is None:
return {
"latents": noise,
"latents_downsampled": noise_downsampled,
"input_latents": None,
"input_latents_downsampled": None,
"foveation_mask": None,
}
pipe.load_models_to_device(["vae"])
image = pipe.preprocess_image(input_image)
input_latents = pipe.vae.encode(image)
# Decide between saliency-based and bbox-based mask creation.
foveation_mask = self.get_foveation_mask_from_image(pipe, image)
#save_image((image + 1) / 2.0, "/home/brianchc/foveated_diffusion/paper_figures/input_image.png")
#save_image(foveation_mask, "/home/brianchc/foveated_diffusion/paper_figures/foveation_mask.png")
#exit()
_, _, latent_height, latent_width = input_latents.shape
input_latents = rearrange(input_latents, "B C H W -> B (H W) C")
# create downsample
image_downsampled = F.interpolate(image, size=(image.shape[-2]//lr_downsample_factor, image.shape[-1]//lr_downsample_factor), mode='bicubic', align_corners=False)
input_latents_downsampled = pipe.vae.encode(image_downsampled)
input_latents_downsampled = rearrange(input_latents_downsampled, "B C H W -> B (H W) C") # 4X less tokens
if pipe.scheduler.training:
return {
"latents": noise,
"latents_downsampled": noise_downsampled,
"input_latents": input_latents,
"input_latents_downsampled": input_latents_downsampled,
"foveation_mask": foveation_mask,
}
else:
latents = pipe.scheduler.add_noise(input_latents, noise, timestep=pipe.scheduler.timesteps[0])
latents_downsampled = pipe.scheduler.add_noise(input_latents_downsampled, noise_downsampled, timestep=pipe.scheduler.timesteps[0])
return {
"latents": latents,
"latents_downsampled": latents_downsampled,
"input_latents": input_latents,
"input_latents_downsampled": input_latents_downsampled,
"foveation_mask": foveation_mask,
}
class Flux2FoveatedUnit_ImageIDs(PipelineUnit):
def __init__(self):
super().__init__(
input_params=("height", "width", "foveation_mask", "lr_downsample_factor"),
output_params=("image_ids",),
)
def prepare_latent_ids(self, height, width, device, dtype, foveation_mask=None, lr_factor=2):
return Flux2FoveatedImagePipeline._prepare_latent_image_ids_foveation(
batch_size=1,
height=height,
width=width,
device=device,
dtype=dtype,
foveation_mask=foveation_mask,
lr_factor=lr_factor,
)
def process(self, pipe: Flux2FoveatedImagePipeline, height, width, foveation_mask, lr_downsample_factor=2):
latent_ids, resolution_mask, resolution_mask_top_left = self.prepare_latent_ids(
height // 16, width // 16, pipe.device, pipe.torch_dtype, foveation_mask, lr_factor=lr_downsample_factor
)
latent_ids = latent_ids.unsqueeze(0).expand(1, -1, -1)
return {
"image_ids": latent_ids,
"resolution_mask": resolution_mask,
"resolution_mask_top_left": resolution_mask_top_left,
}
# =====================================================================
# Model Function for Foveated FLUX2
# =====================================================================
def model_fn_flux2_foveated(
dit: Flux2DiTFoveated,
latents=None,
timestep=None,
embedded_guidance=None,
prompt_embeds=None,
text_ids=None,
image_ids=None,
resolution_mask=None,
resolution_mask_top_left=None,
lr_downsample_factor=2,
use_gradient_checkpointing=False,
use_gradient_checkpointing_offload=False,
**kwargs,
):
"""Model function for foveated FLUX2 generation."""
image_seq_len = latents.shape[1]
embedded_guidance = torch.tensor([embedded_guidance], device=latents.device)
model_output = dit(
hidden_states=latents,
timestep=timestep / 1000,
guidance=embedded_guidance,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=image_ids,
resolution_mask=resolution_mask,
resolution_mask_top_left=resolution_mask_top_left,
lr_factor=lr_downsample_factor,
use_gradient_checkpointing=use_gradient_checkpointing,
use_gradient_checkpointing_offload=use_gradient_checkpointing_offload,
)
model_output = model_output[:, :image_seq_len]
return model_output