Ratio Outpaint Calc : mask_padded + masque des bandes exact ; Region Mask List : extra_mask / empty
- Ratio Outpaint Calc : entree optionnelle mask -> sortie mask_padded (paddee exactement comme l'image). Masque des bandes construit avec le meme pad que l'image : il etait faux quand l'arrondi au multiple rognait la source (pad negatif). - Region Mask List : extra_mask (une region de plus, non decoupee, ex. les bandes d'outpaint) et empty = keep / full_image (aucune region -> image entiere). Par defaut : comportement inchange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ScF8qKYdotua6aKovsgdxV
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
62da29a66e
commit
ea2ef591a3
+48
-10
@@ -1,6 +1,8 @@
|
||||
import math
|
||||
import torch
|
||||
|
||||
from .tensor_utils import resize_lanczos
|
||||
|
||||
|
||||
RATIOS = {
|
||||
"1:1": (1, 1),
|
||||
@@ -37,6 +39,18 @@ class RatioOutpaintCalc:
|
||||
8 / 16 / 32 / 64 - arrondi au multiple le plus proche (round)
|
||||
sur les deux dimensions.
|
||||
Recommande : 16 (Flux), 64 (SDXL).
|
||||
|
||||
mask (optionnel, v1.7.1) :
|
||||
Un masque dessine sur l'image source. Il est remis a la taille de
|
||||
l'image si besoin, puis padde EXACTEMENT comme l'image (zeros dans les
|
||||
bandes) -> sortie 'mask_padded', alignee sur image_padded. Sert a garder
|
||||
les masques d'inpaint en place quand on outpainte dans le meme run.
|
||||
Non branche : mask_padded = zeros.
|
||||
|
||||
Pour un workflow ou les multiples sont geres plus loin (Aioli Mask BBox V2),
|
||||
preferer multiple = none : il ne fait qu'agrandir, sans jamais rogner la
|
||||
source. Les multiples 8..64 arrondissent au plus proche et peuvent rogner
|
||||
quelques pixels.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -46,22 +60,40 @@ class RatioOutpaintCalc:
|
||||
"image": ("IMAGE",),
|
||||
"ratio": (["none"] + list(RATIOS.keys()),),
|
||||
"multiple": (MULTIPLES, {"default": "16 (Flux)"}),
|
||||
}
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image_padded", "mask")
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "MASK")
|
||||
RETURN_NAMES = ("image_padded", "mask", "mask_padded")
|
||||
FUNCTION = "calculate"
|
||||
CATEGORY = "Aioli Nodes"
|
||||
|
||||
def calculate(self, image, ratio, multiple="16 (Flux)"):
|
||||
@staticmethod
|
||||
def _input_mask(mask, B, H, W):
|
||||
"""Masque d'entree aligne sur l'image (B, H, W) ; zeros si absent."""
|
||||
if mask is None:
|
||||
return torch.zeros((B, H, W), dtype=torch.float32)
|
||||
m = mask.float()
|
||||
if m.dim() == 2:
|
||||
m = m.unsqueeze(0)
|
||||
if m.shape[1] != H or m.shape[2] != W:
|
||||
print(f"[RatioOutpaintCalc] mask {m.shape[2]}x{m.shape[1]} != image {W}x{H} "
|
||||
f"-> mask redimensionne sur l'image")
|
||||
m = resize_lanczos(m, W, H, "mask")
|
||||
return m
|
||||
|
||||
def calculate(self, image, ratio, multiple="16 (Flux)", mask=None):
|
||||
B, H, W, C = image.shape
|
||||
in_mask = self._input_mask(mask, B, H, W)
|
||||
|
||||
# Mode ratio=none : pass-through, aucun padding
|
||||
if ratio == "none":
|
||||
print(f"[RatioOutpaintCalc] {W}x{H} | ratio=none, pass-through")
|
||||
mask = torch.zeros((B, H, W), dtype=torch.float32)
|
||||
return (image, mask)
|
||||
return (image, mask, in_mask)
|
||||
|
||||
rw, rh = RATIOS[ratio]
|
||||
|
||||
@@ -77,7 +109,7 @@ class RatioOutpaintCalc:
|
||||
else:
|
||||
print(f"[RatioOutpaintCalc] {W}x{H} deja au ratio {ratio}")
|
||||
mask = torch.zeros((B, H, W), dtype=torch.float32)
|
||||
return (image, mask)
|
||||
return (image, mask, in_mask)
|
||||
mult_label = "legacy x8-ceil"
|
||||
|
||||
else:
|
||||
@@ -93,7 +125,7 @@ class RatioOutpaintCalc:
|
||||
else:
|
||||
print(f"[RatioOutpaintCalc] {W}x{H} deja au ratio {ratio}")
|
||||
mask = torch.zeros((B, H, W), dtype=torch.float32)
|
||||
return (image, mask)
|
||||
return (image, mask, in_mask)
|
||||
mult_label = f"x{mult}-round"
|
||||
|
||||
pad_left = (new_W - W) // 2
|
||||
@@ -112,7 +144,13 @@ class RatioOutpaintCalc:
|
||||
)
|
||||
img = img.permute(0, 2, 3, 1)
|
||||
|
||||
mask = torch.ones((B, new_H, new_W), dtype=torch.float32)
|
||||
mask[:, pad_top:pad_top + H, pad_left:pad_left + W] = 0.0
|
||||
# Masques construits avec EXACTEMENT le meme pad que l'image. Un pad
|
||||
# negatif (l'arrondi au multiple descend sous la taille source) rogne
|
||||
# l'image : le masque des bandes doit le suivre. Avant 1.7.1, il etait
|
||||
# construit par indexation, qui donnait un masque faux dans ce cas.
|
||||
pads = (pad_left, pad_right, pad_top, pad_bottom)
|
||||
mask = torch.nn.functional.pad(
|
||||
torch.zeros((B, H, W), dtype=torch.float32), pads, mode="constant", value=1.0)
|
||||
mask_padded = torch.nn.functional.pad(in_mask, pads, mode="constant", value=0.0)
|
||||
|
||||
return (img, mask)
|
||||
return (img, mask, mask_padded)
|
||||
|
||||
+48
-11
@@ -14,7 +14,18 @@ class RegionMaskList:
|
||||
'list_first_on_top' et RegionalMaskConditioning, "dernier = dessous" -> le
|
||||
fond reste sous les objets. C'est pour ca que add_background ajoute en queue.
|
||||
|
||||
Cette meme liste alimente : AioliMaskBBox (bbox + crop + coords par region),
|
||||
v1.7.1, deux options (non branchees / par defaut : comportement d'avant) :
|
||||
extra_mask : un masque ajoute TEL QUEL comme une region de plus, apres
|
||||
les autres (avant le fond), s'il n'est pas vide.
|
||||
Typiquement le masque des bandes
|
||||
de Ratio Outpaint Calc : une seule region, toute l'image en
|
||||
contexte, a cote des masques dessines ou SAM3.
|
||||
empty : keep (defaut) -> les masques vides passent comme avant.
|
||||
full_image -> les masques vides (rien >= threshold) sont
|
||||
retires ; s'il ne reste aucune region, la liste devient UN
|
||||
masque plein : pas de masque = toute l'image est editee.
|
||||
|
||||
Cette meme liste alimente : AioliMaskBBox V2 (bbox + crop + coords par region),
|
||||
Gemma (caption par region), le KSampler (N passes) ET le conditionnement
|
||||
regional mode B. Le fond y est donc lui aussi captionne et enhance.
|
||||
|
||||
@@ -33,6 +44,8 @@ class RegionMaskList:
|
||||
"add_background": ("BOOLEAN", {"default": True}),
|
||||
"threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"min_bg_area": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"extra_mask": ("MASK",),
|
||||
"empty": (["keep", "full_image"], {"default": "keep"}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,14 +55,13 @@ class RegionMaskList:
|
||||
FUNCTION = "build"
|
||||
CATEGORY = "Aioli Nodes"
|
||||
|
||||
def build(self, masks, add_background=None, threshold=None, min_bg_area=None):
|
||||
add_background = bool((add_background or [True])[0])
|
||||
threshold = float((threshold or [0.5])[0])
|
||||
min_bg_area = float((min_bg_area or [0.0])[0])
|
||||
|
||||
# aplatit batch + liste -> liste de masques HxW
|
||||
@staticmethod
|
||||
def _flatten(masks):
|
||||
"""batch + liste -> liste de masques HxW."""
|
||||
flat = []
|
||||
for entry in masks:
|
||||
for entry in masks or []:
|
||||
if entry is None:
|
||||
continue
|
||||
t = entry
|
||||
if t.dim() == 3: # batch B,H,W
|
||||
for k in range(t.shape[0]):
|
||||
@@ -58,10 +70,34 @@ class RegionMaskList:
|
||||
flat.append(t.float())
|
||||
else:
|
||||
flat.append(t.reshape(t.shape[-2], t.shape[-1]).float())
|
||||
return flat
|
||||
|
||||
def build(self, masks, add_background=None, threshold=None, min_bg_area=None,
|
||||
extra_mask=None, empty=None):
|
||||
add_background = bool((add_background or [True])[0])
|
||||
threshold = float((threshold or [0.5])[0])
|
||||
min_bg_area = float((min_bg_area or [0.0])[0])
|
||||
empty = (empty or ["keep"])[0]
|
||||
|
||||
flat = self._flatten(masks)
|
||||
extras = self._flatten(extra_mask)
|
||||
n_in, n_extra = len(flat), len(extras)
|
||||
|
||||
ref = flat[0] if flat else (extras[0] if extras else None)
|
||||
# un extra_mask vide (ratio d'outpaint = none) n'ajoute jamais de region
|
||||
extras = [m for m in extras if (m >= threshold).any()]
|
||||
if empty == "full_image":
|
||||
flat = [m for m in flat if (m >= threshold).any()]
|
||||
flat = flat + extras
|
||||
|
||||
if not flat:
|
||||
empty = torch.zeros((1, 1), dtype=torch.float32)
|
||||
return ([empty], empty.unsqueeze(0), 0)
|
||||
if empty == "full_image" and ref is not None:
|
||||
full = torch.ones_like(ref)
|
||||
print(f"[RegionMaskList] aucune region ({n_in} masques, {n_extra} extra, vides) "
|
||||
f"-> 1 region = image entiere (empty=full_image)")
|
||||
return ([full], torch.zeros_like(ref).unsqueeze(0), 1)
|
||||
empty_m = torch.zeros((1, 1), dtype=torch.float32) if ref is None else torch.zeros_like(ref)
|
||||
return ([empty_m], empty_m.unsqueeze(0), 0)
|
||||
|
||||
H, W = flat[0].shape
|
||||
union = torch.zeros((H, W), dtype=torch.float32)
|
||||
@@ -78,6 +114,7 @@ class RegionMaskList:
|
||||
if add_background and bg.mean().item() > min_bg_area:
|
||||
out.append(bg)
|
||||
|
||||
print(f"[RegionMaskList] {len(flat)} masques SAM3"
|
||||
extra_info = f" (dont {len(extras)} extra)" if extras else ""
|
||||
print(f"[RegionMaskList] {len(flat)} masques{extra_info}"
|
||||
f"{' + fond (inverse union)' if len(out) > len(flat) else ''} -> {len(out)} calques")
|
||||
return (out, bg.unsqueeze(0), len(out))
|
||||
|
||||
Reference in New Issue
Block a user