Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

295 lines
12 KiB
Python

"""FLUX.2 Klein Reference Image Extension for multi-reference image editing.
This module provides the Flux2RefImageExtension for FLUX.2 Klein models,
which handles encoding reference images using the FLUX.2 VAE and
generating the appropriate position IDs for multi-reference image editing.
FLUX.2 Klein has built-in support for reference image editing (unlike FLUX.1
which requires a separate Kontext model).
"""
import math
import torch
import torch.nn.functional as F
import torchvision.transforms as T
from einops import repeat
from PIL import Image
from invokeai.app.invocations.fields import FluxKontextConditioningField
from invokeai.app.invocations.model import VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux2.sampling_utils import pack_flux2
from invokeai.backend.util.devices import TorchDevice
# Maximum pixel counts for reference images (matches BFL FLUX.2 sampling.py)
# Single reference image: 2024² pixels, Multiple: 1024² pixels
MAX_PIXELS_SINGLE_REF = 2024**2 # ~4.1M pixels
MAX_PIXELS_MULTI_REF = 1024**2 # ~1M pixels
def resize_image_to_max_pixels(image: Image.Image, max_pixels: int) -> Image.Image:
"""Resize image to fit within max_pixels while preserving aspect ratio.
This matches the BFL FLUX.2 sampling.py cap_pixels() behavior.
Args:
image: PIL Image to resize.
max_pixels: Maximum total pixel count (width * height).
Returns:
Resized PIL Image (or original if already within bounds).
"""
width, height = image.size
pixel_count = width * height
if pixel_count <= max_pixels:
return image
# Calculate scale factor to fit within max_pixels (BFL approach)
scale = math.sqrt(max_pixels / pixel_count)
new_width = int(width * scale)
new_height = int(height * scale)
# Ensure dimensions are at least 1
new_width = max(1, new_width)
new_height = max(1, new_height)
return image.resize((new_width, new_height), Image.Resampling.LANCZOS)
def generate_img_ids_flux2_with_offset(
latent_height: int,
latent_width: int,
batch_size: int,
device: torch.device,
idx_offset: int = 0,
h_offset: int = 0,
w_offset: int = 0,
) -> torch.Tensor:
"""Generate tensor of image position ids with optional offsets for FLUX.2.
FLUX.2 uses 4D position coordinates (T, H, W, L) for its rotary position embeddings.
Position IDs use int64 (long) dtype.
Args:
latent_height: Height of image in latent space (before packing).
latent_width: Width of image in latent space (before packing).
batch_size: Number of images in the batch.
device: Device to create tensors on.
idx_offset: Offset for T (time/index) coordinate - use 1 for reference images.
h_offset: Spatial offset for H coordinate in latent space.
w_offset: Spatial offset for W coordinate in latent space.
Returns:
Image position ids with shape [batch_size, (latent_height//2 * latent_width//2), 4].
"""
# After packing, the spatial dimensions are halved due to the 2x2 patch structure
packed_height = latent_height // 2
packed_width = latent_width // 2
# Convert spatial offsets from latent space to packed space
packed_h_offset = h_offset // 2
packed_w_offset = w_offset // 2
# Create base tensor for position IDs with shape [packed_height, packed_width, 4]
# The 4 channels represent: [T, H, W, L]
img_ids = torch.zeros(packed_height, packed_width, 4, device=device, dtype=torch.long)
# Set T (time/index offset) for all positions - use 1 for reference images
img_ids[..., 0] = idx_offset
# Set H (height/y) coordinates with offset
h_coords = torch.arange(packed_height, device=device, dtype=torch.long) + packed_h_offset
img_ids[..., 1] = h_coords[:, None]
# Set W (width/x) coordinates with offset
w_coords = torch.arange(packed_width, device=device, dtype=torch.long) + packed_w_offset
img_ids[..., 2] = w_coords[None, :]
# L (layer) coordinate stays 0
# Expand to include batch dimension: [batch_size, (packed_height * packed_width), 4]
img_ids = img_ids.reshape(1, packed_height * packed_width, 4)
img_ids = repeat(img_ids, "1 s c -> b s c", b=batch_size)
return img_ids
class Flux2RefImageExtension:
"""Applies FLUX.2 Klein reference image conditioning.
This extension handles encoding reference images using the FLUX.2 VAE
and generating the appropriate 4D position IDs for multi-reference image editing.
FLUX.2 Klein has built-in support for reference image editing, unlike FLUX.1
which requires a separate Kontext model.
"""
def __init__(
self,
ref_image_conditioning: list[FluxKontextConditioningField],
context: InvocationContext,
vae_field: VAEField,
device: torch.device,
dtype: torch.dtype,
bn_mean: torch.Tensor | None = None,
bn_std: torch.Tensor | None = None,
):
"""Initialize the Flux2RefImageExtension.
Args:
ref_image_conditioning: List of reference image conditioning fields.
context: The invocation context for loading models and images.
vae_field: The FLUX.2 VAE field for encoding images.
device: Target device for tensors.
dtype: Target dtype for tensors.
bn_mean: BN running mean for normalizing latents (shape: 128).
bn_std: BN running std for normalizing latents (shape: 128).
"""
self._context = context
self._device = device
self._dtype = dtype
self._vae_field = vae_field
self._bn_mean = bn_mean
self._bn_std = bn_std
self.ref_image_conditioning = ref_image_conditioning
# Pre-process and cache the reference image latents and ids upon initialization
self.ref_image_latents, self.ref_image_ids = self._prepare_ref_images()
def _bn_normalize(self, x: torch.Tensor) -> torch.Tensor:
"""Apply BN normalization to packed latents.
BN formula (affine=False): y = (x - mean) / std
Args:
x: Packed latents of shape (B, seq, 128).
Returns:
Normalized latents of same shape.
"""
assert self._bn_mean is not None and self._bn_std is not None
bn_mean = self._bn_mean.to(x.device, x.dtype)
bn_std = self._bn_std.to(x.device, x.dtype)
return (x - bn_mean) / bn_std
def _prepare_ref_images(self) -> tuple[torch.Tensor, torch.Tensor]:
"""Encode reference images and prepare their concatenated latents and IDs with spatial tiling."""
all_latents = []
all_ids = []
# Track cumulative dimensions for spatial tiling
canvas_h = 0
canvas_w = 0
vae_info = self._context.models.load(self._vae_field.vae)
# Determine max pixels based on number of reference images (BFL FLUX.2 approach)
num_refs = len(self.ref_image_conditioning)
max_pixels = MAX_PIXELS_SINGLE_REF if num_refs == 1 else MAX_PIXELS_MULTI_REF
for idx, ref_image_field in enumerate(self.ref_image_conditioning):
image = self._context.images.get_pil(ref_image_field.image.image_name)
image = image.convert("RGB")
# Resize large images to max pixel count (matches BFL FLUX.2 sampling.py)
image = resize_image_to_max_pixels(image, max_pixels)
# Convert to tensor using torchvision transforms
transformation = T.Compose([T.ToTensor()])
image_tensor = transformation(image)
# Convert from [0, 1] to [-1, 1] range expected by VAE
image_tensor = image_tensor * 2.0 - 1.0
image_tensor = image_tensor.unsqueeze(0) # Add batch dimension
# Encode using FLUX.2 VAE
with vae_info.model_on_device() as (_, vae):
vae_dtype = next(iter(vae.parameters())).dtype
image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
# FLUX.2 VAE uses diffusers API
latent_dist = vae.encode(image_tensor, return_dict=False)[0]
# Use mode() for deterministic encoding (no sampling)
if hasattr(latent_dist, "mode"):
ref_image_latents_unpacked = latent_dist.mode()
elif hasattr(latent_dist, "sample"):
ref_image_latents_unpacked = latent_dist.sample()
else:
ref_image_latents_unpacked = latent_dist
TorchDevice.empty_cache()
# Extract tensor dimensions (B, 32, H, W for FLUX.2)
batch_size, _, latent_height, latent_width = ref_image_latents_unpacked.shape
# Pad latents to be compatible with patch_size=2
pad_h = (2 - latent_height % 2) % 2
pad_w = (2 - latent_width % 2) % 2
if pad_h > 0 or pad_w > 0:
ref_image_latents_unpacked = F.pad(ref_image_latents_unpacked, (0, pad_w, 0, pad_h), mode="circular")
_, _, latent_height, latent_width = ref_image_latents_unpacked.shape
# Pack the latents using FLUX.2 pack function (32 channels -> 128)
ref_image_latents_packed = pack_flux2(ref_image_latents_unpacked).to(self._device, self._dtype)
# Apply BN normalization to match the input latents scale
# This is critical - the transformer expects normalized latents
if self._bn_mean is not None and self._bn_std is not None:
ref_image_latents_packed = self._bn_normalize(ref_image_latents_packed)
# Determine spatial offsets for this reference image
h_offset = 0
w_offset = 0
if idx > 0: # First image starts at (0, 0)
# Calculate potential canvas dimensions for each tiling option
potential_h_vertical = canvas_h + latent_height
potential_w_horizontal = canvas_w + latent_width
# Choose arrangement that minimizes the maximum dimension
if potential_h_vertical > potential_w_horizontal:
# Tile horizontally (to the right)
w_offset = canvas_w
canvas_w = canvas_w + latent_width
canvas_h = max(canvas_h, latent_height)
else:
# Tile vertically (below)
h_offset = canvas_h
canvas_h = canvas_h + latent_height
canvas_w = max(canvas_w, latent_width)
else:
canvas_h = latent_height
canvas_w = latent_width
# Generate position IDs with 4D format (T, H, W, L)
# Use T-coordinate offset with scale=10 like diffusers Flux2Pipeline:
# T = scale + scale * idx (so first ref image is T=10, second is T=20, etc.)
# The generated image uses T=0, so this clearly separates reference images
t_offset = 10 + 10 * idx # scale=10 matches diffusers
ref_image_ids = generate_img_ids_flux2_with_offset(
latent_height=latent_height,
latent_width=latent_width,
batch_size=batch_size,
device=self._device,
idx_offset=t_offset, # Reference images use T=10, 20, 30...
h_offset=h_offset,
w_offset=w_offset,
)
all_latents.append(ref_image_latents_packed)
all_ids.append(ref_image_ids)
# Concatenate all latents and IDs along the sequence dimension
concatenated_latents = torch.cat(all_latents, dim=1)
concatenated_ids = torch.cat(all_ids, dim=1)
return concatenated_latents, concatenated_ids
def ensure_batch_size(self, target_batch_size: int) -> None:
"""Ensure the reference image latents and IDs match the target batch size."""
if self.ref_image_latents.shape[0] != target_batch_size:
self.ref_image_latents = self.ref_image_latents.repeat(target_batch_size, 1, 1)
self.ref_image_ids = self.ref_image_ids.repeat(target_batch_size, 1, 1)