feat: initial release of quick merge engine
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
import sys
|
||||
import subprocess
|
||||
import importlib
|
||||
|
||||
def install_dependencies():
|
||||
required_packages = {
|
||||
"transformers": "transformers",
|
||||
"torchvision": "torchvision",
|
||||
"huggingface_hub": "huggingface-hub"
|
||||
}
|
||||
|
||||
missing_packages = []
|
||||
for module_name, pip_name in required_packages.items():
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
except ImportError:
|
||||
missing_packages.append(pip_name)
|
||||
|
||||
if missing_packages:
|
||||
print(f"\n[🎨 Quick Merge] Missing dependencies detected: {', '.join(missing_packages)}")
|
||||
print(f"[🎨 Quick Merge] Auto-installing now. Please wait...\n")
|
||||
try:
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + missing_packages)
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"\n[🎨 Quick Merge] ERROR: Failed auto-installation. Run: pip install {' '.join(missing_packages)}\n")
|
||||
|
||||
install_dependencies()
|
||||
|
||||
from .quick_merge_node import QuickMergeNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"QuickMergeNode": QuickMergeNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"QuickMergeNode": "🎨 Quick Merge"
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,225 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.QuickMergeNode",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "QuickMergeNode") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) {
|
||||
onNodeCreated.apply(this, arguments);
|
||||
}
|
||||
|
||||
const node = this;
|
||||
node.setSize([380, node.computeSize()[1]]);
|
||||
|
||||
setTimeout(() => {
|
||||
const groups = {
|
||||
"📏 initial_resize": [
|
||||
"📏 fg_resize_mode", "📏 fg_crop_align", "📏 fg_initial_width", "📏 fg_initial_height",
|
||||
"📏 bg_resize_mode", "📏 bg_crop_align", "📏 bg_initial_width", "📏 bg_initial_height"
|
||||
],
|
||||
"✂️ merge_image": [
|
||||
"✂️ cutout_model", "✂️ auto-position (WiP)", "✂️ foreground_size", "✂️ foreground_left_right",
|
||||
"✂️ foreground_up_down", "✂️ flip_horizontal", "✂️ flip_vertical",
|
||||
"✂️ rotation_degrees", "✂️ edge_shrink_grow", "✂️ edge_softness",
|
||||
"✂️ foreground_opacity", "✂️ blend_mode", "✂️ blend_strength"
|
||||
],
|
||||
"⚡ ai_upscale": [
|
||||
"⚡ pipeline_order", "⚡ choose_upscale_model", "⚡ upscale_by",
|
||||
"⚡ tile_size (0 means off/native)", "⚡ post_upscale_edge_soften"
|
||||
],
|
||||
"🎨 colors_and_filters": [
|
||||
"🎨 filter_target", "🎨 filter_type", "🎨 filter_strength", "🎨 brightness",
|
||||
"🎨 contrast", "🎨 saturation", "🎨 filter_size", "🎨 background_blur"
|
||||
],
|
||||
"🌗 shadows": [
|
||||
"🌗 shadow_color_hex", "🌗 shadow_darkness", "🌗 shadow_softness",
|
||||
"🌗 shadow_position_x", "🌗 shadow_position_y"
|
||||
],
|
||||
"🎯 target_resolution": [
|
||||
"🎯 target_resize_mode", "🎯 target_crop_align", "🎯 target_width", "🎯 target_height"
|
||||
]
|
||||
};
|
||||
|
||||
const updateVisibility = () => {
|
||||
let isDirty = false;
|
||||
|
||||
const masterBatchWidget = node.widgets.find(w => w.name === "📁 batch_processing");
|
||||
const masterBatchActive = masterBatchWidget ? masterBatchWidget.value : false;
|
||||
|
||||
const fgBatchWidget = node.widgets.find(w => w.name === "📁 foreground_batch");
|
||||
const bgBatchWidget = node.widgets.find(w => w.name === "📁 background_batch");
|
||||
|
||||
["📁 foreground_batch", "📁 background_batch"].forEach(name => {
|
||||
const w = node.widgets.find(wg => wg.name === name);
|
||||
if (w) {
|
||||
if (!masterBatchActive) {
|
||||
if (w.type !== "hidden") {
|
||||
w._original_type = w.type;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, -4];
|
||||
isDirty = true;
|
||||
}
|
||||
} else {
|
||||
if (w.type === "hidden") {
|
||||
w.type = w._original_type || "toggle";
|
||||
w.computeSize = () => [380, 20];
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fgFolderWidget = node.widgets.find(w => w.name === "📁 fg_batch_folder");
|
||||
if (fgFolderWidget) {
|
||||
const visible = masterBatchActive && fgBatchWidget && fgBatchWidget.value;
|
||||
if (!visible && fgFolderWidget.type !== "hidden") {
|
||||
fgFolderWidget._original_type = fgFolderWidget.type;
|
||||
fgFolderWidget.type = "hidden";
|
||||
fgFolderWidget.computeSize = () => [0, -4];
|
||||
isDirty = true;
|
||||
} else if (visible && fgFolderWidget.type === "hidden") {
|
||||
fgFolderWidget.type = fgFolderWidget._original_type || "text";
|
||||
fgFolderWidget.computeSize = () => [380, 20];
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
const bgFolderWidget = node.widgets.find(w => w.name === "📁 bg_batch_folder");
|
||||
if (bgFolderWidget) {
|
||||
const visible = masterBatchActive && bgBatchWidget && bgBatchWidget.value;
|
||||
if (!visible && bgFolderWidget.type !== "hidden") {
|
||||
bgFolderWidget._original_type = bgFolderWidget.type;
|
||||
bgFolderWidget.type = "hidden";
|
||||
bgFolderWidget.computeSize = () => [0, -4];
|
||||
isDirty = true;
|
||||
} else if (visible && bgFolderWidget.type === "hidden") {
|
||||
bgFolderWidget.type = bgFolderWidget._original_type || "text";
|
||||
bgFolderWidget.computeSize = () => [380, 20];
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
const anySubBatchActive = masterBatchActive && ((fgBatchWidget && fgBatchWidget.value) || (bgBatchWidget && bgBatchWidget.value));
|
||||
["📁 batch_pairing_mode", "📁 batch_chunk_size"].forEach(name => {
|
||||
const w = node.widgets.find(wg => wg.name === name);
|
||||
if (w) {
|
||||
if (!anySubBatchActive) {
|
||||
if (w.type !== "hidden") {
|
||||
w._original_type = w.type;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, -4];
|
||||
isDirty = true;
|
||||
}
|
||||
} else {
|
||||
if (w.type === "hidden") {
|
||||
w.type = w._original_type || "combo";
|
||||
w.computeSize = () => [380, 20];
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const [toggleName, widgetNames] of Object.entries(groups)) {
|
||||
const toggleWidget = node.widgets.find(w => w.name === toggleName);
|
||||
if (toggleWidget) {
|
||||
const isVisible = toggleWidget.value;
|
||||
for (const wName of widgetNames) {
|
||||
const w = node.widgets.find(wg => wg.name === wName);
|
||||
if (w) {
|
||||
if (!isVisible) {
|
||||
if (w.type !== "hidden") {
|
||||
w._original_type = w.type;
|
||||
w._original_computeSize = w.computeSize;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, -4];
|
||||
isDirty = true;
|
||||
}
|
||||
} else {
|
||||
if (w.type === "hidden") {
|
||||
w.type = w._original_type || "text";
|
||||
w.computeSize = w._original_computeSize || (() => [380, 20]);
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDirty) {
|
||||
node.setSize([380, node.computeSize()[1]]);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const setupPlaceholder = (widgetName, defaultText) => {
|
||||
const widget = node.widgets.find(w => w.name === widgetName);
|
||||
if (widget) {
|
||||
const originalMouse = widget.mouse;
|
||||
widget.mouse = function(e, pos) {
|
||||
if (widget.type === "hidden") return false;
|
||||
if (e.type === "mousedown" || e.type === "pointerdown") {
|
||||
if (widget.value === defaultText) {
|
||||
widget.value = "";
|
||||
if (widget.callback) widget.callback(widget.value);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
if (originalMouse) return originalMouse.apply(this, arguments);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
setupPlaceholder("📁 fg_batch_folder", "📁 Type or paste folder path here...");
|
||||
setupPlaceholder("📁 bg_batch_folder", "📁 Type or paste folder path here...");
|
||||
|
||||
const shadowHexWidget = node.widgets.find(w => w.name === "🌗 shadow_color_hex");
|
||||
if (shadowHexWidget) {
|
||||
const originalMouse = shadowHexWidget.mouse;
|
||||
shadowHexWidget.mouse = function(e, pos) {
|
||||
if (shadowHexWidget.type === "hidden") return false;
|
||||
if (e.type === "mousedown" || e.type === "pointerdown") {
|
||||
const colorPicker = document.createElement("input");
|
||||
colorPicker.type = "color";
|
||||
colorPicker.value = shadowHexWidget.value.startsWith('#') ? shadowHexWidget.value : '#' + shadowHexWidget.value;
|
||||
colorPicker.style.position = "absolute";
|
||||
colorPicker.style.opacity = "0";
|
||||
colorPicker.style.pointerEvents = "none";
|
||||
document.body.appendChild(colorPicker);
|
||||
|
||||
colorPicker.addEventListener("input", (event) => {
|
||||
shadowHexWidget.value = event.target.value;
|
||||
if (shadowHexWidget.callback) shadowHexWidget.callback(shadowHexWidget.value);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
colorPicker.addEventListener("change", () => {
|
||||
document.body.removeChild(colorPicker);
|
||||
});
|
||||
colorPicker.click();
|
||||
return true;
|
||||
}
|
||||
if (originalMouse) return originalMouse.apply(this, arguments);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
for (const toggleName of [...Object.keys(groups), "📁 batch_processing", "📁 foreground_batch", "📁 background_batch"]) {
|
||||
const toggleWidget = node.widgets.find(w => w.name === toggleName);
|
||||
if (toggleWidget) {
|
||||
const originalCallback = toggleWidget.callback;
|
||||
toggleWidget.callback = function () {
|
||||
if (originalCallback) originalCallback.apply(this, arguments);
|
||||
updateVisibility();
|
||||
};
|
||||
}
|
||||
}
|
||||
updateVisibility();
|
||||
}, 50);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,942 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import os
|
||||
import math
|
||||
import urllib.request
|
||||
from PIL import Image, ImageOps
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
import comfy.model_management as mm
|
||||
from comfy.utils import load_torch_file, ProgressBar
|
||||
import spandrel
|
||||
|
||||
try:
|
||||
from transformers import AutoModelForImageSegmentation
|
||||
from torchvision import transforms
|
||||
import torchvision.transforms.functional as TF
|
||||
TRANSFORMERS_AVAILABLE = True
|
||||
except ImportError:
|
||||
TRANSFORMERS_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
ONNXRUNTIME_AVAILABLE = True
|
||||
except ImportError:
|
||||
ONNXRUNTIME_AVAILABLE = False
|
||||
|
||||
class QuickMergeNode:
|
||||
def __init__(self):
|
||||
self.cached_bg_model = None
|
||||
self.cached_bg_model_name = None
|
||||
self.cached_upscaler = None
|
||||
self.cached_upscaler_name = None
|
||||
self.onnx_session = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
upscale_models = folder_paths.get_filename_list("upscale_models")
|
||||
auto_dl_name = "RealESRGAN_x2plus.pth (Auto-Download)"
|
||||
if not upscale_models:
|
||||
upscale_models = [auto_dl_name]
|
||||
elif "RealESRGAN_x2plus.pth" not in upscale_models:
|
||||
upscale_models.insert(0, auto_dl_name)
|
||||
|
||||
resize_modes = ["Crop to Fill (Preserve Aspect)", "Stretch to Fit", "Fit Inside (Letterbox)", "Fit Inside (Pad with Blur)"]
|
||||
align_modes = ["center", "top", "bottom", "left", "right"]
|
||||
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"Background": ("IMAGE",),
|
||||
"Foreground (optional)": ("IMAGE",),
|
||||
"mask (optional)": ("MASK",),
|
||||
|
||||
# --- 0. BATCH PROCESSING ---
|
||||
"📁 batch_processing": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "CRITICAL: Ensure all images processed in a folder batch share identical resolutions and aspect ratios to prevent platform layout shifts."
|
||||
}),
|
||||
"📁 foreground_batch": ("BOOLEAN", {"default": False}),
|
||||
"📁 fg_batch_folder": ("STRING", {"default": "📁 Type or paste folder path here..."}),
|
||||
"📁 background_batch": ("BOOLEAN", {"default": False}),
|
||||
"📁 bg_batch_folder": ("STRING", {"default": "📁 Type or paste folder path here..."}),
|
||||
"📁 batch_pairing_mode": (["1-to-1 Match (Sequential)", "Cross-Multiply (All Combinations)"],),
|
||||
"📁 batch_chunk_size": ("INT", {
|
||||
"default": 4, "min": 1, "max": 128, "step": 1,
|
||||
"tooltip": "Max images processed at once through the heavy pipeline. 0 = no limit."
|
||||
}),
|
||||
|
||||
# --- 1. INITIAL RESIZE ---
|
||||
"📏 initial_resize": ("BOOLEAN", {"default": False}),
|
||||
"📏 fg_resize_mode": (resize_modes, {"default": "Crop to Fill (Preserve Aspect)"}),
|
||||
"📏 fg_crop_align": (align_modes, {"default": "center"}),
|
||||
"📏 fg_initial_width": ("INT", {"default": 720, "min": 8, "max": 16384, "step": 8}),
|
||||
"📏 fg_initial_height": ("INT", {"default": 1280, "min": 8, "max": 16384, "step": 8}),
|
||||
"📏 bg_resize_mode": (resize_modes, {"default": "Crop to Fill (Preserve Aspect)"}),
|
||||
"📏 bg_crop_align": (align_modes, {"default": "center"}),
|
||||
"📏 bg_initial_width": ("INT", {"default": 1920, "min": 8, "max": 16384, "step": 8}),
|
||||
"📏 bg_initial_height": ("INT", {"default": 1080, "min": 8, "max": 16384, "step": 8}),
|
||||
|
||||
# --- 2. FOREGROUND MERGE ---
|
||||
"✂️ merge_image": ("BOOLEAN", {"default": False}),
|
||||
"✂️ cutout_model": (["BiRefNet_lite (Fast/Accurate)", "RMBG-2.0 (New/Accurate)", "RMBG-1.4 (Fast)", "BiRefNet (High Quality)", "BiRefNet_HR (4K/8K High Quality)", "None (Use Alpha)"],),
|
||||
"✂️ auto-position (WiP)": ("BOOLEAN", {"default": False}),
|
||||
"✂️ foreground_size": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}),
|
||||
"✂️ foreground_left_right": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
|
||||
"✂️ foreground_up_down": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
|
||||
"✂️ flip_horizontal": ("BOOLEAN", {"default": False}),
|
||||
"✂️ flip_vertical": ("BOOLEAN", {"default": False}),
|
||||
"✂️ rotation_degrees": ("INT", {"default": 0, "min": -360, "max": 360, "step": 1}),
|
||||
"✂️ edge_shrink_grow": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
|
||||
"✂️ edge_softness": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
||||
"✂️ foreground_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"✂️ blend_mode": (["Normal", "Multiply", "Screen", "Overlay", "Soft Light"],),
|
||||
"✂️ blend_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
|
||||
# --- 3. AI SUPERSAMPLING ---
|
||||
"⚡ ai_upscale": ("BOOLEAN", {"default": False}),
|
||||
"⚡ pipeline_order": (["upscale composite", "upscale foreground only", "upscale background only"],),
|
||||
"⚡ choose_upscale_model": (upscale_models,),
|
||||
"⚡ upscale_by": ("FLOAT", {
|
||||
"default": 2.0, "min": 0.25, "max": 32.0, "step": 0.25,
|
||||
}),
|
||||
"⚡ tile_size (0 means off/native)": ("INT", {"default": 512, "min": 0, "max": 8192, "step": 32}),
|
||||
"⚡ post_upscale_edge_soften": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
||||
|
||||
# --- 4. COLORS & FILTERS ---
|
||||
"🎨 colors_and_filters": ("BOOLEAN", {"default": False}),
|
||||
"🎨 filter_target": (["Foreground", "Background", "Both"],),
|
||||
"🎨 filter_type": (["Gaussian Blur", "Auto-Color Match", "Laplacian Sharpen", "Edge Detect", "Sepia", "Invert", "None"],),
|
||||
"🎨 filter_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.05}),
|
||||
"🎨 brightness": ("FLOAT", {"default": 0.0, "min": -2.0, "max": 2.0, "step": 0.05}),
|
||||
"🎨 contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.05}),
|
||||
"🎨 saturation": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.05}),
|
||||
"🎨 filter_size": ("INT", {"default": 3, "min": 1, "max": 127, "step": 2}),
|
||||
"🎨 background_blur": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
||||
|
||||
# --- 5. SHADOWS ---
|
||||
"🌗 shadows": ("BOOLEAN", {"default": False}),
|
||||
"🌗 shadow_color_hex": ("STRING", {"default": "#000000"}),
|
||||
"🌗 shadow_darkness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"🌗 shadow_softness": ("INT", {"default": 15, "min": 1, "max": 255, "step": 1}),
|
||||
"🌗 shadow_position_x": ("INT", {"default": 10, "min": -1000, "max": 1000, "step": 1}),
|
||||
"🌗 shadow_position_y": ("INT", {"default": 10, "min": -1000, "max": 1000, "step": 1}),
|
||||
|
||||
# --- 6. TARGET RESOLUTION ---
|
||||
"🎯 target_resolution": ("BOOLEAN", {"default": False}),
|
||||
"🎯 target_resize_mode": (resize_modes,),
|
||||
"🎯 target_crop_align": (align_modes,),
|
||||
"🎯 target_width": ("INT", {"default": 1920, "min": 64, "max": 16384, "step": 8}),
|
||||
"🎯 target_height": ("INT", {"default": 1080, "min": 64, "max": 16384, "step": 8}),
|
||||
|
||||
# --- GLOBAL ---
|
||||
"🖼️ pixel_smoothing": (["bicubic", "lanczos", "bilinear", "nearest"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "IMAGE", "IMAGE")
|
||||
RETURN_NAMES = ("composited_image", "light_mask", "relight_fg_crop", "relight_bg_crop")
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "image/compositing"
|
||||
|
||||
def color_match(self, fg, bg, mask):
|
||||
mask_sum = mask.sum(dim=(2, 3), keepdim=True) + 1e-6
|
||||
mu_fg = (fg * mask).sum(dim=(2, 3), keepdim=True) / mask_sum
|
||||
std_fg = torch.sqrt(torch.sum((((fg - mu_fg) * mask) ** 2), dim=(2, 3), keepdim=True) / mask_sum) + 1e-6
|
||||
mu_bg = bg.mean(dim=(2, 3), keepdim=True)
|
||||
std_bg = bg.std(dim=(2, 3), keepdim=True) + 1e-6
|
||||
matched_fg = (fg - mu_fg) * (std_bg / std_fg) + mu_bg
|
||||
return torch.clamp(matched_fg, 0.0, 1.0)
|
||||
|
||||
def load_bg_model(self, model_choice, device):
|
||||
if not TRANSFORMERS_AVAILABLE:
|
||||
raise Exception("Please install transformers via ComfyUI Manager")
|
||||
if self.cached_bg_model_name == model_choice:
|
||||
return self.cached_bg_model
|
||||
|
||||
mm.soft_empty_cache()
|
||||
original_getattr = torch.nn.Module.__getattr__
|
||||
def safe_getattr(module_self, name):
|
||||
if name == "all_tied_weights_keys": return {}
|
||||
return original_getattr(module_self, name)
|
||||
|
||||
try:
|
||||
torch.nn.Module.__getattr__ = safe_getattr
|
||||
if model_choice == "RMBG-1.4 (Fast)":
|
||||
model = AutoModelForImageSegmentation.from_pretrained("briaai/RMBG-1.4", trust_remote_code=True)
|
||||
elif model_choice == "RMBG-2.0 (New/Accurate)":
|
||||
model = AutoModelForImageSegmentation.from_pretrained("briaai/RMBG-2.0", trust_remote_code=True)
|
||||
elif model_choice == "BiRefNet_lite (Fast/Accurate)":
|
||||
model = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet_lite", trust_remote_code=True)
|
||||
elif model_choice == "BiRefNet (High Quality)":
|
||||
model = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet", trust_remote_code=True)
|
||||
elif model_choice == "BiRefNet_HR (4K/8K High Quality)":
|
||||
model = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet_HR", trust_remote_code=True)
|
||||
finally:
|
||||
torch.nn.Module.__getattr__ = original_getattr
|
||||
|
||||
model.to(device)
|
||||
model.eval()
|
||||
self.cached_bg_model = model
|
||||
self.cached_bg_model_name = model_choice
|
||||
return model
|
||||
|
||||
def apply_bg_removal(self, image, model_choice, device):
|
||||
model = self.load_bg_model(model_choice, device)
|
||||
transform = transforms.Compose([
|
||||
transforms.Resize((1024, 1024)),
|
||||
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
|
||||
])
|
||||
|
||||
img_t = image.permute(0, 3, 1, 2)
|
||||
if img_t.shape[1] == 4:
|
||||
img_t = img_t[:, :3, :, :]
|
||||
|
||||
model_dtype = next(model.parameters()).dtype
|
||||
b_size = img_t.shape[0]
|
||||
out_masks = []
|
||||
|
||||
for i in range(b_size):
|
||||
chunk = img_t[i:i+1].to(device)
|
||||
input_tensor = transform(chunk).to(dtype=model_dtype)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = model(input_tensor)
|
||||
def get_all_tensors(obj):
|
||||
if isinstance(obj, torch.Tensor): return [obj]
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
res = []
|
||||
for item in obj: res.extend(get_all_tensors(item))
|
||||
return res
|
||||
elif isinstance(obj, dict):
|
||||
res = []
|
||||
for val in obj.values(): res.extend(get_all_tensors(val))
|
||||
return res
|
||||
return []
|
||||
|
||||
all_tensors = get_all_tensors(outputs)
|
||||
preds_tensor = all_tensors[0] if "RMBG" in model_choice else all_tensors[-1]
|
||||
preds_tensor = preds_tensor.float().sigmoid()
|
||||
preds_tensor = (preds_tensor - preds_tensor.min()) / (preds_tensor.max() - preds_tensor.min() + 1e-8)
|
||||
preds_tensor = torch.where(preds_tensor < 0.05, torch.zeros_like(preds_tensor), preds_tensor)
|
||||
|
||||
mask = F.interpolate(preds_tensor, size=(image.shape[1], image.shape[2]), mode='bilinear', align_corners=False)
|
||||
if mask.shape[1] > 1: mask = mask[:, 0:1, :, :]
|
||||
out_masks.append(mask.cpu())
|
||||
mm.soft_empty_cache()
|
||||
|
||||
return torch.cat(out_masks, dim=0)
|
||||
|
||||
def tiled_upscale(self, model, tensor, tile_size=512, overlap=32, pbar=None, start_pct=0, end_pct=100):
|
||||
b, c, h, w = tensor.shape
|
||||
device = tensor.device
|
||||
is_mask = (c == 1)
|
||||
if is_mask:
|
||||
tensor = tensor.repeat(1, 3, 1, 1)
|
||||
|
||||
with torch.no_grad():
|
||||
dummy = torch.zeros((1, 3, 16, 16), device=device)
|
||||
scale = model(dummy).shape[-1] // 16
|
||||
|
||||
if tile_size == 0:
|
||||
with torch.no_grad():
|
||||
result = model(tensor)
|
||||
if is_mask:
|
||||
result = result.mean(dim=1, keepdim=True)
|
||||
if pbar:
|
||||
pbar.update_absolute(end_pct)
|
||||
return result
|
||||
|
||||
out_h, out_w = h * scale, w * scale
|
||||
output = torch.zeros((b, 3, out_h, out_w), device=device)
|
||||
weights = torch.zeros((b, 1, out_h, out_w), device=device)
|
||||
stride = max(1, tile_size - overlap)
|
||||
|
||||
y_range = list(range(0, h, stride))
|
||||
x_range = list(range(0, w, stride))
|
||||
total_tiles = max(1, len(y_range) * len(x_range))
|
||||
tiles_done = 0
|
||||
|
||||
for y in y_range:
|
||||
for x in x_range:
|
||||
y1 = max(0, min(y, h - tile_size)) if h > tile_size else 0
|
||||
x1 = max(0, min(x, w - tile_size)) if w > tile_size else 0
|
||||
y2 = min(y1 + tile_size, h)
|
||||
x2 = min(x1 + tile_size, w)
|
||||
|
||||
tile = tensor[:, :, y1:y2, x1:x2]
|
||||
tile_h, tile_w = tile.shape[2], tile.shape[3]
|
||||
|
||||
gy = torch.linspace(-1.0, 1.0, tile_h, device=device)
|
||||
gx = torch.linspace(-1.0, 1.0, tile_w, device=device)
|
||||
yy, xx = torch.meshgrid(gy, gx, indexing='ij')
|
||||
g_map = torch.exp(-(xx ** 2 + yy ** 2) / (2 * 0.5 ** 2))
|
||||
g_map = g_map.unsqueeze(0).unsqueeze(0)
|
||||
|
||||
with torch.no_grad():
|
||||
up_tile = model(tile)
|
||||
|
||||
up_th, up_tw = up_tile.shape[2], up_tile.shape[3]
|
||||
g_map_up = F.interpolate(g_map, size=(up_th, up_tw), mode='bilinear', align_corners=False)
|
||||
|
||||
out_y1, out_x1 = y1 * scale, x1 * scale
|
||||
out_y2, out_x2 = out_y1 + up_th, out_x1 + up_tw
|
||||
|
||||
output[:, :, out_y1:out_y2, out_x1:out_x2] += up_tile * g_map_up
|
||||
weights[:, :, out_y1:out_y2, out_x1:out_x2] += g_map_up
|
||||
del tile, up_tile, g_map, g_map_up
|
||||
|
||||
tiles_done += 1
|
||||
if pbar:
|
||||
pbar.update_absolute(int(start_pct + (tiles_done / total_tiles) * (end_pct - start_pct)))
|
||||
|
||||
final_out = output / weights.clamp(min=1e-6)
|
||||
if is_mask:
|
||||
final_out = final_out.mean(dim=1, keepdim=True)
|
||||
return final_out
|
||||
|
||||
def process_alpha(self, mask, dilate_erode, feather):
|
||||
if dilate_erode > 0:
|
||||
mask = F.max_pool2d(mask, kernel_size=dilate_erode*2+1, stride=1, padding=dilate_erode)
|
||||
elif dilate_erode < 0:
|
||||
erode_val = abs(dilate_erode)
|
||||
mask = -F.max_pool2d(-mask, kernel_size=erode_val*2+1, stride=1, padding=erode_val)
|
||||
if feather > 0:
|
||||
kernel_size = feather * 2 + 1
|
||||
sigma = 0.3 * ((kernel_size - 1) * 0.5 - 1) + 0.8
|
||||
x = torch.arange(kernel_size, dtype=torch.float32, device=mask.device) - kernel_size // 2
|
||||
x = x.expand(kernel_size, kernel_size)
|
||||
y = x.t()
|
||||
kernel = torch.exp(-(x**2 + y**2) / (2 * sigma**2))
|
||||
kernel = kernel / kernel.sum()
|
||||
kernel = kernel.view(1, 1, kernel_size, kernel_size)
|
||||
mask = F.conv2d(mask, kernel, padding=feather)
|
||||
return torch.clamp(mask, 0.0, 1.0)
|
||||
|
||||
def generate_gaussian_blur(self, t_img, blur_amount, device):
|
||||
k_size = blur_amount if blur_amount % 2 != 0 else blur_amount + 1
|
||||
sigma = max(0.3 * ((k_size - 1) * 0.5 - 1) + 0.8, 0.1)
|
||||
coords = torch.arange(k_size, dtype=torch.float32, device=device) - k_size // 2
|
||||
x_g = coords.expand(k_size, k_size)
|
||||
y_g = x_g.t()
|
||||
g_kernel = torch.exp(-(x_g**2 + y_g**2) / (2 * sigma**2))
|
||||
g_kernel = (g_kernel / g_kernel.sum()).view(1, 1, k_size, k_size).expand(t_img.shape[1], 1, k_size, k_size)
|
||||
return F.conv2d(t_img, g_kernel, padding=k_size//2, groups=t_img.shape[1])
|
||||
|
||||
# --- RE-INSTATED METHODS: RESIZE AND GRADIENT ENGINE SLOTS ---
|
||||
def generate_smart_light_gradient(self, mask_tensor, target_h, target_w, device):
|
||||
if mask_tensor is None:
|
||||
return torch.zeros((1, target_h, target_w), dtype=torch.float32).cpu()
|
||||
|
||||
b_size = mask_tensor.shape[0]
|
||||
tiny_size = 128
|
||||
out_masks = []
|
||||
for i in range(b_size):
|
||||
m_tiny = F.interpolate(mask_tensor[i:i+1].float().to(device), size=(tiny_size, tiny_size), mode='area')
|
||||
m_blurred = self.generate_gaussian_blur(m_tiny, 31, device)
|
||||
m_final = F.interpolate(m_blurred, size=(target_h, target_w), mode='bicubic', align_corners=False)
|
||||
out_masks.append(torch.clamp(m_final, 0.0, 1.0).squeeze(1).cpu())
|
||||
return torch.cat(out_masks, dim=0)
|
||||
|
||||
def resize_tensor(self, tensor, target_w, target_h, mode, align, interpolation):
|
||||
b, c, h, w = tensor.shape
|
||||
target_ratio = target_w / target_h
|
||||
tensor_ratio = w / h
|
||||
|
||||
if mode == "Crop to Fill (Preserve Aspect)":
|
||||
if abs(tensor_ratio - target_ratio) > 0.001:
|
||||
if tensor_ratio > target_ratio:
|
||||
crop_w = int(h * target_ratio)
|
||||
if align == "left": start_x = 0
|
||||
elif align == "right": start_x = w - crop_w
|
||||
else: start_x = (w - crop_w) // 2
|
||||
tensor = tensor[:, :, :, start_x:start_x + crop_w]
|
||||
else:
|
||||
crop_h = int(w / target_ratio)
|
||||
if align == "top": start_y = 0
|
||||
elif align == "bottom": start_y = h - crop_h
|
||||
else: start_y = (h - crop_h) // 2
|
||||
tensor = tensor[:, :, start_y:start_y + crop_h, :]
|
||||
return F.interpolate(tensor, size=(target_h, target_w), mode=interpolation, align_corners=False)
|
||||
|
||||
elif mode == "Stretch to Fit":
|
||||
return F.interpolate(tensor, size=(target_h, target_w), mode=interpolation, align_corners=False)
|
||||
|
||||
elif mode == "Fit Inside (Letterbox)":
|
||||
if tensor_ratio > target_ratio:
|
||||
new_w = target_w
|
||||
new_h = int(target_w / tensor_ratio)
|
||||
else:
|
||||
new_h = target_h
|
||||
new_w = int(target_h * tensor_ratio)
|
||||
tensor_scaled = F.interpolate(tensor, size=(max(1, new_h), max(1, new_w)), mode=interpolation, align_corners=False)
|
||||
padded = torch.zeros((b, c, target_h, target_w), device=tensor.device)
|
||||
start_y = (target_h - new_h) // 2
|
||||
start_x = (target_w - new_w) // 2
|
||||
padded[:, :, start_y:start_y+new_h, start_x:start_x+new_w] = tensor_scaled
|
||||
return padded
|
||||
|
||||
elif mode == "Fit Inside (Pad with Blur)":
|
||||
bg_fill = F.interpolate(tensor, size=(target_h, target_w), mode=interpolation, align_corners=False)
|
||||
bg_blur = self.generate_gaussian_blur(bg_fill.to(mm.get_torch_device()), 51, mm.get_torch_device()).cpu() if tensor.device.type == 'cpu' else self.generate_gaussian_blur(bg_fill, 51, tensor.device)
|
||||
if tensor_ratio > target_ratio:
|
||||
new_w = target_w
|
||||
new_h = int(target_w / tensor_ratio)
|
||||
else:
|
||||
new_h = target_h
|
||||
new_w = int(target_h * tensor_ratio)
|
||||
tensor_scaled = F.interpolate(tensor, size=(max(1, new_h), max(1, new_w)), mode=interpolation, align_corners=False)
|
||||
start_y = (target_h - new_h) // 2
|
||||
start_x = (target_w - new_w) // 2
|
||||
bg_blur[:, :, start_y:start_y+new_h, start_x:start_x+new_w] = tensor_scaled
|
||||
return bg_blur
|
||||
|
||||
def load_folder_images(self, folder_path, device, is_active):
|
||||
if not is_active or not os.path.isdir(folder_path):
|
||||
return None
|
||||
|
||||
valid_exts = ['.jpg', '.jpeg', '.png', '.webp']
|
||||
processed_tensors = []
|
||||
first_w, first_h = None, None
|
||||
|
||||
for file in sorted(os.listdir(folder_path)):
|
||||
if any(file.lower().endswith(ext) for ext in valid_exts):
|
||||
img_path = os.path.join(folder_path, file)
|
||||
try:
|
||||
i = Image.open(img_path)
|
||||
i = ImageOps.exif_transpose(i)
|
||||
|
||||
i = i.convert("RGBA")
|
||||
img_arr = np.array(i).astype(np.float32) / 255.0
|
||||
|
||||
t = torch.from_numpy(img_arr).unsqueeze(0).cpu()
|
||||
t_perm = t.permute(0, 3, 1, 2)
|
||||
|
||||
if first_w is None:
|
||||
first_w, first_h = t_perm.shape[3], t_perm.shape[2]
|
||||
elif t_perm.shape[3] != first_w or t_perm.shape[2] != first_h:
|
||||
t_perm = self.resize_tensor(t_perm, first_w, first_h, "Fit Inside (Letterbox)", "center", "bicubic")
|
||||
|
||||
processed_tensors.append(t_perm.permute(0, 2, 3, 1))
|
||||
except Exception as e:
|
||||
print(f"Quick Merge Image Pack Alignment Failed: {file} - {e}")
|
||||
|
||||
if not processed_tensors:
|
||||
return None
|
||||
|
||||
return torch.cat(processed_tensors, dim=0)
|
||||
|
||||
def process(self, **kwargs):
|
||||
device = mm.get_torch_device()
|
||||
|
||||
alpha = None
|
||||
fg = None
|
||||
b_size = 1
|
||||
effective_chunk = 1
|
||||
num_chunks = 1
|
||||
multi_chunk = False
|
||||
pre_transform_bg_h = 1080
|
||||
pre_transform_bg_w = 1920
|
||||
pbar = ProgressBar(100)
|
||||
|
||||
# --- MAP SETTINGS BLOCK CONTEXT ---
|
||||
batch_processing = kwargs.get("📁 batch_processing", False)
|
||||
foreground_batch = kwargs.get("📁 foreground_batch", False)
|
||||
background_batch = kwargs.get("📁 background_batch", False)
|
||||
fg_batch_folder = kwargs.get("📁 fg_batch_folder", "")
|
||||
bg_batch_folder = kwargs.get("📁 bg_batch_folder", "")
|
||||
batch_pairing_mode = kwargs.get("📁 batch_pairing_mode", "1-to-1 Match (Sequential)")
|
||||
batch_chunk_size = kwargs.get("📁 batch_chunk_size", 4)
|
||||
|
||||
initial_resize = kwargs.get("📏 initial_resize", False)
|
||||
fg_resize_mode = kwargs.get("📏 fg_resize_mode", "Crop to Fill (Preserve Aspect)")
|
||||
fg_crop_align = kwargs.get("📏 fg_crop_align", "center")
|
||||
fg_initial_width = kwargs.get("📏 fg_initial_width", 720)
|
||||
fg_initial_height = kwargs.get("📏 fg_initial_height", 1280)
|
||||
bg_resize_mode = kwargs.get("📏 bg_resize_mode", "Crop to Fill (Preserve Aspect)")
|
||||
bg_crop_align = kwargs.get("📏 bg_crop_align", "center")
|
||||
bg_initial_width = kwargs.get("📏 bg_initial_width", 1920)
|
||||
bg_initial_height = kwargs.get("📏 bg_initial_height", 1080)
|
||||
|
||||
cutout_model = kwargs.get("✂️ cutout_model", "BiRefNet_lite (Fast/Accurate)")
|
||||
foreground_size = kwargs.get("✂️ foreground_size", 1.0)
|
||||
foreground_left_right = kwargs.get("✂️ foreground_left_right", 0)
|
||||
foreground_up_down = kwargs.get("✂️ foreground_up_down", 0)
|
||||
edge_shrink_grow = kwargs.get("✂️ edge_shrink_grow", 0)
|
||||
edge_softness = kwargs.get("✂️ edge_softness", 0)
|
||||
flip_horizontal = kwargs.get("✂️ flip_horizontal", False)
|
||||
flip_vertical = kwargs.get("✂️ flip_vertical", False)
|
||||
rotation_degrees = kwargs.get("✂️ rotation_degrees", 0)
|
||||
foreground_opacity = kwargs.get("✂️ foreground_opacity", 1.0)
|
||||
blend_mode = kwargs.get("✂️ blend_mode", "Normal")
|
||||
blend_strength = kwargs.get("✂️ blend_strength", 1.0)
|
||||
|
||||
auto_position = False
|
||||
|
||||
ai_upscale = kwargs.get("⚡ ai_upscale", False)
|
||||
pipeline_order = kwargs.get("⚡ pipeline_order", "upscale composite")
|
||||
ai_upscale_model = kwargs.get("⚡ choose_upscale_model", "None Found")
|
||||
upscale_by = kwargs.get("⚡ upscale_by", 2.0)
|
||||
ai_upscale_tile_size = kwargs.get("⚡ tile_size (0 means off/native)", 512)
|
||||
post_upscale_edge_soften = kwargs.get("⚡ post_upscale_edge_soften", 0)
|
||||
|
||||
colors_and_filters = kwargs.get("🎨 colors_and_filters", False)
|
||||
filter_target = kwargs.get("🎨 filter_target", "Foreground")
|
||||
color_and_filters = kwargs.get("🎨 filter_type", "Auto-Color Match")
|
||||
filter_strength = kwargs.get("🎨 filter_strength", 1.0)
|
||||
filter_size = kwargs.get("🎨 filter_size", 3)
|
||||
brightness = kwargs.get("🎨 brightness", 0.0)
|
||||
contrast = kwargs.get("🎨 contrast", 1.0)
|
||||
saturation = kwargs.get("🎨 saturation", 1.0)
|
||||
background_blur = kwargs.get("🎨 background_blur", 0)
|
||||
|
||||
shadows = kwargs.get("🌗 shadows", False)
|
||||
shadow_color_hex = kwargs.get("🌗 shadow_color_hex", "#000000").lstrip('#')
|
||||
if len(shadow_color_hex) != 6: shadow_color_hex = "000000"
|
||||
shadow_R = int(shadow_color_hex[0:2], 16) / 255.0
|
||||
shadow_G = int(shadow_color_hex[2:4], 16) / 255.0
|
||||
shadow_B = int(shadow_color_hex[4:6], 16) / 255.0
|
||||
shadow_darkness = kwargs.get("🌗 shadow_darkness", 0.5)
|
||||
shadow_softness = kwargs.get("🌗 shadow_softness", 15)
|
||||
shadow_position_x = kwargs.get("🌗 shadow_position_x", 10)
|
||||
shadow_position_y = kwargs.get("🌗 shadow_position_y", 10)
|
||||
|
||||
target_resolution = kwargs.get("🎯 target_resolution", False)
|
||||
target_resize_mode = kwargs.get("🎯 target_resize_mode", "Crop to Fill (Preserve Aspect)")
|
||||
target_crop_align = kwargs.get("🎯 target_crop_align", "center")
|
||||
target_width = kwargs.get("🎯 target_width", 1920)
|
||||
target_height = kwargs.get("🎯 target_height", 1080)
|
||||
|
||||
pixel_smoothing = kwargs.get("🖼️ pixel_smoothing", "bicubic")
|
||||
interpolation = pixel_smoothing if pixel_smoothing != "lanczos" else "bicubic"
|
||||
|
||||
# --- 1. DIRECTORY CONDITIONAL PROCESSING OVERRIDES ---
|
||||
bg_input = kwargs.get("Background", None)
|
||||
fg_input = kwargs.get("Foreground (optional)", None)
|
||||
mask_input = kwargs.get("mask (optional)", None)
|
||||
|
||||
default_folder_text = "📁 Type or paste folder path here..."
|
||||
if fg_batch_folder == default_folder_text: fg_batch_folder = ""
|
||||
if bg_batch_folder == default_folder_text: bg_batch_folder = ""
|
||||
|
||||
if batch_processing:
|
||||
if background_batch and bg_batch_folder and os.path.isdir(bg_batch_folder):
|
||||
bg_input = self.load_folder_images(bg_batch_folder, device, True)
|
||||
if foreground_batch and fg_batch_folder and os.path.isdir(fg_batch_folder):
|
||||
fg_input = self.load_folder_images(fg_batch_folder, device, True)
|
||||
|
||||
has_bg = bg_input is not None
|
||||
has_fg = fg_input is not None
|
||||
has_mask = mask_input is not None
|
||||
|
||||
if not has_bg and not has_fg and not has_mask:
|
||||
empty_img = torch.zeros((1, 64, 64, 3), dtype=torch.float32).cpu()
|
||||
empty_msk = torch.zeros((1, 64, 64), dtype=torch.float32).cpu()
|
||||
return (empty_img, empty_msk, empty_img, empty_img)
|
||||
|
||||
if has_bg:
|
||||
bg = bg_input.clone().detach().cpu().permute(0, 3, 1, 2) if bg_input.shape[3] in [3, 4] else bg_input.clone().detach().cpu()
|
||||
if bg.shape[1] == 4: bg = bg[:, :3, :, :]
|
||||
if has_fg:
|
||||
fg = fg_input.clone().detach().cpu().permute(0, 3, 1, 2) if fg_input.shape[3] in [3, 4] else fg_input.clone().detach().cpu()
|
||||
if has_mask:
|
||||
m_tensor = mask_input.clone().detach().cpu()
|
||||
if len(m_tensor.shape) == 3: m_tensor = m_tensor.unsqueeze(1)
|
||||
|
||||
merge_image = kwargs.get("✂️ merge_image", True)
|
||||
if not has_fg:
|
||||
merge_image = False
|
||||
|
||||
is_real_bg = has_bg
|
||||
if has_bg:
|
||||
b_size = bg.shape[0]
|
||||
elif has_fg:
|
||||
if merge_image:
|
||||
b_size = fg.shape[0]
|
||||
bg = torch.zeros((b_size, 3, fg.shape[2], fg.shape[3]), device='cpu')
|
||||
has_bg = True
|
||||
else:
|
||||
bg = fg.clone()
|
||||
has_bg = True
|
||||
is_real_bg = True
|
||||
b_size = bg.shape[0]
|
||||
elif has_mask:
|
||||
b_size = m_tensor.shape[0]
|
||||
bg = torch.zeros((b_size, 3, m_tensor.shape[2], m_tensor.shape[3]), device='cpu')
|
||||
has_bg = True
|
||||
|
||||
if not merge_image:
|
||||
has_fg = False
|
||||
|
||||
# --- PRE-PROCESS FOREGROUND CUTOUT ENGINE ---
|
||||
if has_fg and merge_image:
|
||||
if cutout_model != "None (Use Alpha)":
|
||||
pass_fg = fg.permute(0, 2, 3, 1)
|
||||
alpha = self.apply_bg_removal(pass_fg, cutout_model, device)
|
||||
del pass_fg
|
||||
if fg.shape[1] == 4: fg = fg[:, :3, :, :]
|
||||
else:
|
||||
if fg.shape[1] == 4:
|
||||
alpha = fg[:, 3:4, :, :]
|
||||
fg = fg[:, :3, :, :]
|
||||
else:
|
||||
alpha = torch.ones((fg.shape[0], 1, fg.shape[2], fg.shape[3]), dtype=torch.float32, device='cpu')
|
||||
|
||||
alpha = self.process_alpha(alpha, edge_shrink_grow, edge_softness)
|
||||
if flip_horizontal:
|
||||
fg, alpha = torch.flip(fg, [3]), torch.flip(alpha, [3])
|
||||
if flip_vertical:
|
||||
fg, alpha = torch.flip(fg, [2]), torch.flip(alpha, [2])
|
||||
if rotation_degrees != 0:
|
||||
fg = TF.rotate(fg, rotation_degrees, interpolation=TF.InterpolationMode.BILINEAR, expand=True)
|
||||
alpha = TF.rotate(alpha, rotation_degrees, interpolation=TF.InterpolationMode.BILINEAR, expand=True)
|
||||
|
||||
# --- COMBINATORIAL PAIRING SYNCS ---
|
||||
if is_real_bg and merge_image and has_fg:
|
||||
b_bg = bg.shape[0]
|
||||
b_fg = fg.shape[0]
|
||||
|
||||
if batch_pairing_mode == "1-to-1 Match (Sequential)":
|
||||
b_size = max(b_bg, b_fg)
|
||||
if b_bg < b_size:
|
||||
repeats_bg = math.ceil(b_size / b_bg)
|
||||
bg = bg.repeat(repeats_bg, 1, 1, 1)[:b_size]
|
||||
if b_fg < b_size:
|
||||
repeats_fg = math.ceil(b_size / b_fg)
|
||||
fg = fg.repeat(repeats_fg, 1, 1, 1)[:b_size]
|
||||
if alpha is not None:
|
||||
alpha = alpha.repeat(repeats_fg, 1, 1, 1)[:b_size]
|
||||
else:
|
||||
b_size = b_bg * b_fg
|
||||
bg = bg.repeat_interleave(b_fg, dim=0)
|
||||
fg = fg.repeat(b_bg, 1, 1, 1)
|
||||
if alpha is not None:
|
||||
alpha = alpha.repeat(b_bg, 1, 1, 1)
|
||||
|
||||
# --- UNCONDITIONAL CHUNK EVALUATION METRICS ---
|
||||
pre_transform_bg_h, pre_transform_bg_w = bg.shape[2], bg.shape[3]
|
||||
effective_chunk = batch_chunk_size if (batch_chunk_size > 0 and b_size > batch_chunk_size) else b_size
|
||||
num_chunks = math.ceil(b_size / effective_chunk)
|
||||
multi_chunk = num_chunks > 1
|
||||
|
||||
if has_mask:
|
||||
if m_tensor.shape[0] != b_size:
|
||||
m_tensor = m_tensor.repeat(math.ceil(b_size / m_tensor.shape[0]), 1, 1, 1)[:b_size]
|
||||
|
||||
if initial_resize:
|
||||
bg = self.resize_tensor(bg, bg_initial_width, bg_initial_height, bg_resize_mode, bg_crop_align, interpolation)
|
||||
if merge_image and has_fg:
|
||||
fg = self.resize_tensor(fg, fg_initial_width, fg_initial_height, fg_resize_mode, fg_crop_align, interpolation)
|
||||
alpha = self.resize_tensor(alpha, fg_initial_width, fg_initial_height, fg_resize_mode, fg_crop_align, interpolation)
|
||||
pbar.update_absolute(5)
|
||||
|
||||
# ---- CLOSURES ----
|
||||
def execute_filters(t_fg, t_alpha, t_bg):
|
||||
if not colors_and_filters: return t_fg
|
||||
if brightness != 0.0 or contrast != 1.0:
|
||||
t_fg = t_fg + brightness
|
||||
t_fg = torch.clamp((t_fg - 0.5) * contrast + 0.5, 0.0, 1.0)
|
||||
if saturation != 1.0:
|
||||
lum = 0.299 * t_fg[:, 0:1] + 0.587 * t_fg[:, 1:2] + 0.114 * t_fg[:, 2:3]
|
||||
t_fg = torch.clamp(torch.lerp(lum.repeat(1, 3, 1, 1), t_fg, saturation), 0.0, 1.0)
|
||||
if color_and_filters == "Laplacian Sharpen":
|
||||
lap_kernel = torch.tensor([[[[0., 1., 0.], [1., -4., 1.], [0., 1., 0.]]]], device=device).expand(t_fg.shape[1], 1, 3, 3)
|
||||
edges = F.conv2d(t_fg, lap_kernel, padding=1, groups=t_fg.shape[1])
|
||||
return torch.clamp(torch.lerp(t_fg, t_fg - (edges * filter_strength), min(filter_strength, 1.0)), 0.0, 1.0)
|
||||
elif color_and_filters == "Gaussian Blur":
|
||||
blur_fg = self.generate_gaussian_blur(t_fg, filter_size, device)
|
||||
return torch.lerp(t_fg, blur_fg, min(filter_strength, 1.0))
|
||||
elif color_and_filters == "Edge Detect":
|
||||
sobel_x = torch.tensor([[[[-1., 0., 1.], [-2., 0., 2.], [-1., 0., 1.]]]], device=device).expand(t_fg.shape[1], 1, 3, 3)
|
||||
sobel_y = torch.tensor([[[[-1., -2., -1.], [0., 0., 0.], [1., 2., 1.]]]], device=device).expand(t_fg.shape[1], 1, 3, 3)
|
||||
edge_x = F.conv2d(t_fg, sobel_x, padding=1, groups=t_fg.shape[1])
|
||||
edge_y = F.conv2d(t_fg, sobel_y, padding=1, groups=t_fg.shape[1])
|
||||
edges = torch.clamp(torch.sqrt(edge_x**2 + edge_y**2), 0.0, 1.0)
|
||||
return torch.lerp(t_fg, edges, min(filter_strength, 1.0))
|
||||
elif color_and_filters == "Sepia":
|
||||
sepia_w = torch.tensor([[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]], device=device)
|
||||
sepia_fg = torch.einsum('b c h w, d c -> b d h w', t_fg, sepia_w)
|
||||
return torch.lerp(t_fg, torch.clamp(sepia_fg, 0.0, 1.0), min(filter_strength, 1.0))
|
||||
elif color_and_filters == "Invert":
|
||||
return torch.lerp(t_fg, 1.0 - t_fg, min(filter_strength, 1.0))
|
||||
elif color_and_filters == "Auto-Color Match":
|
||||
if t_alpha is not None and is_real_bg:
|
||||
matched_fg = self.color_match(t_fg, t_bg, t_alpha)
|
||||
return torch.lerp(t_fg, matched_fg, min(filter_strength, 1.0))
|
||||
return t_fg
|
||||
return t_fg
|
||||
|
||||
def execute_upscale(t_img, t_is_mask=False, start_pct=0, end_pct=100):
|
||||
if not ai_upscale or ai_upscale_model == "None Found":
|
||||
pbar.update_absolute(end_pct)
|
||||
return t_img
|
||||
|
||||
chunk_b, orig_c, orig_h, orig_w = t_img.shape
|
||||
target_out_h = max(8, round(orig_h * upscale_by))
|
||||
target_out_w = max(8, round(orig_w * upscale_by))
|
||||
|
||||
mm.soft_empty_cache()
|
||||
model_name_clean = ai_upscale_model.replace(" (Auto-Download)", "")
|
||||
|
||||
if self.cached_upscaler_name != model_name_clean:
|
||||
model_path = folder_paths.get_full_path("upscale_models", model_name_clean)
|
||||
if not model_path:
|
||||
if model_name_clean == "RealESRGAN_x2plus.pth":
|
||||
dl_dir = os.path.join(folder_paths.models_dir, "upscale_models")
|
||||
os.makedirs(dl_dir, exist_ok=True)
|
||||
model_path = os.path.join(dl_dir, "RealESRGAN_x2plus.pth")
|
||||
try:
|
||||
urllib.request.urlretrieve("https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth", model_path)
|
||||
except Exception:
|
||||
pbar.update_absolute(end_pct)
|
||||
return t_img
|
||||
else:
|
||||
pbar.update_absolute(end_pct)
|
||||
return t_img
|
||||
|
||||
try:
|
||||
sd = load_torch_file(model_path, safe_load=True)
|
||||
self.cached_upscaler = spandrel.ModelLoader().load_from_state_dict(sd).eval().to(device)
|
||||
self.cached_upscaler_name = model_name_clean
|
||||
except Exception:
|
||||
pbar.update_absolute(end_pct)
|
||||
return t_img
|
||||
|
||||
tile = max(64, ai_upscale_tile_size) if ai_upscale_tile_size > 0 else 0
|
||||
|
||||
out_upscaled_slices = []
|
||||
for i in range(chunk_b):
|
||||
frame_slice = t_img[i:i+1].to(device)
|
||||
slice_pct_s = start_pct + (i / chunk_b) * (end_pct - start_pct)
|
||||
slice_pct_e = start_pct + ((i + 1) / chunk_b) * (end_pct - start_pct)
|
||||
|
||||
up_slice = self.tiled_upscale(self.cached_upscaler, frame_slice, tile_size=tile, pbar=pbar, start_pct=slice_pct_s, end_pct=slice_pct_e)
|
||||
if up_slice.shape[2] != target_out_h or up_slice.shape[3] != target_out_w:
|
||||
up_slice = F.interpolate(up_slice, size=(target_out_h, target_out_w), mode=interpolation, align_corners=False)
|
||||
out_upscaled_slices.append(up_slice.cpu())
|
||||
del frame_slice, up_slice
|
||||
mm.soft_empty_cache()
|
||||
|
||||
up_img = torch.cat(out_upscaled_slices, dim=0).to(device)
|
||||
if t_is_mask and post_upscale_edge_soften > 0:
|
||||
up_img = self.process_alpha(up_img, 0, post_upscale_edge_soften)
|
||||
return up_img
|
||||
|
||||
def execute_composite(t_fg, t_alpha, t_bg, canvas_scale_x=1.0, canvas_scale_y=1.0, chunk_offset_start=0):
|
||||
chunk_b = t_bg.shape[0]
|
||||
canvas_h, canvas_w = t_bg.shape[2], t_bg.shape[3]
|
||||
result_img = t_bg.clone()
|
||||
final_alpha = torch.zeros((chunk_b, 1, canvas_h, canvas_w), device=device)
|
||||
relight_fg_out = torch.zeros_like(t_bg)
|
||||
relight_bg_out = t_bg.clone()
|
||||
|
||||
pre_size_fg_w = max(8, int(t_fg.shape[3] * canvas_scale_x))
|
||||
pre_size_fg_h = max(8, int(t_fg.shape[2] * canvas_scale_y))
|
||||
t_fg = F.interpolate(t_fg, size=(pre_size_fg_h, pre_size_fg_w), mode=interpolation, align_corners=False)
|
||||
t_alpha = F.interpolate(t_alpha, size=(pre_size_fg_h, pre_size_fg_w), mode=interpolation, align_corners=False)
|
||||
|
||||
target_fg_w = max(8, int(pre_size_fg_w * foreground_size))
|
||||
target_fg_h = max(8, int(pre_size_fg_h * foreground_size))
|
||||
t_fg = F.interpolate(t_fg, size=(target_fg_h, target_fg_w), mode=interpolation, align_corners=False)
|
||||
t_alpha = F.interpolate(t_alpha, size=(target_fg_h, target_fg_w), mode=interpolation, align_corners=False)
|
||||
|
||||
center_x = (canvas_w // 2) - (target_fg_w // 2)
|
||||
center_y = (canvas_h // 2) - (target_fg_h // 2)
|
||||
|
||||
scaled_lr = int(round(foreground_left_right * canvas_scale_x))
|
||||
scaled_ud = int(round(foreground_up_down * canvas_scale_y))
|
||||
final_x = center_x + scaled_lr
|
||||
final_y = center_y + scaled_ud
|
||||
|
||||
x1, y1 = max(0, final_x), max(0, final_y)
|
||||
x2, y2 = min(canvas_w, final_x + target_fg_w), min(canvas_h, final_y + target_fg_h)
|
||||
fg_x1, fg_y1 = max(0, -final_x), max(0, -final_y)
|
||||
fg_x2, fg_y2 = fg_x1 + (x2 - x1), fg_y1 + (y2 - y1)
|
||||
|
||||
for b in range(chunk_b):
|
||||
if (x2 > x1) and (y2 > y1):
|
||||
crop_fg = t_fg[b:b+1, :, fg_y1:fg_y2, fg_x1:fg_x2]
|
||||
crop_alpha = t_alpha[b:b+1, :, fg_y1:fg_y2, fg_x1:fg_x2]
|
||||
bg_slice = result_img[b:b+1, :, y1:y2, x1:x2]
|
||||
|
||||
relight_fg_out[b:b+1, :, y1:y2, x1:x2] = crop_fg * crop_alpha
|
||||
active_alpha = crop_alpha * foreground_opacity
|
||||
|
||||
if shadows and shadow_darkness > 0.0:
|
||||
s_blur = shadow_softness if shadow_softness % 2 != 0 else shadow_softness + 1
|
||||
full_shadow_mask = F.avg_pool2d(t_alpha[b:b+1] * foreground_opacity, kernel_size=s_blur, stride=1, padding=s_blur // 2)
|
||||
scaled_spx = int(round(shadow_position_x * canvas_scale_x))
|
||||
scaled_spy = int(round(shadow_position_y * canvas_scale_y))
|
||||
sx1, sy1 = max(0, final_x + scaled_spx), max(0, final_y + scaled_spy)
|
||||
sx2, sy2 = min(canvas_w, final_x + target_fg_w + scaled_spx), min(canvas_h, final_y + target_fg_h + scaled_spy)
|
||||
sfg_x1, sfg_y1 = max(0, -(final_x + scaled_spx)), max(0, -(final_y + scaled_spy))
|
||||
sfg_x2, sfg_y2 = sfg_x1 + (sx2 - sx1), sfg_y1 + (sy2 - sy1)
|
||||
if (sx2 > sx1) and (sy2 > sy1):
|
||||
crop_smask = full_shadow_mask[:, :, sfg_y1:sfg_y2, sfg_x1:sfg_x2]
|
||||
s_color = torch.tensor([shadow_R, shadow_G, shadow_B], device=device).view(1, 3, 1, 1)
|
||||
s_color = s_color.expand_as(result_img[b:b+1, :, sy1:sy2, sx1:sx2])
|
||||
result_img[b:b+1, :, sy1:sy2, sx1:sx2] = torch.lerp(
|
||||
result_img[b:b+1, :, sy1:sy2, sx1:sx2], s_color, crop_smask * shadow_darkness)
|
||||
|
||||
if blend_mode == "Multiply":
|
||||
blend_math = bg_slice * crop_fg
|
||||
elif blend_mode == "Screen":
|
||||
blend_math = 1.0 - (1.0 - bg_slice) * (1.0 - crop_fg)
|
||||
elif blend_mode == "Overlay":
|
||||
mask_lt = (bg_slice < 0.5).float()
|
||||
blend_math = mask_lt * (2.0 * bg_slice * crop_fg) + (1.0 - mask_lt) * (1.0 - 2.0 * (1.0 - bg_slice) * (1.0 - crop_fg))
|
||||
elif blend_mode == "Soft Light":
|
||||
blend_math = (1.0 - 2.0 * crop_fg) * (bg_slice ** 2) + 2.0 * crop_fg * bg_slice
|
||||
else:
|
||||
blend_math = crop_fg
|
||||
|
||||
active_fg = torch.lerp(crop_fg, blend_math, blend_strength)
|
||||
result_img[b:b+1, :, y1:y2, x1:x2] = torch.lerp(bg_slice, active_fg, active_alpha)
|
||||
final_alpha[b:b+1, :, y1:y2, x1:x2] = active_alpha
|
||||
|
||||
return result_img, final_alpha, relight_fg_out, relight_bg_out
|
||||
|
||||
# ==========================================
|
||||
# PATH A: BACKGROUND-ONLY BYPASS
|
||||
# ==========================================
|
||||
if not merge_image:
|
||||
out_images = []
|
||||
for c_idx in range(num_chunks):
|
||||
c_start = c_idx * effective_chunk
|
||||
c_end = min(c_start + effective_chunk, b_size)
|
||||
c_bg = bg[c_start:c_end].clone().to(device)
|
||||
|
||||
if colors_and_filters and filter_target in ["Background", "Both"]:
|
||||
c_bg = execute_filters(c_bg, None, c_bg)
|
||||
|
||||
if ai_upscale:
|
||||
pct_s = 50 + int(c_start / b_size * 45)
|
||||
pct_e = 50 + int(c_end / b_size * 45)
|
||||
c_bg = execute_upscale(c_bg, False, start_pct=pct_s, end_pct=pct_e)
|
||||
|
||||
if target_resolution:
|
||||
c_bg = self.resize_tensor(c_bg, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
|
||||
out_images.append(c_bg.cpu() if multi_chunk else c_bg)
|
||||
del c_bg
|
||||
if multi_chunk: mm.soft_empty_cache()
|
||||
|
||||
pbar.update_absolute(100)
|
||||
bg_final = torch.cat(out_images, dim=0)
|
||||
|
||||
final_h, final_w = bg_final.shape[2], bg_final.shape[3]
|
||||
if has_mask:
|
||||
out_light_mask = self.generate_smart_light_gradient(m_tensor, final_h, final_w, device)
|
||||
else:
|
||||
out_light_mask = torch.zeros((b_size, final_h, final_w), dtype=torch.float32).cpu()
|
||||
|
||||
empty_fg_tensor = torch.zeros((b_size, final_h, final_w, 3), dtype=torch.float32).cpu()
|
||||
return (bg_final.permute(0, 2, 3, 1).cpu(), out_light_mask, empty_fg_tensor, bg_final.permute(0, 2, 3, 1).cpu())
|
||||
|
||||
# ==========================================
|
||||
# PATH B: COMPOSITING CHUNK LOOPS
|
||||
# ==========================================
|
||||
all_fi, all_fm, all_rf, all_rb = [], [], [], []
|
||||
|
||||
for c_idx in range(num_chunks):
|
||||
c_start = c_idx * effective_chunk
|
||||
c_end = min(c_start + effective_chunk, b_size)
|
||||
pct_lo = 30 + int( c_idx / num_chunks * 70)
|
||||
pct_hi = 30 + int((c_idx + 1) / num_chunks * 70)
|
||||
|
||||
c_bg = bg[c_start:c_end].clone().to(device)
|
||||
c_fg = fg[c_start:c_end].clone().to(device)
|
||||
c_alpha = alpha[c_start:c_end].clone().to(device)
|
||||
|
||||
if target_resolution and pipeline_order != "upscale composite":
|
||||
c_bg = self.resize_tensor(c_bg, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
if colors_and_filters and background_blur > 0:
|
||||
c_bg = self.generate_gaussian_blur(c_bg, background_blur, device)
|
||||
|
||||
if colors_and_filters and filter_target in ["Background", "Both"]:
|
||||
c_bg = execute_filters(c_bg, None, c_bg)
|
||||
if colors_and_filters and filter_target in ["Foreground", "Both"]:
|
||||
c_fg = execute_filters(c_fg, c_alpha, c_bg)
|
||||
|
||||
_sx = c_bg.shape[3] / pre_transform_bg_w
|
||||
_sy = c_bg.shape[2] / pre_transform_bg_h
|
||||
|
||||
if pipeline_order == "upscale foreground only":
|
||||
mid = pct_lo + (pct_hi - pct_lo) // 3
|
||||
c_fg = execute_upscale(c_fg, False, start_pct=pct_lo, end_pct=mid)
|
||||
c_alpha = execute_upscale(c_alpha, True, start_pct=mid, end_pct=pct_lo + (pct_hi - pct_lo) * 2 // 3)
|
||||
c_fi, c_fm, c_rf, c_rb = execute_composite(c_fg, c_alpha, c_bg, _sx, _sy, c_start)
|
||||
|
||||
elif pipeline_order == "upscale background only":
|
||||
c_bg = execute_upscale(c_bg, False, start_pct=pct_lo, end_pct=pct_lo + (pct_hi - pct_lo) * 2 // 3)
|
||||
_sx = c_bg.shape[3] / pre_transform_bg_w
|
||||
_sy = c_bg.shape[2] / pre_transform_bg_h
|
||||
c_fi, c_fm, c_rf, c_rb = execute_composite(c_fg, c_alpha, c_bg, _sx, _sy, c_start)
|
||||
|
||||
else: # upscale composite
|
||||
c_fi, c_fm, c_rf, c_rb = execute_composite(c_fg, c_alpha, c_bg, _sx, _sy, c_start)
|
||||
mid = pct_lo + (pct_hi - pct_lo) // 2
|
||||
c_fi = execute_upscale(c_fi, False, start_pct=pct_lo, end_pct=mid)
|
||||
c_fm = execute_upscale(c_fm, True, start_pct=mid, end_pct=pct_hi)
|
||||
up_h, up_w = c_fi.shape[2], c_fi.shape[3]
|
||||
c_rf = F.interpolate(c_rf, size=(up_h, up_w), mode=interpolation, align_corners=False)
|
||||
c_rb = F.interpolate(c_rb, size=(up_h, up_w), mode=interpolation, align_corners=False)
|
||||
|
||||
if target_resolution:
|
||||
c_fi = self.resize_tensor(c_fi, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
c_fm = self.resize_tensor(c_fm, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
c_rf = self.resize_tensor(c_rf, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
c_rb = self.resize_tensor(c_rb, target_width, target_height, target_resize_mode, target_crop_align, interpolation)
|
||||
|
||||
all_fi.append(c_fi.cpu() if multi_chunk else c_fi)
|
||||
all_fm.append(c_fm.cpu() if multi_chunk else c_fm)
|
||||
all_rf.append(c_rf.cpu() if multi_chunk else c_rf)
|
||||
all_rb.append(c_rb.cpu() if multi_chunk else c_rb)
|
||||
|
||||
del c_bg, c_fg, c_alpha, c_fi, c_fm, c_rf, c_rb
|
||||
if multi_chunk:
|
||||
mm.soft_empty_cache()
|
||||
|
||||
pbar.update_absolute(100)
|
||||
|
||||
final_img = torch.cat(all_fi, dim=0)
|
||||
final_mask = torch.cat(all_fm, dim=0)
|
||||
relight_fg = torch.cat(all_rf, dim=0)
|
||||
relight_bg = torch.cat(all_rb, dim=0)
|
||||
if multi_chunk:
|
||||
final_img = final_img.to(device)
|
||||
final_mask = final_mask.to(device)
|
||||
relight_fg = relight_fg.to(device)
|
||||
relight_bg = relight_bg.to(device)
|
||||
|
||||
# ==========================================
|
||||
# LIGHT MASK & OPTION A ALPHA GENERATION
|
||||
# ==========================================
|
||||
final_h, final_w = final_img.shape[2], final_img.shape[3]
|
||||
|
||||
if has_mask:
|
||||
m_resized = F.interpolate(m_tensor.float(), size=(final_h, final_w), mode='bilinear', align_corners=False)
|
||||
out_light_mask = self.generate_gaussian_blur(m_resized.to(device), 51, device).squeeze(1).cpu()
|
||||
elif has_fg and not is_real_bg:
|
||||
out_light_mask = final_mask.squeeze(1).cpu()
|
||||
else:
|
||||
out_light_mask = torch.zeros((b_size, final_h, final_w), dtype=torch.float32).cpu()
|
||||
|
||||
out_images = final_img.permute(0, 2, 3, 1).cpu()
|
||||
out_relight_fg = relight_fg.permute(0, 2, 3, 1).cpu()
|
||||
out_relight_bg = relight_bg.permute(0, 2, 3, 1).cpu()
|
||||
|
||||
ui_msg = "[Quick Merge Engine] Auto-Position Disabled (WiP Phase Lock)"
|
||||
|
||||
return {
|
||||
"ui": {"text": [ui_msg]},
|
||||
"result": (out_images, out_light_mask, out_relight_fg, out_relight_bg)
|
||||
}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"QuickMergeNode": QuickMergeNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"QuickMergeNode": "🎨 Quick Merge"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# 🎨 ComfyUI Quick Merge
|
||||
|
||||
A high-performance, modular image layering, blending, and batch execution engine for ComfyUI. Replace complex multi-node canvas workflows with a single, production-grade layer processing hub engineered for heavy datasets and memory safety.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Key Use Cases
|
||||
|
||||
* **E-Commerce Automation:** Seamlessly clip product variants, fix layer placement offsets, and overlay items across variable marketing backgrounds.
|
||||
* **Concept Art Pre-Compositing:** Rapidly pair generated character assets over different landscape sheets to evaluate lighting, shadows, and composition grids.
|
||||
* **Dataset Enhancement:** Batch-apply standalone background image alterations, filters, structural resolution resizing, and deep Gaussian blurs.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 How To Use
|
||||
|
||||
Connect your assets directly to the node's native input sockets:
|
||||
* **Background (Required):** The baseline background image canvas or folder canvas stream.
|
||||
* **Foreground (Optional):** The product cutout, character asset, or foreground target stack.
|
||||
* **Mask (Optional):** Core transparency sheets used to project custom lighting layouts down your tree.
|
||||
|
||||
### 🔄 Folder Sizing & Fallback Behaviors
|
||||
When processing datasets, the node automatically computes matching rules across mismatched input arrays to guarantee execution stability:
|
||||
* **1 Foreground Asset + N Backgrounds:** The node locks onto the single product input wire and automatically cycles it sequentially across your entire background folder.
|
||||
* **N Foreground Assets + 1 Background:** The node continuously stamps your folder of product assets onto a single, stationary background wire frame.
|
||||
* **M Mismatched Quantities:** If `batch_pairing_mode` is set to `Cross-Multiply`, the engine executes a full matrix pass ($N \times M$), pairing every asset variation exactly once without creating uneven duplicates.
|
||||
|
||||
---
|
||||
|
||||
## 💎 Hidden Architecture Protections
|
||||
|
||||
* **⚡ Sequential VRAM Isolation:** Processing heavy segment loaders (like BiRefNet) over massive batches normally triggers OOM (Out of Memory) failures. Quick Merge pins large tensor stacks safely in system RAM, feeds them to the GPU one single frame at a time, and aggressively clears the CUDA cache after every execution step.
|
||||
* **🛡️ Matrix Format Intersector:** Mixing standard 3-channel `.jpg` backdrops with transparent 4-channel `.png` foreground assets breaks typical PyTorch matrix math. The loader instantly intercepts incoming files and normalizes them into uniform RGBA float32 tensors before they reach the compositor core.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Critical Performance Warnings
|
||||
|
||||
* **❗ AI Supersampling VRAM Load:** The integrated AI Upscaling engine (`⚡ ai_upscale`) is computationally heavy. Running high multipliers (`>2.0x`) over high-resolution image batches simultaneously will easily exhaust your graphics memory. Keep your `📁 batch_chunk_size` set to low numbers (e.g., `1` to `4`) to prevent system crashes.
|
||||
* **📐 Background Aspect Ratio Parity Rule:** For studio backgrounds containing a podium, pedestal, or table, **all images in your folder must share identical aspect ratios and resolutions**. If background sizes fluctuate, standard bounding box and letterbox resizers will cause the pedestal coordinates to shift unevenly between frames, causing product layouts to miss their structural targets.
|
||||
* **🛠️ Auto-Position State:** The `auto-position (WiP)` feature is undergoing a structural logic migration and is temporarily locked down. Leave this parameter deactivated (`False`) to ensure stable rendering execution.
|
||||
@@ -0,0 +1,3 @@
|
||||
transformers>=4.38.0
|
||||
torchvision
|
||||
spandrel
|
||||
Reference in New Issue
Block a user