Files
mo230761-InsertAnything-Com…/insert_anything_node.py
T

525 lines
20 KiB
Python

import torch
import numpy as np
import cv2
from PIL import Image
from diffusers import FluxFillPipeline, FluxPriorReduxPipeline
import math
import torch
import numpy as np
import cv2
from PIL import Image
def standardize_and_binarize_mask(mask_input):
"""
Converts a ComfyUI MASK input (torch.Tensor) to a standardized
2D NumPy array (H, W), uint8, binarized (0 or 255).
"""
if isinstance(mask_input, torch.Tensor):
mask_np = mask_input.cpu().numpy()
elif isinstance(mask_input, np.ndarray):
mask_np = mask_input
else:
raise TypeError(f"Mask input type {type(mask_input)} not supported. Expected torch.Tensor or np.ndarray.")
# Handle common ComfyUI mask shapes like (N, H, W) or (N, C, H, W)
if mask_np.ndim == 3 and mask_np.shape[0] == 1: # (1, H, W)
mask_np = mask_np.squeeze(0)
elif mask_np.ndim == 4 and mask_np.shape[0] == 1 and mask_np.shape[1] == 1: # (1,1,H,W)
mask_np = mask_np.squeeze(0).squeeze(0)
# Ensure single channel (H, W)
if mask_np.ndim == 3:
if mask_np.shape[-1] == 1: # (H, W, 1)
mask_np = mask_np.squeeze(axis=-1)
elif mask_np.shape[0] == 1: # Also (1, H, W) if not caught above
mask_np = mask_np.squeeze(0)
elif mask_np.shape[-1] > 1: # (H, W, C) with C > 1, e.g., RGB mask passed incorrectly
mask_np = mask_np[..., 0]
if mask_np.ndim != 2:
raise ValueError(f"Mask has unexpected shape {mask_np.shape} after initial processing. Expected 2D mask (H,W). Original input shape was {mask_input.shape if hasattr(mask_input, 'shape') else 'unknown'}.")
# Normalize to 0-255 uint8 if it's a float mask (0.0-1.0)
if mask_np.dtype == np.float32 or mask_np.dtype == np.float64:
if mask_np.min() >= 0.0 and mask_np.max() <= 1.0:
mask_np = (mask_np * 255).astype(np.uint8)
else: # Floats not in 0-1 range, clip and convert
mask_np = np.clip(mask_np, 0, 255).astype(np.uint8)
elif mask_np.dtype != np.uint8: # Other types, attempt to convert
mask_np = np.clip(mask_np, 0, 255).astype(np.uint8)
# Binarize the mask: values > 128 become 255, others 0.
_, mask_np_binarized = cv2.threshold(mask_np, 128, 255, cv2.THRESH_BINARY)
return mask_np_binarized
def create_highlighted_mask(image_np, mask_np, alpha=0.5, gray_value=128):
if mask_np.max() <= 1.0:
mask_np = (mask_np * 255).astype(np.uint8)
mask_bool = mask_np > 128
image_float = image_np.astype(np.float32)
gray_overlay = np.full_like(image_float, gray_value, dtype=np.float32)
result = image_float.copy()
result[mask_bool] = (
(1 - alpha) * image_float[mask_bool] + alpha * gray_overlay[mask_bool]
)
return result.astype(np.uint8)
def f(r, T=0.6, beta=0.1):
return np.where(r < T, beta + (1 - beta) / T * r, 1)
# Get the bounding box of the mask
def get_bbox_from_mask(mask):
h,w = mask.shape[0],mask.shape[1]
if mask.sum() < 10:
return 0,h,0,w
rows = np.any(mask,axis=1)
cols = np.any(mask,axis=0)
y1,y2 = np.where(rows)[0][[0,-1]]
x1,x2 = np.where(cols)[0][[0,-1]]
return (y1,y2,x1,x2)
# Expand the bounding box
def expand_bbox(mask, yyxx, ratio, min_crop=0):
y1,y2,x1,x2 = yyxx
H,W = mask.shape[0], mask.shape[1]
yyxx_area = (y2-y1+1) * (x2-x1+1)
r1 = yyxx_area / (H * W)
r2 = f(r1)
ratio = math.sqrt(r2 / r1)
xc, yc = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
h = ratio * (y2-y1+1)
w = ratio * (x2-x1+1)
h = max(h,min_crop)
w = max(w,min_crop)
x1 = int(xc - w * 0.5)
x2 = int(xc + w * 0.5)
y1 = int(yc - h * 0.5)
y2 = int(yc + h * 0.5)
x1 = max(0,x1)
x2 = min(W,x2)
y1 = max(0,y1)
y2 = min(H,y2)
return (y1,y2,x1,x2)
# Pad the image to a square shape
def pad_to_square(image, pad_value = 255, random = False):
H,W = image.shape[0], image.shape[1]
if H == W:
return image
padd = abs(H - W)
if random:
padd_1 = int(np.random.randint(0,padd))
else:
padd_1 = int(padd / 2)
padd_2 = padd - padd_1
if len(image.shape) == 2:
if H > W:
pad_param = ((0, 0), (padd_1, padd_2))
else:
pad_param = ((padd_1, padd_2), (0, 0))
elif len(image.shape) == 3:
if H > W:
pad_param = ((0, 0), (padd_1, padd_2), (0, 0))
else:
pad_param = ((padd_1, padd_2), (0, 0), (0, 0))
image = np.pad(image, pad_param, 'constant', constant_values=pad_value)
return image
# Expand the image and mask
def expand_image_mask(image, mask, ratio=1.4):
h,w = image.shape[0], image.shape[1]
H,W = int(h * ratio), int(w * ratio)
h1 = int((H - h) // 2)
h2 = H - h - h1
w1 = int((W -w) // 2)
w2 = W -w - w1
pad_param_image = ((h1,h2),(w1,w2),(0,0))
pad_param_mask = ((h1,h2),(w1,w2))
image = np.pad(image, pad_param_image, 'constant', constant_values=255)
mask = np.pad(mask, pad_param_mask, 'constant', constant_values=0)
return image, mask
# Convert the bounding box to a square shape
def box2squre(image, box):
H,W = image.shape[0], image.shape[1]
y1,y2,x1,x2 = box
cx = (x1 + x2) // 2
cy = (y1 + y2) // 2
h,w = y2-y1, x2-x1
if h >= w:
x1 = cx - h//2
x2 = cx + h//2
else:
y1 = cy - w//2
y2 = cy + w//2
x1 = max(0,x1)
x2 = min(W,x2)
y1 = max(0,y1)
y2 = min(H,y2)
return (y1,y2,x1,x2)
# Crop the predicted image back to the original image
def crop_back( pred, tar_image, extra_sizes, tar_box_yyxx_crop):
H1, W1, H2, W2 = extra_sizes
y1,y2,x1,x2 = tar_box_yyxx_crop
pred = cv2.resize(pred, (W2, H2))
m = 2 # maigin_pixel
if W1 == H1:
if m != 0:
tar_image[y1+m :y2-m, x1+m:x2-m, :] = pred[m:-m, m:-m]
else:
tar_image[y1 :y2, x1:x2, :] = pred[:, :]
return tar_image
if W1 < W2:
pad1 = int((W2 - W1) / 2)
pad2 = W2 - W1 - pad1
pred = pred[:,pad1: -pad2, :]
else:
pad1 = int((H2 - H1) / 2)
pad2 = H2 - H1 - pad1
pred = pred[pad1: -pad2, :, :]
gen_image = tar_image.copy()
if m != 0:
gen_image[y1+m :y2-m, x1+m:x2-m, :] = pred[m:-m, m:-m]
else:
gen_image[y1 :y2, x1:x2, :] = pred[:, :]
return gen_image
class MaskOption:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sketch": ("MASK", ),
"upload": ("MASK", ),
"option": (["sketch", "upload"], {"default": "sketch"}),
}
}
RETURN_TYPES = ("MASK", )
FUNCTION = "MaskOption"
def MaskOption(self, sketch, upload, option):
if option == "sketch":
mask = sketch
elif option == "upload":
mask = upload
return mask
class ReduxProcess:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"ref_image": ("IMAGE", ),
"ref_mask": ("MASK", ),
}
}
RETURN_TYPES = ("IMAGE", )
FUNCTION = "ReduxProcess"
def ReduxProcess(self, ref_image, ref_mask):
ref_image_np = np.array(ref_image)[0] # Assuming ref_image is a batch of 1
ref_image_np = (ref_image_np * 255).round().astype(np.uint8)
# Standardize and binarize the reference mask
ref_mask_np = standardize_and_binarize_mask(ref_mask) # ref_mask is the MASK input
# Remove the background information of the reference picture
# Ensure ref_mask_np is 2D before passing to get_bbox_from_mask
ref_box_yyxx = get_bbox_from_mask(ref_mask_np)
# Create a 0/1 mask for multiplication (0 for background, 1 for foreground)
ref_mask_0_1 = ref_mask_np // 255
ref_mask_3_0_1 = np.stack([ref_mask_0_1, ref_mask_0_1, ref_mask_0_1], axis=-1)
# Apply mask: foreground where mask is 1, white background where mask is 0
masked_ref_image = ref_image_np * ref_mask_3_0_1 + \
np.ones_like(ref_image_np, dtype=np.uint8) * 255 * (1 - ref_mask_3_0_1)
masked_ref_image = masked_ref_image.astype(np.uint8)
# Extract the box where the reference image is located, and place the reference object at the center of the image
y1,y2,x1,x2 = ref_box_yyxx
# Ensure coordinates are valid for slicing
y1, y2 = max(0, y1), min(masked_ref_image.shape[0], y2)
x1, x2 = max(0, x1), min(masked_ref_image.shape[1], x2)
if y1 >= y2 or x1 >= x2: # Handle empty crop case
pass
masked_ref_image = masked_ref_image[y1:y2, x1:x2, :]
ref_mask_processed = ref_mask_np[y1:y2, x1:x2] # Use the binarized mask for consistency
ratio = 1.3
masked_ref_image, ref_mask_expanded = expand_image_mask(masked_ref_image, ref_mask_processed, ratio=ratio)
masked_ref_image = pad_to_square(masked_ref_image, pad_value = 255, random = False)
# ref_mask_expanded is the corresponding mask for the expanded image, if needed later.
# Extract the features of the reference image
masked_ref_image = cv2.resize(masked_ref_image.astype(np.uint8), (768, 768)).astype(np.uint8)
masked_ref_image = torch.from_numpy(masked_ref_image).unsqueeze(0).float() / 255.0
return (masked_ref_image, )
class FillProcess:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"source_image": ("IMAGE", ),
"ref_image": ("IMAGE", ),
"source_mask": ("MASK", ),
"ref_mask": ("MASK", ),
"iterations": ("INT", {"default": 2}),
}
}
RETURN_TYPES = (
"IMAGE", "MASK", "IMAGE", "BOX", "CROP_PARAMS", "IMAGE", # Adjusted
)
RETURN_NAMES = ( # Adjusted
"image", "mask", "old_tar_image", "tar_box_yyxx_crop", "crop_params", "preview_image",
)
FUNCTION = "FillProcess"
def FillProcess(self, source_image, ref_image, source_mask, ref_mask, iterations):
size = (768, 768)
ref_image_np = np.array(ref_image)[0] # Assuming batch of 1
tar_image_np = np.array(source_image)[0] # Assuming batch of 1
ref_image_np = (ref_image_np * 255).round().astype(np.uint8)
tar_image_np = (tar_image_np * 255).round().astype(np.uint8)
# Standardize and binarize masks
ref_mask_np = standardize_and_binarize_mask(ref_mask) # ref_mask is MASK input
tar_mask_np = standardize_and_binarize_mask(source_mask) # source_mask is MASK input
# --- Reference Image Processing (similar to ReduxProcess) ---
ref_box_yyxx = get_bbox_from_mask(ref_mask_np)
ref_mask_0_1 = ref_mask_np // 255
ref_mask_3_0_1 = np.stack([ref_mask_0_1, ref_mask_0_1, ref_mask_0_1], axis=-1)
masked_ref_image = ref_image_np * ref_mask_3_0_1 + \
np.ones_like(ref_image_np, dtype=np.uint8) * 255 * (1 - ref_mask_3_0_1)
masked_ref_image = masked_ref_image.astype(np.uint8)
y1_ref, y2_ref, x1_ref, x2_ref = ref_box_yyxx
y1_ref, y2_ref = max(0, y1_ref), min(masked_ref_image.shape[0], y2_ref)
x1_ref, x2_ref = max(0, x1_ref), min(masked_ref_image.shape[1], x2_ref)
if not(y1_ref >= y2_ref or x1_ref >= x2_ref): # Check for valid crop
masked_ref_image = masked_ref_image[y1_ref:y2_ref, x1_ref:x2_ref, :]
ref_mask_cropped = ref_mask_np[y1_ref:y2_ref, x1_ref:x2_ref]
else: # Fallback if bbox is invalid
ref_mask_cropped = ref_mask_np # use original mask if crop failed
ratio_ref = 1.3
masked_ref_image, _ = expand_image_mask(masked_ref_image, ref_mask_cropped, ratio=ratio_ref) # We mainly need the image
masked_ref_image = pad_to_square(masked_ref_image, pad_value = 255, random = False)
# --- End Reference Image Processing ---
# Dilate the target mask
kernel = np.ones((7, 7), np.uint8)
tar_mask_dilated = cv2.dilate(tar_mask_np, kernel, iterations=iterations)
# Zoom in on the target image based on the dilated target mask
tar_box_yyxx = get_bbox_from_mask(tar_mask_dilated)
tar_box_yyxx_expanded = expand_bbox(tar_mask_dilated, tar_box_yyxx, ratio=1.2) # Pass tar_mask_dilated
tar_box_yyxx_crop = expand_bbox(tar_image_np, tar_box_yyxx_expanded, ratio=2)
tar_box_yyxx_crop = expand_bbox(tar_mask_dilated, tar_box_yyxx_expanded, ratio=2)
tar_box_yyxx_crop = box2squre(tar_image_np, tar_box_yyxx_crop) # box2square uses image for H,W
y1_tar, y2_tar, x1_tar, x2_tar = tar_box_yyxx_crop
old_tar_image_np = tar_image_np.copy()
# Ensure crop coordinates are valid
y1_tar, y2_tar = max(0, y1_tar), min(tar_image_np.shape[0], y2_tar)
x1_tar, x2_tar = max(0, x1_tar), min(tar_image_np.shape[1], x2_tar)
if y1_tar >= y2_tar or x1_tar >= x2_tar: # Handle empty crop
# Fallback: use full image, or handle error
# This indicates an issue with bbox calculation, possibly empty mask
processed_tar_image = tar_image_np.copy()
processed_tar_mask = tar_mask_dilated.copy() # or tar_mask_np
else:
processed_tar_image = tar_image_np[y1_tar:y2_tar, x1_tar:x2_tar, :]
processed_tar_mask = tar_mask_dilated[y1_tar:y2_tar, x1_tar:x2_tar]
H1, W1 = processed_tar_image.shape[0], processed_tar_image.shape[1]
processed_tar_mask = pad_to_square(processed_tar_mask, pad_value=0)
processed_tar_mask = cv2.resize(processed_tar_mask, size) # size is (768,768)
# Resize reference image (already processed and padded to square)
masked_ref_image_resized = cv2.resize(masked_ref_image.astype(np.uint8), size).astype(np.uint8)
processed_tar_image_padded = pad_to_square(processed_tar_image, pad_value=255)
H2, W2 = processed_tar_image_padded.shape[0], processed_tar_image_padded.shape[1]
processed_tar_image_resized = cv2.resize(processed_tar_image_padded, size)
diptych_ref_tar = np.concatenate([masked_ref_image_resized, processed_tar_image_resized], axis=1)
# Create diptych mask (mask is for the target part)
processed_tar_mask_3ch = np.stack([processed_tar_mask, processed_tar_mask, processed_tar_mask], -1)
mask_black_ref_part = np.zeros_like(masked_ref_image_resized, dtype=np.uint8) # Black mask for ref part
mask_diptych = np.concatenate([mask_black_ref_part, processed_tar_mask_3ch], axis=1)
show_diptych_ref_tar = create_highlighted_mask(diptych_ref_tar, mask_diptych)
output_mask_diptych_np = mask_diptych[:, :, 0]
diptych_ref_tar_tensor = torch.from_numpy(diptych_ref_tar.copy()).unsqueeze(0).float() / 255.0
mask_diptych_tensor = torch.from_numpy(output_mask_diptych_np.copy()).unsqueeze(0).unsqueeze(1).float() / 255.0 # (B, C, H, W) so (1,1,H,W)
mask_diptych_tensor = torch.from_numpy(output_mask_diptych_np.copy()).unsqueeze(0).float() / 255.0 # This was duplicated, using the (1,1,H,W) version above this line. Correcting.
# The line above was: mask_diptych_tensor = torch.from_numpy(output_mask_diptych_np.copy()).unsqueeze(0).unsqueeze(1).float() / 255.0
# The line below it was: mask_diptych_tensor = torch.from_numpy(output_mask_diptych_np.copy()).unsqueeze(0).float() / 255.0
# The unsqueeze(1) is for channel, so (B, C, H, W) is (1,1,H,W) for a single channel mask.
# Let's ensure the correct one is used. The one with unsqueeze(1) is typical for masks.
# Re-evaluating: output_mask_diptych_np is (H,W).
# For ComfyUI MASK type, it's often (B, H, W) or (B, 1, H, W).
# If it's (B,H,W) then .unsqueeze(0).float() / 255.0 is fine.
# If it needs to be (B,1,H,W) then .unsqueeze(0).unsqueeze(1).float() / 255.0 is correct.
# The original code had both, the second one overwriting the first.
# Let's stick to the (B, H, W) convention for mask output unless (B,1,H,W) is strictly needed by other nodes.
# Given `mask_diptych_tensor = torch.from_numpy(output_mask_diptych_np.copy()).unsqueeze(0).float() / 255.0` was the last one, let's assume it's (B,H,W)
# However, the type hint for MASK in ComfyUI is often a 2D tensor (H,W) or (1,H,W) for batch 1.
# Let's assume the output MASK type should be (1, H, W) for consistency with typical ComfyUI mask tensors.
# output_mask_diptych_np is (H,W). So, .unsqueeze(0) makes it (1,H,W). This is fine.
old_tar_image_tensor = torch.from_numpy(old_tar_image_np.astype(np.float32) / 255.0).unsqueeze(0)
crop_params_tuple = (H1, W1, H2, W2)
# tar_box_yyxx_crop is already a tuple (y1,y2,x1,x2)
show_diptych_ref_tar_tensor = torch.from_numpy(show_diptych_ref_tar.copy()).unsqueeze(0).float() / 255.0
return (diptych_ref_tar_tensor, mask_diptych_tensor, old_tar_image_tensor, tar_box_yyxx_crop, crop_params_tuple, show_diptych_ref_tar_tensor)
class CropBack:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"raw_image": ("IMAGE",),
"old_tar_image": ("IMAGE",), # Changed from custom type to IMAGE
"tar_box_yyxx_crop": ("BOX",), # Assuming BOX is the type for the tuple
"crop_params": ("CROP_PARAMS",) # Assuming CROP_PARAMS for (H1,W1,H2,W2)
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "CropBack"
def CropBack(
self,
raw_image,
old_tar_image, # This is now a tensor
tar_box_yyxx_crop, # This is a tuple (y1,y2,x1,x2)
crop_params # This is a tuple (H1,W1,H2,W2)
):
# Unpack crop_params
H1, W1, H2, W2 = crop_params
# Convert old_tar_image tensor to numpy array for crop_back helper
old_tar_image_np = (old_tar_image[0].cpu().numpy() * 255).round().astype(np.uint8)
raw_image_np = np.array(raw_image)[0] # raw_image is already a tensor
raw_image_np = (raw_image_np * 255).round().astype(np.uint8)
# Convert raw_image_np to PIL Image for cropping the right half
pil_raw_image = Image.fromarray(raw_image_np)
width, height = pil_raw_image.size
left = width // 2
right = width
top = 0
bottom = height
edited_image_pil = pil_raw_image.crop((left, top, right, bottom))
edited_image_np = np.array(edited_image_pil)
# tar_box_yyxx_crop is already a tuple, convert to numpy array if crop_back expects it
# The crop_back helper function expects tar_image (old_tar_image_np) and tar_box_yyxx_crop as numpy arrays.
# extra_sizes is also expected as a numpy array.
edited_image_final_np = crop_back(edited_image_np, old_tar_image_np,
np.array([H1, W1, H2, W2]),
np.array(tar_box_yyxx_crop))
edited_image_tensor = torch.from_numpy(edited_image_final_np.astype(np.float32) / 255.0).unsqueeze(0)
return (edited_image_tensor,)
raw_image = (raw_image * 255).round().astype(np.uint8)
raw_image = Image.fromarray(raw_image)
# This block is now part of the new structure above.
# raw_image = np.array(raw_image)[0]
# raw_image = (raw_image * 255).round().astype(np.uint8)
# raw_image = Image.fromarray(raw_image)
# width, height = raw_image.size
# left = width // 2
# right = width
# top = 0
# bottom = height
# edited_image = raw_image.crop((left, top, right, bottom))
# edited_image = np.array(edited_image)
# edited_image = crop_back(edited_image, old_tar_image, np.array([H1, W1, H2, W2]), np.array(tar_box_yyxx_crop))
# edited_image = torch.from_numpy(edited_image).unsqueeze(0).float() / 255.0
# return (edited_image,)
pass # Placeholder for the removed block, the logic is now in the new method body