Files
jaidaken f09734b0ee
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 00:56:42 +00:00

2418 lines
97 KiB
Python

import os
import warnings
import torch
from segment_anything import SamPredictor
from comfy_extras.nodes_custom_sampler import Noise_RandomNoise
from collections import namedtuple
import numpy as np
from PIL import ImageOps, Image
import nodes
from server import PromptServer
import comfy
import impact.wildcards as wildcards
import math
import cv2
import time
from comfy import model_management
from impact import utils
from impact import impact_sampling
from concurrent.futures import ThreadPoolExecutor
import inspect
from collections import OrderedDict
import torch.nn.functional as F
import logging
import sys
import importlib
is_sam2_available = importlib.util.find_spec("sam2")
sam2_unavailable_message = f"\n----------------------------------------------------------------------------\n[Impact Pack] The SAM2 functionality is unavailable because the `facebook/sam2` dependency is not installed.\n\nInstallation command:\n{sys.executable} -m pip install git+https://github.com/facebookresearch/sam2\n----------------------------------------------------------------------------\n"
if is_sam2_available:
from sam2.sam2_image_predictor import SAM2ImagePredictor
from sam2.build_sam import build_sam2, build_sam2_video_predictor
else:
logging.warning(sam2_unavailable_message)
try:
from comfy_extras import nodes_differential_diffusion
except Exception:
logging.warning("\n#############################################\n[Impact Pack] ComfyUI is an outdated version.\n#############################################\n")
raise Exception("[Impact Pack] ComfyUI is an outdated version.")
SEG = namedtuple("SEG",
['cropped_image', 'cropped_mask', 'confidence', 'crop_region', 'bbox', 'label', 'control_net_wrapper'],
defaults=[None])
pb_id_cnt = time.time()
preview_bridge_image_id_map = {}
preview_bridge_image_name_map = {}
preview_bridge_cache = {}
preview_bridge_last_mask_cache = {}
current_prompt = None
ADDITIONAL_SCHEDULERS = ['AYS SDXL', 'AYS SD1', 'AYS SVD', 'GITS[coeff=1.2]', 'LTXV[default]', 'OSS FLUX', 'OSS Wan', 'OSS Chroma']
def get_schedulers():
return list(comfy.samplers.SCHEDULER_HANDLERS) + ADDITIONAL_SCHEDULERS
def is_execution_model_version_supported():
try:
import comfy_execution # noqa: F401
return True
except Exception:
return False
def set_previewbridge_image(node_id, file, item):
global pb_id_cnt
if file in preview_bridge_image_name_map:
pb_id = preview_bridge_image_name_map[node_id, file]
if pb_id.startswith(f"${node_id}"):
return pb_id
pb_id = f"${node_id}-{pb_id_cnt}"
preview_bridge_image_id_map[pb_id] = (file, item)
preview_bridge_image_name_map[node_id, file] = (pb_id, item)
if os.path.isfile(file):
i = Image.open(file)
i = ImageOps.exif_transpose(i)
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
preview_bridge_last_mask_cache[node_id] = mask.unsqueeze(0)
pb_id_cnt += 1
return pb_id
def erosion_mask(mask, grow_mask_by):
mask = utils.make_2d_mask(mask)
w = mask.shape[1]
h = mask.shape[0]
device = comfy.model_management.get_torch_device()
mask = mask.clone().to(device)
mask2 = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(w, h), mode="bilinear").to(device)
if grow_mask_by == 0:
mask_erosion = mask2
else:
kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by)).to(device)
padding = math.ceil((grow_mask_by - 1) / 2)
mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask2.round(), kernel_tensor, padding=padding), 0, 1)
return mask_erosion[:, :, :w, :h].round().cpu()
# CREDIT: https://github.com/BlenderNeko/ComfyUI_Noise/blob/afb14757216257b12268c91845eac248727a55e2/nodes.py#L68
# https://discuss.pytorch.org/t/help-regarding-slerp-function-for-generative-model-sampling/32475/3
def slerp(val, low, high):
dims = low.shape
low = low.reshape(dims[0], -1)
high = high.reshape(dims[0], -1)
low_norm = low/torch.norm(low, dim=1, keepdim=True)
high_norm = high/torch.norm(high, dim=1, keepdim=True)
low_norm[low_norm != low_norm] = 0.0
high_norm[high_norm != high_norm] = 0.0
omega = torch.acos((low_norm*high_norm).sum(1))
so = torch.sin(omega)
res = (torch.sin((1.0-val)*omega)/so).unsqueeze(1)*low + (torch.sin(val*omega)/so).unsqueeze(1) * high
return res.reshape(dims)
def mix_noise(from_noise, to_noise, strength, variation_method):
if variation_method == 'slerp':
mixed_noise = slerp(strength, from_noise, to_noise)
else:
# linear
mixed_noise = (1 - strength) * from_noise + strength * to_noise
# NOTE: Since the variance of the Gaussian noise in mixed_noise has changed, it must be corrected through scaling.
scale_factor = math.sqrt((1 - strength) ** 2 + strength ** 2)
mixed_noise /= scale_factor
return mixed_noise
class REGIONAL_PROMPT:
def __init__(self, mask, sampler, variation_seed=0, variation_strength=0.0, variation_method='linear'):
mask = utils.make_2d_mask(mask)
self.mask = mask
self.sampler = sampler
self.mask_erosion = None
self.erosion_factor = None
self.variation_seed = variation_seed
self.variation_strength = variation_strength
self.variation_method = variation_method
def clone_with_sampler(self, sampler):
rp = REGIONAL_PROMPT(self.mask, sampler)
rp.mask_erosion = self.mask_erosion
rp.erosion_factor = self.erosion_factor
rp.variation_seed = self.variation_seed
rp.variation_strength = self.variation_strength
rp.variation_method = self.variation_method
return rp
def get_mask_erosion(self, factor):
if self.mask_erosion is None or self.erosion_factor != factor:
self.mask_erosion = erosion_mask(self.mask, factor)
self.erosion_factor = factor
return self.mask_erosion
def touch_noise(self, noise):
if self.variation_strength > 0.0:
mask = utils.make_3d_mask(self.mask)
mask = utils.resize_mask(mask, (noise.shape[2], noise.shape[3])).unsqueeze(0)
regional_noise = Noise_RandomNoise(self.variation_seed).generate_noise({'samples': noise})
mixed_noise = mix_noise(noise, regional_noise, self.variation_strength, variation_method=self.variation_method)
return (mask == 1).float() * mixed_noise + (mask == 0).float() * noise
return noise
class NO_BBOX_DETECTOR:
pass
class NO_SEGM_DETECTOR:
pass
def create_segmasks(results):
bboxs = results[1]
segms = results[2]
confidence = results[3]
results = []
for i in range(len(segms)):
item = (bboxs[i], segms[i].astype(np.float32), confidence[i])
results.append(item)
return results
def gen_detection_hints_from_mask_area(x, y, mask, threshold, use_negative):
mask = utils.make_2d_mask(mask)
points = []
plabs = []
# minimum sampling step >= 3
y_step = max(3, int(mask.shape[0] / 20))
x_step = max(3, int(mask.shape[1] / 20))
for i in range(0, len(mask), y_step):
for j in range(0, len(mask[i]), x_step):
if mask[i][j] > threshold:
points.append((x + j, y + i))
plabs.append(1)
elif use_negative and mask[i][j] == 0:
points.append((x + j, y + i))
plabs.append(0)
return points, plabs
def gen_negative_hints(w, h, x1, y1, x2, y2):
npoints = []
nplabs = []
# minimum sampling step >= 3
y_step = max(3, int(w / 20))
x_step = max(3, int(h / 20))
for i in range(10, h - 10, y_step):
for j in range(10, w - 10, x_step):
if not (x1 - 10 <= j and j <= x2 + 10 and y1 - 10 <= i and i <= y2 + 10):
npoints.append((j, i))
nplabs.append(0)
return npoints, nplabs
def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max_size, bbox, seed, steps, cfg,
sampler_name,
scheduler, positive, negative, denoise, noise_mask, force_inpaint,
wildcard_opt=None, wildcard_opt_concat_mode=None,
detailer_hook=None,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None,
refiner_negative=None, control_net_wrapper=None, cycle=1,
inpaint_model=False, noise_mask_feather=0, scheduler_func=None,
vae_tiled_encode=False, vae_tiled_decode=False):
if noise_mask is not None:
noise_mask = utils.tensor_gaussian_blur_mask(noise_mask, noise_mask_feather)
noise_mask = noise_mask.squeeze(3)
if noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
model = nodes_differential_diffusion.DifferentialDiffusion().execute(model)[0]
if wildcard_opt is not None and wildcard_opt != "":
model, _, wildcard_positive = wildcards.process_with_loras(wildcard_opt, model, clip)
if wildcard_opt_concat_mode == "concat":
positive = nodes.ConditioningConcat().concat(positive, wildcard_positive)[0]
else:
positive = wildcard_positive
positive = [positive[0].copy()]
if 'pooled_output' in wildcard_positive[0][1]:
positive[0][1]['pooled_output'] = wildcard_positive[0][1]['pooled_output']
elif 'pooled_output' in positive[0][1]:
del positive[0][1]['pooled_output']
h = image.shape[1]
w = image.shape[2]
bbox_h = bbox[3] - bbox[1]
bbox_w = bbox[2] - bbox[0]
# Skip processing if the detected bbox is already larger than the guide_size
if not force_inpaint and bbox_h >= guide_size and bbox_w >= guide_size:
logging.info("Detailer: segment skip (enough big)")
return None, None
if guide_size_for_bbox: # == "bbox"
# Scale up based on the smaller dimension between width and height.
upscale = guide_size / min(bbox_w, bbox_h)
else:
# for cropped_size
upscale = guide_size / min(w, h)
new_w = int(w * upscale)
new_h = int(h * upscale)
# safeguard
if 'aitemplate_keep_loaded' in model.model_options:
max_size = min(4096, max_size)
if new_w > max_size or new_h > max_size:
upscale *= max_size / max(new_w, new_h)
new_w = int(w * upscale)
new_h = int(h * upscale)
if not force_inpaint:
if upscale <= 1.0:
logging.info(f"Detailer: segment skip [determined upscale factor={upscale}]")
return None, None
if new_w == 0 or new_h == 0:
logging.info(f"Detailer: segment skip [zero size={new_w, new_h}]")
return None, None
else:
if upscale <= 1.0 or new_w == 0 or new_h == 0:
logging.info("Detailer: force inpaint")
upscale = 1.0
new_w = w
new_h = h
if detailer_hook is not None:
new_w, new_h = detailer_hook.touch_scaled_size(new_w, new_h)
logging.info(f"Detailer: segment upscale for ({bbox_w, bbox_h}) | crop region {w, h} x {upscale} -> {new_w, new_h}")
# upscale
upscaled_image = utils.tensor_resize(image, new_w, new_h)
if detailer_hook is not None:
upscaled_image = detailer_hook.post_upscale(upscaled_image, noise_mask)
cnet_pils = None
if control_net_wrapper is not None:
positive, negative, cnet_pils = control_net_wrapper.apply(positive, negative, upscaled_image, noise_mask)
model, cnet_pils2 = control_net_wrapper.doit_ipadapter(model)
cnet_pils.extend(cnet_pils2)
# prepare mask
if detailer_hook is None or not detailer_hook.get_skip_sampling():
if noise_mask is not None and inpaint_model:
imc_encode = nodes.InpaintModelConditioning().encode
if 'noise_mask' in inspect.signature(imc_encode).parameters:
positive, negative, latent_image = imc_encode(positive, negative, upscaled_image, vae, mask=noise_mask, noise_mask=True)
else:
logging.warning("[Impact Pack] ComfyUI is an outdated version.")
positive, negative, latent_image = imc_encode(positive, negative, upscaled_image, vae, noise_mask)
else:
latent_image = utils.to_latent_image(upscaled_image, vae, vae_tiled_encode=vae_tiled_encode)
if noise_mask is not None:
latent_image['noise_mask'] = noise_mask
if detailer_hook is not None:
latent_image = detailer_hook.post_encode(latent_image)
refined_latent = latent_image
sampler_opt=None
if detailer_hook is not None:
sampler_opt = detailer_hook.get_custom_sampler()
# ksampler
for i in range(0, cycle):
if detailer_hook is not None:
if detailer_hook is not None:
detailer_hook.set_steps((i, cycle))
refined_latent = detailer_hook.cycle_latent(refined_latent)
model2, seed2, steps2, cfg2, sampler_name2, scheduler2, positive2, negative2, upscaled_latent2, denoise2 = \
detailer_hook.pre_ksample(model, seed+i, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise)
noise, is_touched = detailer_hook.get_custom_noise(seed+i, torch.zeros(latent_image['samples'].size()), is_touched=False)
if not is_touched:
noise = None
else:
model2, seed2, steps2, cfg2, sampler_name2, scheduler2, positive2, negative2, _, denoise2 = \
model, seed + i, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise
noise = None
refined_latent = impact_sampling.ksampler_wrapper(model2, seed2, steps2, cfg2, sampler_name2, scheduler2, positive2, negative2,
refined_latent, denoise2, refiner_ratio, refiner_model, refiner_clip, refiner_positive, refiner_negative,
noise=noise, scheduler_func=scheduler_func, sampler_opt=sampler_opt)
if detailer_hook is not None:
refined_latent = detailer_hook.pre_decode(refined_latent)
# non-latent downscale - latent downscale cause bad quality
start = time.time()
if vae_tiled_decode:
(refined_image,) = nodes.VAEDecodeTiled().decode(vae, refined_latent, 512) # using default settings
logging.info(f"[Impact Pack] vae decoded (tiled) in {time.time() - start:.1f}s")
else:
try:
refined_image = vae.decode(refined_latent['samples'])
except Exception:
# usually an out-of-memory exception from the decode, so try a tiled approach
logging.warning(f"[Impact Pack] failed after {time.time() - start:.1f}s, doing vae.decode_tiled 64...")
refined_image = vae.decode_tiled(refined_latent["samples"], tile_x=64, tile_y=64, )
logging.info(f"[Impact Pack] vae decoded in {time.time() - start:.1f}s")
else:
# skipped
refined_image = upscaled_image
if detailer_hook is not None:
refined_image = detailer_hook.post_decode(refined_image)
# downscale
# workaround: support WAN as an i2i model
if len(refined_image.shape) == 5:
refined_image = refined_image.squeeze(0)
refined_image = utils.tensor_resize(refined_image, w, h)
# prevent mixing of device
refined_image = refined_image.cpu()
# don't convert to latent - latent break image
# preserving pil is much better
return refined_image, cnet_pils
def enhance_detail_for_animatediff(image_frames, model, clip, vae, guide_size, guide_size_for_bbox, max_size, bbox, seed, steps, cfg,
sampler_name,
scheduler, positive, negative, denoise, noise_mask,
wildcard_opt=None, wildcard_opt_concat_mode=None,
detailer_hook=None,
refiner_ratio=None, refiner_model=None, refiner_clip=None, refiner_positive=None,
refiner_negative=None, control_net_wrapper=None, noise_mask_feather=0, scheduler_func=None):
if noise_mask is not None:
noise_mask = utils.tensor_gaussian_blur_mask(noise_mask, noise_mask_feather)
noise_mask = noise_mask.squeeze(3)
if noise_mask_feather > 0 and 'denoise_mask_function' not in model.model_options:
model = nodes_differential_diffusion.DifferentialDiffusion().execute(model)[0]
if wildcard_opt is not None and wildcard_opt != "":
model, _, wildcard_positive = wildcards.process_with_loras(wildcard_opt, model, clip)
if wildcard_opt_concat_mode == "concat":
positive = nodes.ConditioningConcat().concat(positive, wildcard_positive)[0]
else:
positive = wildcard_positive
h = image_frames.shape[1]
w = image_frames.shape[2]
bbox_h = bbox[3] - bbox[1]
bbox_w = bbox[2] - bbox[0]
# Skip processing if the detected bbox is already larger than the guide_size
if guide_size_for_bbox: # == "bbox"
# Scale up based on the smaller dimension between width and height.
upscale = guide_size / min(bbox_w, bbox_h)
else:
# for cropped_size
upscale = guide_size / min(w, h)
new_w = int(w * upscale)
new_h = int(h * upscale)
# safeguard
if 'aitemplate_keep_loaded' in model.model_options:
max_size = min(4096, max_size)
if new_w > max_size or new_h > max_size:
upscale *= max_size / max(new_w, new_h)
new_w = int(w * upscale)
new_h = int(h * upscale)
if upscale <= 1.0 or new_w == 0 or new_h == 0:
logging.info("Detailer: force inpaint")
upscale = 1.0
new_w = w
new_h = h
if detailer_hook is not None:
new_w, new_h = detailer_hook.touch_scaled_size(new_w, new_h)
logging.info(f"Detailer: segment upscale for ({bbox_w, bbox_h}) | crop region {w, h} x {upscale} -> {new_w, new_h}")
# upscale the mask tensor by a factor of 2 using bilinear interpolation
if isinstance(noise_mask, np.ndarray):
noise_mask = torch.from_numpy(noise_mask)
if len(noise_mask.shape) == 2:
noise_mask = noise_mask.unsqueeze(0)
else: # == 3
noise_mask = noise_mask
upscaled_mask = None
for single_mask in noise_mask:
single_mask = single_mask.unsqueeze(0).unsqueeze(0)
upscaled_single_mask = torch.nn.functional.interpolate(single_mask, size=(new_h, new_w), mode='bilinear', align_corners=False)
upscaled_single_mask = upscaled_single_mask.squeeze(0)
if upscaled_mask is None:
upscaled_mask = upscaled_single_mask
else:
upscaled_mask = torch.cat((upscaled_mask, upscaled_single_mask), dim=0)
latent_frames = None
for image in image_frames:
image = torch.from_numpy(image).unsqueeze(0)
# upscale
upscaled_image = utils.tensor_resize(image, new_w, new_h)
# ksampler
samples = utils.to_latent_image(upscaled_image, vae)['samples']
if latent_frames is None:
latent_frames = samples
else:
latent_frames = torch.concat((latent_frames, samples), dim=0)
cnet_images = None
if control_net_wrapper is not None:
positive, negative, cnet_images = control_net_wrapper.apply(positive, negative, torch.from_numpy(image_frames), noise_mask, use_acn=True)
if len(upscaled_mask) != len(image_frames) and len(upscaled_mask) > 1:
logging.warning(f"[Impact Pack] DetailerForAnimateDiff: The number of the mask frames({len(upscaled_mask)}) and the image frames({len(image_frames)}) are different. Combine the mask frames and apply.")
combined_mask = upscaled_mask[0].to(torch.uint8)
for frame_mask in upscaled_mask[1:]:
combined_mask |= (frame_mask * 255).to(torch.uint8)
combined_mask = (combined_mask/255.0).to(torch.float32)
upscaled_mask = combined_mask.expand(len(image_frames), -1, -1)
upscaled_mask = utils.to_binary_mask(upscaled_mask, 0.1)
latent = {
'noise_mask': upscaled_mask,
'samples': latent_frames
}
sampler_opt=None
if detailer_hook is not None:
sampler_opt = detailer_hook.get_custom_sampler()
if detailer_hook is not None:
latent = detailer_hook.post_encode(latent)
refined_latent = impact_sampling.ksampler_wrapper(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
latent, denoise, refiner_ratio, refiner_model, refiner_clip, refiner_positive, refiner_negative, scheduler_func=scheduler_func, sampler_opt=sampler_opt)
if detailer_hook is not None:
refined_latent = detailer_hook.pre_decode(refined_latent)
refined_image_frames = None
for refined_sample in refined_latent['samples']:
refined_sample = refined_sample.unsqueeze(0)
# non-latent downscale - latent downscale cause bad quality
refined_image = vae.decode(refined_sample)
if refined_image_frames is None:
refined_image_frames = refined_image
else:
refined_image_frames = torch.concat((refined_image_frames, refined_image), dim=0)
if detailer_hook is not None:
refined_image_frames = detailer_hook.post_decode(refined_image_frames)
refined_image_frames = nodes.ImageScale().upscale(image=refined_image_frames, upscale_method='lanczos', width=w, height=h, crop='disabled')[0]
return refined_image_frames, cnet_images
def composite_to(dest_latent, crop_region, src_latent):
x1 = crop_region[0]
y1 = crop_region[1]
# composite to original latent
lc = nodes.LatentComposite()
orig_image = lc.composite(dest_latent, src_latent, x1, y1)
return orig_image[0]
def sam_predict(predictor, points, plabs, bbox, threshold):
point_coords = None if not points else np.array(points)
point_labels = None if not plabs else np.array(plabs)
box = np.array([bbox]) if bbox is not None else None
cur_masks, scores, _ = predictor.predict(point_coords=point_coords, point_labels=point_labels, box=box)
total_masks = []
selected = False
max_score = 0
max_mask = None
for idx in range(len(scores)):
if scores[idx] > max_score:
max_score = scores[idx]
max_mask = cur_masks[idx]
if scores[idx] >= threshold:
selected = True
total_masks.append(cur_masks[idx])
else:
pass
if not selected and max_mask is not None:
total_masks.append(max_mask)
return total_masks
class SAMWrapper:
def __init__(self, model, is_auto_mode, safe_to_gpu=None):
self.model = model
self.safe_to_gpu = safe_to_gpu if safe_to_gpu is not None else SafeToGPU_stub()
self.is_auto_mode = is_auto_mode
def prepare_device(self):
if self.is_auto_mode:
device = comfy.model_management.get_torch_device()
self.safe_to_gpu.to_device(self.model, device=device)
def release_device(self):
if self.is_auto_mode:
self.model.to(device="cpu")
def predict(self, image, points, plabs, bbox, threshold):
predictor = SamPredictor(self.model)
predictor.set_image(image, "RGB")
return sam_predict(predictor, points, plabs, bbox, threshold)
class SAM2Wrapper:
def __init__(self, config, modelname, is_auto_mode, safe_to_gpu=None, device_mode="AUTO"):
self.config = config
self.modelname = modelname
self.image_predictor = None
self.video_predictor = None
self.device_mode = device_mode
self.safe_to_gpu = safe_to_gpu if safe_to_gpu is not None else SafeToGPU_stub()
self.is_auto_mode = is_auto_mode
def prepare_device(self):
pass
def prepare_image_device(self):
if self.is_auto_mode:
device = comfy.model_management.get_torch_device()
self.safe_to_gpu.to_device(self.image_predictor.model, device=device)
def prepare_video_device(self):
if self.is_auto_mode:
device = comfy.model_management.get_torch_device()
self.safe_to_gpu.to_device(self.video_predictor, device=device)
def release_device(self):
if self.is_auto_mode:
if self.image_predictor:
self.image_predictor.model.to(device="cpu")
if self.video_predictor:
self.video_predictor.to(device="cpu")
def predict(self, image, points, plabs, bbox, threshold):
if not is_sam2_available:
raise Exception(sam2_unavailable_message)
if self.image_predictor is None:
self.image_predictor = SAM2ImagePredictor(build_sam2(self.config, self.modelname))
self.prepare_image_device()
self.image_predictor.set_image(image)
return sam_predict(self.image_predictor, points, plabs, bbox, threshold)
def predict_video_segs(self, image_frames, segs):
if not is_sam2_available:
raise Exception(sam2_unavailable_message)
if self.video_predictor is None:
self.video_predictor = build_sam2_video_predictor(self.config, self.modelname)
self.prepare_video_device()
orig_video_height = image_frames.shape[1]
orig_video_width = image_frames.shape[2]
image_frames, padding = utils.resize_with_padding(image_frames, self.video_predictor.image_size, self.video_predictor.image_size)
image_frames = image_frames.permute(0, 3, 1, 2)
inference_state = {}
inference_state["images"] = image_frames
inference_state["num_frames"] = len(image_frames)
inference_state["video_height"] = self.video_predictor.image_size
inference_state["video_width"] = self.video_predictor.image_size
inference_state["offload_video_to_cpu"] = True
inference_state["offload_state_to_cpu"] = self.device_mode == "CPU"
inference_state["device"] = self.video_predictor.device
if inference_state["offload_state_to_cpu"]:
inference_state["storage_device"] = torch.device("cpu")
else:
inference_state["storage_device"] = self.video_predictor.device
inference_state["point_inputs_per_obj"] = {}
inference_state["mask_inputs_per_obj"] = {}
inference_state["cached_features"] = {}
inference_state["constants"] = {}
inference_state["obj_id_to_idx"] = OrderedDict()
inference_state["obj_idx_to_id"] = OrderedDict()
inference_state["obj_ids"] = []
inference_state["output_dict_per_obj"] = {}
inference_state["temp_output_dict_per_obj"] = {}
inference_state["frames_tracked_per_obj"] = {}
self.video_predictor._get_image_feature(inference_state, frame_idx=0, batch_size=1)
temp_masks = {}
for i in range(0, len(segs[1])):
bbox = segs[1][i].bbox
adjusted_bbox = utils.adjust_bbox_after_resize(
bbox,
(orig_video_height, orig_video_width),
(self.video_predictor.image_size, self.video_predictor.image_size),
padding
)
points = [utils.center_of_bbox(adjusted_bbox)]
plabs = [1]
self.video_predictor.add_new_points_or_box(inference_state=inference_state, frame_idx=0, obj_id=i, points=points, labels=plabs, box=adjusted_bbox)
temp_masks[i] = []
for frame_idx, object_ids, masks in self.video_predictor.propagate_in_video(inference_state):
for i in object_ids:
m = masks[i]
m = m.permute(1, 2, 0)
temp_masks[i].append(m)
result = {}
for k, v in temp_masks.items():
m = torch.stack(v, dim=0)
m = utils.remove_padding(m, padding)
result[k] = utils.resize_with_padding(m, orig_video_width, orig_video_height)[0]
return result
class ESAMWrapper:
def __init__(self, model, device):
self.model = model
self.func_inference = nodes.NODE_CLASS_MAPPINGS['Yoloworld_ESAM_Zho']
self.device = device
def prepare_device(self):
pass
def release_device(self):
pass
def predict(self, image, points, plabs, bbox, threshold):
if self.device == 'CPU':
self.device = 'cpu'
else:
self.device = 'cuda'
detected_masks = self.func_inference.inference_sam_with_boxes(image=image, xyxy=[bbox], model=self.model, device=self.device)
return [detected_masks.squeeze(0)]
def make_sam_mask(sam, segs, image, detection_hint, dilation,
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
if not hasattr(sam, 'sam_wrapper') and not isinstance(sam, SAM2Wrapper):
raise Exception("[Impact Pack] Invalid SAMLoader is connected. Make sure 'SAMLoader (Impact)'.\nKnown issue: The ComfyUI-YOLO node overrides the SAMLoader (Impact), making it unusable. You need to uninstall ComfyUI-YOLO.\n\n\n")
if isinstance(sam, SAM2Wrapper):
sam_obj = sam
else:
sam_obj = sam.sam_wrapper
sam_obj.prepare_device()
try:
image = np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
total_masks = []
use_small_negative = mask_hint_use_negative == "Small"
# seg_shape = segs[0]
segs = segs[1]
if detection_hint == "mask-points":
points = []
plabs = []
for i in range(len(segs)):
bbox = segs[i].bbox
center = utils.center_of_bbox(segs[i].bbox)
points.append(center)
# small point is background, big point is foreground
if use_small_negative and bbox[2] - bbox[0] < 10:
plabs.append(0)
else:
plabs.append(1)
detected_masks = sam_obj.predict(image, points, plabs, None, threshold)
total_masks += detected_masks
else:
for i in range(len(segs)):
bbox = segs[i].bbox
center = utils.center_of_bbox(bbox)
x1 = max(bbox[0] - bbox_expansion, 0)
y1 = max(bbox[1] - bbox_expansion, 0)
x2 = min(bbox[2] + bbox_expansion, image.shape[1])
y2 = min(bbox[3] + bbox_expansion, image.shape[0])
dilated_bbox = [x1, y1, x2, y2]
points = []
plabs = []
if detection_hint == "center-1":
points.append(center)
plabs = [1] # 1 = foreground point, 0 = background point
elif detection_hint == "horizontal-2":
gap = (x2 - x1) / 3
points.append((x1 + gap, center[1]))
points.append((x1 + gap * 2, center[1]))
plabs = [1, 1]
elif detection_hint == "vertical-2":
gap = (y2 - y1) / 3
points.append((center[0], y1 + gap))
points.append((center[0], y1 + gap * 2))
plabs = [1, 1]
elif detection_hint == "rect-4":
x_gap = (x2 - x1) / 3
y_gap = (y2 - y1) / 3
points.append((x1 + x_gap, center[1]))
points.append((x1 + x_gap * 2, center[1]))
points.append((center[0], y1 + y_gap))
points.append((center[0], y1 + y_gap * 2))
plabs = [1, 1, 1, 1]
elif detection_hint == "diamond-4":
x_gap = (x2 - x1) / 3
y_gap = (y2 - y1) / 3
points.append((x1 + x_gap, y1 + y_gap))
points.append((x1 + x_gap * 2, y1 + y_gap))
points.append((x1 + x_gap, y1 + y_gap * 2))
points.append((x1 + x_gap * 2, y1 + y_gap * 2))
plabs = [1, 1, 1, 1]
elif detection_hint == "mask-point-bbox":
center = utils.center_of_bbox(segs[i].bbox)
points.append(center)
plabs = [1]
elif detection_hint == "mask-area":
points, plabs = gen_detection_hints_from_mask_area(segs[i].crop_region[0], segs[i].crop_region[1],
segs[i].cropped_mask,
mask_hint_threshold, use_small_negative)
if mask_hint_use_negative == "Outter":
npoints, nplabs = gen_negative_hints(image.shape[0], image.shape[1],
segs[i].crop_region[0], segs[i].crop_region[1],
segs[i].crop_region[2], segs[i].crop_region[3])
points += npoints
plabs += nplabs
detected_masks = sam_obj.predict(image, points, plabs, dilated_bbox, threshold)
total_masks += detected_masks
# merge every collected masks
mask = utils.combine_masks2(total_masks)
finally:
sam_obj.release_device()
if mask is not None:
mask = mask.float()
mask = utils.dilate_mask(mask.cpu().numpy(), dilation)
mask = torch.from_numpy(mask)
else:
size = image.shape[0], image.shape[1]
mask = torch.zeros(size, dtype=torch.float32, device="cpu") # empty mask
mask = utils.make_3d_mask(mask)
return mask
def generate_detection_hints(image, seg, center, detection_hint, dilated_bbox, mask_hint_threshold, use_small_negative,
mask_hint_use_negative):
[x1, y1, x2, y2] = dilated_bbox
points = []
plabs = []
if detection_hint == "center-1":
points.append(center)
plabs = [1] # 1 = foreground point, 0 = background point
elif detection_hint == "horizontal-2":
gap = (x2 - x1) / 3
points.append((x1 + gap, center[1]))
points.append((x1 + gap * 2, center[1]))
plabs = [1, 1]
elif detection_hint == "vertical-2":
gap = (y2 - y1) / 3
points.append((center[0], y1 + gap))
points.append((center[0], y1 + gap * 2))
plabs = [1, 1]
elif detection_hint == "rect-4":
x_gap = (x2 - x1) / 3
y_gap = (y2 - y1) / 3
points.append((x1 + x_gap, center[1]))
points.append((x1 + x_gap * 2, center[1]))
points.append((center[0], y1 + y_gap))
points.append((center[0], y1 + y_gap * 2))
plabs = [1, 1, 1, 1]
elif detection_hint == "diamond-4":
x_gap = (x2 - x1) / 3
y_gap = (y2 - y1) / 3
points.append((x1 + x_gap, y1 + y_gap))
points.append((x1 + x_gap * 2, y1 + y_gap))
points.append((x1 + x_gap, y1 + y_gap * 2))
points.append((x1 + x_gap * 2, y1 + y_gap * 2))
plabs = [1, 1, 1, 1]
elif detection_hint == "mask-point-bbox":
center = utils.center_of_bbox(seg.bbox)
points.append(center)
plabs = [1]
elif detection_hint == "mask-area":
points, plabs = gen_detection_hints_from_mask_area(seg.crop_region[0], seg.crop_region[1],
seg.cropped_mask,
mask_hint_threshold, use_small_negative)
if mask_hint_use_negative == "Outter":
npoints, nplabs = gen_negative_hints(image.shape[0], image.shape[1],
seg.crop_region[0], seg.crop_region[1],
seg.crop_region[2], seg.crop_region[3])
points += npoints
plabs += nplabs
return points, plabs
def convert_and_stack_masks(masks):
if len(masks) == 0:
return None
mask_tensors = []
for mask in masks:
mask_array = np.array(mask, dtype=np.uint8)
mask_tensor = torch.from_numpy(mask_array)
mask_tensors.append(mask_tensor)
stacked_masks = torch.stack(mask_tensors, dim=0)
stacked_masks = stacked_masks.unsqueeze(1)
return stacked_masks
def merge_and_stack_masks(stacked_masks, group_size):
if stacked_masks is None:
return None
num_masks = stacked_masks.size(0)
merged_masks = []
for i in range(0, num_masks, group_size):
subset_masks = stacked_masks[i:i + group_size]
merged_mask = torch.any(subset_masks, dim=0)
merged_masks.append(merged_mask)
if len(merged_masks) > 0:
merged_masks = torch.stack(merged_masks, dim=0)
return merged_masks
def segs_scale_match(segs, target_shape):
h = segs[0][0]
w = segs[0][1]
th = target_shape[1]
tw = target_shape[2]
if (h == th and w == tw) or h == 0 or w == 0:
return segs
rh = th / h
rw = tw / w
new_segs = []
for seg in segs[1]:
cropped_image = seg.cropped_image
cropped_mask = seg.cropped_mask
x1, y1, x2, y2 = seg.crop_region
bx1, by1, bx2, by2 = seg.bbox
crop_region = int(x1*rw), int(y1*rw), int(x2*rh), int(y2*rh)
bbox = int(bx1*rw), int(by1*rw), int(bx2*rh), int(by2*rh)
new_w = crop_region[2] - crop_region[0]
new_h = crop_region[3] - crop_region[1]
if isinstance(cropped_mask, np.ndarray):
cropped_mask = torch.from_numpy(cropped_mask)
if isinstance(cropped_mask, torch.Tensor) and len(cropped_mask.shape) == 3:
cropped_mask = torch.nn.functional.interpolate(cropped_mask.unsqueeze(0), size=(new_h, new_w), mode='bilinear', align_corners=False)
cropped_mask = cropped_mask.squeeze(0)
else:
cropped_mask = torch.nn.functional.interpolate(cropped_mask.unsqueeze(0).unsqueeze(0), size=(new_h, new_w), mode='bilinear', align_corners=False)
cropped_mask = cropped_mask.squeeze(0).squeeze(0).numpy()
if cropped_image is not None:
cropped_image = utils.tensor_resize(cropped_image if isinstance(cropped_image, torch.Tensor) else torch.from_numpy(cropped_image), new_w, new_h)
cropped_image = cropped_image.numpy()
new_seg = SEG(cropped_image, cropped_mask, seg.confidence, crop_region, bbox, seg.label, seg.control_net_wrapper)
new_segs.append(new_seg)
return (th, tw), new_segs
# Used Python's slicing feature. stacked_masks[2::3] means starting from index 2, selecting every third tensor with a step size of 3.
# This allows for quickly obtaining the last tensor of every three tensors in stacked_masks.
def every_three_pick_last(stacked_masks):
selected_masks = stacked_masks[2::3]
return selected_masks
def make_sam_mask_segmented(sam, segs, image, detection_hint, dilation,
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
if not hasattr(sam, 'sam_wrapper'):
raise Exception("[Impact Pack] Invalid SAMLoader is connected. Make sure 'SAMLoader (Impact)'.")
sam_obj = sam.sam_wrapper
sam_obj.prepare_device()
try:
image = np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
total_masks = []
use_small_negative = mask_hint_use_negative == "Small"
# seg_shape = segs[0]
segs = segs[1]
if detection_hint == "mask-points":
points = []
plabs = []
for i in range(len(segs)):
bbox = segs[i].bbox
center = utils.center_of_bbox(bbox)
points.append(center)
# small point is background, big point is foreground
if use_small_negative and bbox[2] - bbox[0] < 10:
plabs.append(0)
else:
plabs.append(1)
detected_masks = sam_obj.predict(image, points, plabs, None, threshold)
total_masks += detected_masks
else:
for i in range(len(segs)):
bbox = segs[i].bbox
center = utils.center_of_bbox(bbox)
x1 = max(bbox[0] - bbox_expansion, 0)
y1 = max(bbox[1] - bbox_expansion, 0)
x2 = min(bbox[2] + bbox_expansion, image.shape[1])
y2 = min(bbox[3] + bbox_expansion, image.shape[0])
dilated_bbox = [x1, y1, x2, y2]
points, plabs = generate_detection_hints(image, segs[i], center, detection_hint, dilated_bbox,
mask_hint_threshold, use_small_negative,
mask_hint_use_negative)
detected_masks = sam_obj.predict(image, points, plabs, dilated_bbox, threshold)
total_masks += detected_masks
# merge every collected masks
mask = utils.combine_masks2(total_masks)
finally:
sam_obj.release_device()
mask_working_device = torch.device("cpu")
if mask is not None:
mask = mask.float()
mask = utils.dilate_mask(mask.cpu().numpy(), dilation)
mask = torch.from_numpy(mask)
mask = mask.to(device=mask_working_device)
else:
# Extracting batch, height and width
height, width, _ = image.shape
mask = torch.zeros(
(height, width), dtype=torch.float32, device=mask_working_device
) # empty mask
stacked_masks = convert_and_stack_masks(total_masks)
return (mask, merge_and_stack_masks(stacked_masks, group_size=3))
# return every_three_pick_last(stacked_masks)
def segs_bitwise_and_mask(segs, mask):
mask = utils.make_2d_mask(mask)
if mask is None:
logging.warning("[SegsBitwiseAndMask] Cannot operate: MASK is empty.")
return ([],)
items = []
mask = (mask.cpu().numpy() * 255).astype(np.uint8)
for seg in segs[1]:
cropped_mask = (seg.cropped_mask * 255).astype(np.uint8)
crop_region = seg.crop_region
cropped_mask2 = mask[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]]
new_mask = np.bitwise_and(cropped_mask.astype(np.uint8), cropped_mask2)
new_mask = new_mask.astype(np.float32) / 255.0
item = SEG(seg.cropped_image, new_mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, None)
items.append(item)
return segs[0], items
def segs_bitwise_subtract_mask(segs, mask):
mask = utils.make_2d_mask(mask)
if mask is None:
logging.warning("[SegsBitwiseSubtractMask] Cannot operate: MASK is empty.")
return ([],)
items = []
mask = (mask.cpu().numpy() * 255).astype(np.uint8)
for seg in segs[1]:
cropped_mask = (seg.cropped_mask * 255).astype(np.uint8)
crop_region = seg.crop_region
cropped_mask2 = mask[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]]
new_mask = cv2.subtract(cropped_mask.astype(np.uint8), cropped_mask2)
new_mask = new_mask.astype(np.float32) / 255.0
item = SEG(seg.cropped_image, new_mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, None)
items.append(item)
return segs[0], items
def apply_mask_to_each_seg(segs, masks):
if masks is None:
logging.warning("[SegsBitwiseAndMask] Cannot operate: MASK is empty.")
return (segs[0], [],)
items = []
masks = masks.squeeze(1)
for seg, mask in zip(segs[1], masks):
cropped_mask = (seg.cropped_mask * 255).astype(np.uint8)
crop_region = seg.crop_region
cropped_mask2 = (mask.cpu().numpy() * 255).astype(np.uint8)
cropped_mask2 = cropped_mask2[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]]
new_mask = np.bitwise_and(cropped_mask.astype(np.uint8), cropped_mask2)
new_mask = new_mask.astype(np.float32) / 255.0
item = SEG(seg.cropped_image, new_mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, None)
items.append(item)
return segs[0], items
def dilate_segs(segs, factor):
if factor == 0:
return segs
new_segs = []
for seg in segs[1]:
new_mask = utils.dilate_mask(seg.cropped_mask, factor)
new_seg = SEG(seg.cropped_image, new_mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, seg.control_net_wrapper)
new_segs.append(new_seg)
return (segs[0], new_segs)
class ONNXDetector:
onnx_model = None
def __init__(self, onnx_model):
self.onnx_model = onnx_model
def detect(self, image, threshold, dilation, crop_factor, drop_size=1, detailer_hook=None):
drop_size = max(drop_size, 1)
try:
import impact.impact_onnx as onnx
h = image.shape[1]
w = image.shape[2]
labels, scores, boxes = onnx.onnx_inference(image, self.onnx_model)
# collect feasible item
result = []
for i in range(len(labels)):
if scores[i] > threshold:
item_bbox = boxes[i]
x1, y1, x2, y2 = item_bbox
if x2 - x1 > drop_size and y2 - y1 > drop_size: # minimum dimension must be (2,2) to avoid squeeze issue
crop_region = utils.make_crop_region(w, h, item_bbox, crop_factor)
if detailer_hook is not None:
crop_region = item_bbox.post_crop_region(w, h, item_bbox, crop_region)
crop_x1, crop_y1, crop_x2, crop_y2, = crop_region
# prepare cropped mask
cropped_mask = np.zeros((crop_y2 - crop_y1, crop_x2 - crop_x1))
cropped_mask[y1 - crop_y1:y2 - crop_y1, x1 - crop_x1:x2 - crop_x1] = 1
cropped_mask = utils.dilate_mask(cropped_mask, dilation)
# make items. just convert the integer label to a string
item = SEG(None, cropped_mask, scores[i], crop_region, item_bbox, str(labels[i]), None)
result.append(item)
shape = h, w
segs = shape, result
if detailer_hook is not None and hasattr(detailer_hook, "post_detection"):
segs = detailer_hook.post_detection(segs)
return segs
except Exception as e:
logging.error(f"ONNXDetector: unable to execute.\n{e}")
def detect_combined(self, image, threshold, dilation):
return segs_to_combined_mask(self.detect(image, threshold, dilation, 1))
def setAux(self, x):
pass
def batch_mask_to_segs(mask, combined, crop_factor, bbox_fill, drop_size=1, label='A', crop_min_size=None, detailer_hook=None):
combined_mask = mask.max(dim=0).values
segs = mask_to_segs(combined_mask, combined, crop_factor, bbox_fill, drop_size, label, crop_min_size, detailer_hook)
new_segs = []
for seg in segs[1]:
x1, y1, x2, y2 = seg.crop_region
cropped_mask = mask[:, y1:y2, x1:x2]
item = SEG(None, cropped_mask, 1.0, seg.crop_region, seg.bbox, label, None)
new_segs.append(item)
return segs[0], new_segs
def mask_to_segs(mask, combined, crop_factor, bbox_fill, drop_size=1, label='A', crop_min_size=None, detailer_hook=None, is_contour=True):
drop_size = max(drop_size, 1)
if mask is None:
logging.info("[mask_to_segs] Cannot operate: MASK is empty.")
return ([],)
if isinstance(mask, np.ndarray):
pass # `mask` is already a NumPy array
else:
try:
mask = mask.numpy()
except AttributeError:
logging.info("[mask_to_segs] Cannot operate: MASK is not a NumPy array or Tensor.")
return ([],)
if mask is None:
logging.info("[mask_to_segs] Cannot operate: MASK is empty.")
return ([],)
result = []
if len(mask.shape) == 2:
mask = np.expand_dims(mask, axis=0)
for i in range(mask.shape[0]):
mask_i = mask[i]
if combined:
indices = np.nonzero(mask_i)
if len(indices[0]) > 0 and len(indices[1]) > 0:
bbox = (
np.min(indices[1]),
np.min(indices[0]),
np.max(indices[1]),
np.max(indices[0]),
)
crop_region = utils.make_crop_region(
mask_i.shape[1], mask_i.shape[0], bbox, crop_factor
)
x1, y1, x2, y2 = crop_region
if detailer_hook is not None:
crop_region = detailer_hook.post_crop_region(mask_i.shape[1], mask_i.shape[0], bbox, crop_region)
if x2 - x1 > 0 and y2 - y1 > 0:
cropped_mask = mask_i[y1:y2, x1:x2]
if bbox_fill:
bx1, by1, bx2, by2 = bbox
cropped_mask = cropped_mask.copy()
cropped_mask[by1:by2, bx1:bx2] = 1.0
if cropped_mask is not None:
item = SEG(None, cropped_mask, 1.0, crop_region, bbox, label, None)
result.append(item)
else:
mask_i_uint8 = (mask_i * 255.0).astype(np.uint8)
contours, ctree = cv2.findContours(mask_i_uint8, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for j, contour in enumerate(contours):
hierarchy = ctree[0][j]
if hierarchy[3] != -1:
continue
separated_mask = np.zeros_like(mask_i_uint8)
cv2.drawContours(separated_mask, [contour], 0, 255, -1)
separated_mask = np.array(separated_mask / 255.0).astype(np.float32)
x, y, w, h = cv2.boundingRect(contour)
bbox = x, y, x + w, y + h
crop_region = utils.make_crop_region(
mask_i.shape[1], mask_i.shape[0], bbox, crop_factor, crop_min_size
)
if detailer_hook is not None:
crop_region = detailer_hook.post_crop_region(mask_i.shape[1], mask_i.shape[0], bbox, crop_region)
if w > drop_size and h > drop_size:
if is_contour:
mask_src = separated_mask
else:
mask_src = mask_i * separated_mask
cropped_mask = np.array(
mask_src[
crop_region[1]: crop_region[3],
crop_region[0]: crop_region[2],
]
)
if bbox_fill:
cx1, cy1, _, _ = crop_region
bx1 = x - cx1
bx2 = x+w - cx1
by1 = y - cy1
by2 = y+h - cy1
cropped_mask[by1:by2, bx1:bx2] = 1.0
if cropped_mask is not None:
cropped_mask = torch.clip(torch.from_numpy(cropped_mask), 0, 1.0)
item = SEG(None, cropped_mask.numpy(), 1.0, crop_region, bbox, label, None)
result.append(item)
if not result:
logging.info("[mask_to_segs] Empty mask.")
logging.info(f"# of Detected SEGS: {len(result)}")
# for r in result:
# print(f"\tbbox={r.bbox}, crop={r.crop_region}, label={r.label}")
# shape: (b,h,w) -> (h,w)
return (mask.shape[1], mask.shape[2]), result
def mediapipe_facemesh_to_segs(image, crop_factor, bbox_fill, crop_min_size, drop_size, dilation, face, mouth, left_eyebrow, left_eye, left_pupil, right_eyebrow, right_eye, right_pupil):
parts = {
"face": np.array([0x0A, 0xC8, 0x0A]),
"mouth": np.array([0x0A, 0xB4, 0x0A]),
"left_eyebrow": np.array([0xB4, 0xDC, 0x0A]),
"left_eye": np.array([0xB4, 0xC8, 0x0A]),
"left_pupil": np.array([0xFA, 0xC8, 0x0A]),
"right_eyebrow": np.array([0x0A, 0xDC, 0xB4]),
"right_eye": np.array([0x0A, 0xC8, 0xB4]),
"right_pupil": np.array([0x0A, 0xC8, 0xFA]),
}
def create_segments(image, color):
image = (image * 255).to(torch.uint8)
image = image.squeeze(0).numpy()
mask = cv2.inRange(image, color, color)
contours, ctree = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
mask_list = []
for i, contour in enumerate(contours):
hierarchy = ctree[0][i]
if hierarchy[3] == -1:
convex_hull = cv2.convexHull(contour)
convex_segment = np.zeros_like(image)
cv2.fillPoly(convex_segment, [convex_hull], (255, 255, 255))
convex_segment = np.expand_dims(convex_segment, axis=0).astype(np.float32) / 255.0
tensor = torch.from_numpy(convex_segment)
mask_tensor = torch.any(tensor != 0, dim=-1).float()
mask_tensor = mask_tensor.squeeze(0)
mask_tensor = torch.from_numpy(utils.dilate_mask(mask_tensor.numpy(), dilation))
mask_list.append(mask_tensor.unsqueeze(0))
return mask_list
segs = []
def create_seg(label):
mask_list = create_segments(image, parts[label])
for mask in mask_list:
seg = mask_to_segs(mask, False, crop_factor, bbox_fill, drop_size=drop_size, label=label, crop_min_size=crop_min_size)
if len(seg[1]) > 0:
segs.extend(seg[1])
if face:
create_seg('face')
if mouth:
create_seg('mouth')
if left_eyebrow:
create_seg('left_eyebrow')
if left_eye:
create_seg('left_eye')
if left_pupil:
create_seg('left_pupil')
if right_eyebrow:
create_seg('right_eyebrow')
if right_eye:
create_seg('right_eye')
if right_pupil:
create_seg('right_pupil')
return (image.shape[1], image.shape[2]), segs
def segs_to_combined_mask(segs):
shape = segs[0]
h = shape[0]
w = shape[1]
mask = np.zeros((h, w), dtype=np.uint8)
for seg in segs[1]:
cropped_mask = seg.cropped_mask
crop_region = seg.crop_region
mask[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]] |= (cropped_mask * 255).astype(np.uint8)
return torch.from_numpy(mask.astype(np.float32) / 255.0)
def segs_to_masklist(segs):
shape = segs[0]
h = shape[0]
w = shape[1]
masks = []
for seg in segs[1]:
if isinstance(seg.cropped_mask, np.ndarray):
cropped_mask = torch.from_numpy(seg.cropped_mask)
else:
cropped_mask = seg.cropped_mask
if cropped_mask.ndim == 2:
cropped_mask = cropped_mask.unsqueeze(0)
n = len(cropped_mask)
mask = torch.zeros((n, h, w), dtype=torch.uint8)
crop_region = seg.crop_region
mask[:, crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]] |= (cropped_mask * 255).to(torch.uint8)
mask = (mask / 255.0).to(torch.float32)
for x in mask:
masks.append(x)
if len(masks) == 0:
empty_mask = torch.zeros((h, w), dtype=torch.float32, device="cpu")
masks = [empty_mask]
return masks
def vae_decode(vae, samples, use_tile, hook, tile_size=512, overlap=64):
if use_tile:
decoder = nodes.VAEDecodeTiled()
if 'overlap' in inspect.signature(decoder.decode).parameters:
pixels = decoder.decode(vae, samples, tile_size, overlap=overlap)[0]
else:
logging.warning("[Impact Pack] Your ComfyUI is outdated.")
pixels = decoder.decode(vae, samples, tile_size)[0]
else:
pixels = nodes.VAEDecode().decode(vae, samples)[0]
if hook is not None:
pixels = hook.post_decode(pixels)
return pixels
def vae_encode(vae, pixels, use_tile, hook, tile_size=512, overlap=64):
if use_tile:
encoder = nodes.VAEEncodeTiled()
if 'overlap' in inspect.signature(encoder.encode).parameters:
samples = encoder.encode(vae, pixels, tile_size, overlap=overlap)[0]
else:
logging.warning("[Impact Pack] Your ComfyUI is outdated.")
samples = encoder.encode(vae, pixels, tile_size)[0]
else:
samples = nodes.VAEEncode().encode(vae, pixels)[0]
if hook is not None:
samples = hook.post_encode(samples)
return samples
def latent_upscale_on_pixel_space_shape(samples, scale_method, w, h, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
return latent_upscale_on_pixel_space_shape2(samples, scale_method, w, h, vae, use_tile, tile_size, save_temp_prefix, hook, overlap=overlap)[0]
def latent_upscale_on_pixel_space_shape2(samples, scale_method, w, h, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
pixels = vae_decode(vae, samples, use_tile, hook, tile_size=tile_size, overlap=overlap)
if save_temp_prefix is not None:
nodes.PreviewImage().save_images(pixels, filename_prefix=save_temp_prefix)
pixels = nodes.ImageScale().upscale(pixels, scale_method, int(w), int(h), False)[0]
old_pixels = pixels
if hook is not None:
pixels = hook.post_upscale(pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space(samples, scale_method, scale_factor, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
return latent_upscale_on_pixel_space2(samples, scale_method, scale_factor, vae, use_tile, tile_size, save_temp_prefix, hook, overlap=overlap)[0]
def latent_upscale_on_pixel_space2(samples, scale_method, scale_factor, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
pixels = vae_decode(vae, samples, use_tile, hook, tile_size=tile_size, overlap=overlap)
if save_temp_prefix is not None:
nodes.PreviewImage().save_images(pixels, filename_prefix=save_temp_prefix)
w = pixels.shape[2] * scale_factor
h = pixels.shape[1] * scale_factor
pixels = nodes.ImageScale().upscale(pixels, scale_method, int(w), int(h), False)[0]
old_pixels = pixels
if hook is not None:
pixels = hook.post_upscale(pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space_with_model_shape(samples, scale_method, upscale_model, new_w, new_h, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
return latent_upscale_on_pixel_space_with_model_shape2(samples, scale_method, upscale_model, new_w, new_h, vae, use_tile, tile_size, save_temp_prefix, hook, overlap=overlap)[0]
def latent_upscale_on_pixel_space_with_model_shape2(samples, scale_method, upscale_model, new_w, new_h, vae, use_tile=False, tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
pixels = vae_decode(vae, samples, use_tile, hook, tile_size=tile_size, overlap=overlap)
if save_temp_prefix is not None:
nodes.PreviewImage().save_images(pixels, filename_prefix=save_temp_prefix)
w = pixels.shape[2]
# upscale by model upscaler
current_w = w
while current_w < new_w:
model_upscaler = nodes.NODE_CLASS_MAPPINGS['ImageUpscaleWithModel']()
if hasattr(model_upscaler, 'execute'):
pixels = model_upscaler.execute(upscale_model, pixels)[0]
else:
pixels = model_upscaler.upscale(upscale_model, pixels)[0]
current_w = pixels.shape[2]
if current_w == w:
logging.info("[latent_upscale_on_pixel_space_with_model] x1 upscale model selected")
break
# downscale to target scale
pixels = nodes.ImageScale().upscale(pixels, scale_method, int(new_w), int(new_h), False)[0]
old_pixels = pixels
if hook is not None:
pixels = hook.post_upscale(pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
def latent_upscale_on_pixel_space_with_model(samples, scale_method, upscale_model, scale_factor, vae, use_tile=False,
tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
return latent_upscale_on_pixel_space_with_model2(samples, scale_method, upscale_model, scale_factor, vae, use_tile, tile_size, save_temp_prefix, hook, overlap=overlap)[0]
def latent_upscale_on_pixel_space_with_model2(samples, scale_method, upscale_model, scale_factor, vae, use_tile=False,
tile_size=512, save_temp_prefix=None, hook=None, overlap=64):
pixels = vae_decode(vae, samples, use_tile, hook, tile_size=tile_size, overlap=overlap)
if save_temp_prefix is not None:
nodes.PreviewImage().save_images(pixels, filename_prefix=save_temp_prefix)
w = pixels.shape[2]
h = pixels.shape[1]
new_w = w * scale_factor
new_h = h * scale_factor
# upscale by model upscaler
current_w = w
while current_w < new_w:
model_upscaler = nodes.NODE_CLASS_MAPPINGS['ImageUpscaleWithModel']()
if hasattr(model_upscaler, 'execute'):
pixels = model_upscaler.execute(upscale_model, pixels)[0]
else:
pixels = model_upscaler.upscale(upscale_model, pixels)[0]
current_w = pixels.shape[2]
if current_w == w:
logging.info("[latent_upscale_on_pixel_space_with_model] x1 upscale model selected")
break
# downscale to target scale
pixels = nodes.ImageScale().upscale(pixels, scale_method, int(new_w), int(new_h), False)[0]
old_pixels = pixels
if hook is not None:
pixels = hook.post_upscale(pixels)
return vae_encode(vae, pixels, use_tile, hook, tile_size=tile_size, overlap=overlap), old_pixels
class TwoSamplersForMaskUpscaler:
def __init__(self, scale_method, sample_schedule, use_tiled_vae, base_sampler, mask_sampler, mask, vae,
full_sampler_opt=None, upscale_model_opt=None, hook_base_opt=None, hook_mask_opt=None,
hook_full_opt=None,
tile_size=512):
mask = utils.make_2d_mask(mask)
mask = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
self.params = scale_method, sample_schedule, use_tiled_vae, base_sampler, mask_sampler, mask, vae
self.upscale_model = upscale_model_opt
self.full_sampler = full_sampler_opt
self.hook_base = hook_base_opt
self.hook_mask = hook_mask_opt
self.hook_full = hook_full_opt
self.use_tiled_vae = use_tiled_vae
self.tile_size = tile_size
self.is_tiled = False
self.vae = vae
def upscale(self, step_info, samples, upscale_factor, save_temp_prefix=None):
scale_method, sample_schedule, use_tiled_vae, base_sampler, mask_sampler, mask, vae = self.params
mask = utils.make_2d_mask(mask)
self.prepare_hook(step_info)
# upscale latent
if self.upscale_model is None:
upscaled_latent = latent_upscale_on_pixel_space(samples, scale_method, upscale_factor, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook_base, tile_size=self.tile_size)
else:
upscaled_latent = latent_upscale_on_pixel_space_with_model(samples, scale_method, self.upscale_model,
upscale_factor, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook_mask, tile_size=self.tile_size)
return self.do_samples(step_info, base_sampler, mask_sampler, sample_schedule, mask, upscaled_latent)
def prepare_hook(self, step_info):
if self.hook_base is not None:
self.hook_base.set_steps(step_info)
if self.hook_mask is not None:
self.hook_mask.set_steps(step_info)
if self.hook_full is not None:
self.hook_full.set_steps(step_info)
def upscale_shape(self, step_info, samples, w, h, save_temp_prefix=None):
scale_method, sample_schedule, use_tiled_vae, base_sampler, mask_sampler, mask, vae = self.params
mask = utils.make_2d_mask(mask)
self.prepare_hook(step_info)
# upscale latent
if self.upscale_model is None:
upscaled_latent = latent_upscale_on_pixel_space_shape(samples, scale_method, w, h, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook_base,
tile_size=self.tile_size)
else:
upscaled_latent = latent_upscale_on_pixel_space_with_model_shape(samples, scale_method, self.upscale_model,
w, h, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook_mask,
tile_size=self.tile_size)
return self.do_samples(step_info, base_sampler, mask_sampler, sample_schedule, mask, upscaled_latent)
def is_full_sample_time(self, step_info, sample_schedule):
cur_step, total_step = step_info
# make start from 1 instead of zero
cur_step += 1
total_step += 1
if sample_schedule == "none":
return False
elif sample_schedule == "interleave1":
return cur_step % 2 == 0
elif sample_schedule == "interleave2":
return cur_step % 3 == 0
elif sample_schedule == "interleave3":
return cur_step % 4 == 0
elif sample_schedule == "last1":
return cur_step == total_step
elif sample_schedule == "last2":
return cur_step >= total_step - 1
elif sample_schedule == "interleave1+last1":
return cur_step % 2 == 0 or cur_step >= total_step - 1
elif sample_schedule == "interleave2+last1":
return cur_step % 2 == 0 or cur_step >= total_step - 1
elif sample_schedule == "interleave3+last1":
return cur_step % 2 == 0 or cur_step >= total_step - 1
def do_samples(self, step_info, base_sampler, mask_sampler, sample_schedule, mask, upscaled_latent):
mask = utils.make_2d_mask(mask)
if self.is_full_sample_time(step_info, sample_schedule):
logging.info(f"step_info={step_info} / full time")
upscaled_latent = base_sampler.sample(upscaled_latent, self.hook_base)
sampler = self.full_sampler if self.full_sampler is not None else base_sampler
return sampler.sample(upscaled_latent, self.hook_full)
else:
logging.info(f"step_info={step_info} / non-full time")
# upscale mask
if mask.ndim == 2:
mask = mask[None, :, :, None]
upscaled_mask = F.interpolate(mask, size=(upscaled_latent['samples'].shape[2], upscaled_latent['samples'].shape[3]), mode='bilinear', align_corners=True)
upscaled_mask = upscaled_mask[:, :, :upscaled_latent['samples'].shape[2], :upscaled_latent['samples'].shape[3]]
# base sampler
upscaled_inv_mask = torch.where(upscaled_mask != 1.0, torch.tensor(1.0), torch.tensor(0.0))
upscaled_latent['noise_mask'] = upscaled_inv_mask
upscaled_latent = base_sampler.sample(upscaled_latent, self.hook_base)
# mask sampler
upscaled_latent['noise_mask'] = upscaled_mask
upscaled_latent = mask_sampler.sample(upscaled_latent, self.hook_mask)
# remove mask
del upscaled_latent['noise_mask']
return upscaled_latent
class PixelKSampleUpscaler:
def __init__(self, scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise,
use_tiled_vae, upscale_model_opt=None, hook_opt=None, tile_size=512, scheduler_func=None,
tile_cnet_opt=None, tile_cnet_strength=1.0):
self.params = scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise
self.upscale_model = upscale_model_opt
self.hook = hook_opt
self.use_tiled_vae = use_tiled_vae
self.tile_size = tile_size
self.is_tiled = False
self.vae = vae
self.scheduler_func = scheduler_func
self.tile_cnet = tile_cnet_opt
self.tile_cnet_strength = tile_cnet_strength
def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise, images):
if self.tile_cnet is not None:
image_batch, image_w, image_h, _ = images.shape
if image_batch > 1:
warnings.warn('Multiple latents in batch, Tile ControlNet being ignored')
else:
if 'TilePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
raise RuntimeError("'TilePreprocessor' node (from comfyui_controlnet_aux) isn't installed.")
preprocessor = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
# might add capacity to set pyrUp_iters later, not needed for now though
preprocessed = preprocessor.execute(images, pyrUp_iters=3, resolution=min(image_w, image_h))[0]
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive=positive,
negative=negative,
control_net=self.tile_cnet,
image=preprocessed,
strength=self.tile_cnet_strength,
start_percent=0,
end_percent=1.0,
vae=self.vae)
refined_latent = impact_sampling.impact_sample(model, seed, steps, cfg, sampler_name, scheduler,
positive, negative, upscaled_latent, denoise, scheduler_func=self.scheduler_func)
return refined_latent
def upscale(self, step_info, samples, upscale_factor, save_temp_prefix=None):
scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise = self.params
if self.hook is not None:
self.hook.set_steps(step_info)
if self.upscale_model is None:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space2(samples, scale_method, upscale_factor, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix, hook=self.hook, tile_size=512)
else:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_with_model2(samples, scale_method, self.upscale_model,
upscale_factor, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook,
tile_size=self.tile_size)
if self.hook is not None:
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise = \
self.hook.pre_ksample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
upscaled_latent, denoise)
if 'noise_mask' in samples:
upscaled_latent['noise_mask'] = samples['noise_mask']
refined_latent = self.sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise, upscaled_images)
return refined_latent
def upscale_shape(self, step_info, samples, w, h, save_temp_prefix=None):
scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise = self.params
if self.hook is not None:
self.hook.set_steps(step_info)
if self.upscale_model is None:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_shape2(samples, scale_method, w, h, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix, hook=self.hook,
tile_size=self.tile_size)
else:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_with_model_shape2(samples, scale_method, self.upscale_model,
w, h, vae,
use_tile=self.use_tiled_vae,
save_temp_prefix=save_temp_prefix,
hook=self.hook,
tile_size=self.tile_size)
if self.hook is not None:
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise = \
self.hook.pre_ksample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
upscaled_latent, denoise)
if 'noise_mask' in samples:
upscaled_latent['noise_mask'] = samples['noise_mask']
refined_latent = self.sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise, upscaled_images)
return refined_latent
class IPAdapterWrapper:
def __init__(self, ipadapter_pipe, weight, noise, weight_type, start_at, end_at, unfold_batch, weight_v2, reference_image, neg_image=None, prev_control_net=None, combine_embeds='concat'):
self.reference_image = reference_image
self.ipadapter_pipe = ipadapter_pipe
self.weight = weight
self.weight_type = weight_type
self.noise = noise
self.start_at = start_at
self.end_at = end_at
self.unfold_batch = unfold_batch
self.prev_control_net = prev_control_net
self.weight_v2 = weight_v2
self.image = reference_image
self.neg_image = neg_image
self.combine_embeds = combine_embeds
# name 'apply_ipadapter' isn't allowed
def doit_ipadapter(self, model):
cnet_image_list = [self.image]
prev_cnet_images = []
if 'IPAdapterAdvanced' not in nodes.NODE_CLASS_MAPPINGS:
if 'IPAdapterApply' in nodes.NODE_CLASS_MAPPINGS:
raise Exception("[ERROR] 'ComfyUI IPAdapter Plus' is outdated.")
utils.try_install_custom_node('https://github.com/cubiq/ComfyUI_IPAdapter_plus',
"To use 'IPAdapterApplySEGS' node, 'ComfyUI IPAdapter Plus' extension is required.")
raise Exception("[ERROR] To use IPAdapterApplySEGS, you need to install 'ComfyUI IPAdapter Plus'")
obj = nodes.NODE_CLASS_MAPPINGS['IPAdapterAdvanced']
ipadapter, _, clip_vision, insightface, lora_loader = self.ipadapter_pipe
model = lora_loader(model)
if self.prev_control_net is not None:
model, prev_cnet_images = self.prev_control_net.doit_ipadapter(model)
model = obj().apply_ipadapter(model=model, ipadapter=ipadapter, weight=self.weight, weight_type=self.weight_type,
start_at=self.start_at, end_at=self.end_at, combine_embeds=self.combine_embeds,
clip_vision=clip_vision, image=self.image, image_negative=self.neg_image, attn_mask=None,
insightface=insightface, weight_faceidv2=self.weight_v2)[0]
cnet_image_list.extend(prev_cnet_images)
return model, cnet_image_list
def apply(self, positive, negative, image, mask=None, use_acn=False):
if self.prev_control_net is not None:
return self.prev_control_net.apply(positive, negative, image, mask, use_acn=use_acn)
else:
return positive, negative, []
class ControlNetWrapper:
def __init__(self, control_net, strength, preprocessor, prev_control_net=None, original_size=None, crop_region=None, control_image=None):
self.control_net = control_net
self.strength = strength
self.preprocessor = preprocessor
self.prev_control_net = prev_control_net
if original_size is not None and crop_region is not None and control_image is not None:
self.control_image = utils.tensor_resize(control_image, original_size[1], original_size[0])
self.control_image = torch.tensor(utils.tensor_crop(self.control_image, crop_region))
else:
self.control_image = None
def apply(self, positive, negative, image, mask=None, use_acn=False):
cnet_image_list = []
prev_cnet_images = []
if self.prev_control_net is not None:
positive, negative, prev_cnet_images = self.prev_control_net.apply(positive, negative, image, mask, use_acn=use_acn)
if self.control_image is not None:
cnet_image = self.control_image
elif self.preprocessor is not None:
cnet_image = self.preprocessor.apply(image, mask)
else:
cnet_image = image
cnet_image_list.extend(prev_cnet_images)
cnet_image_list.append(cnet_image)
if use_acn:
if "ACN_AdvancedControlNetApply" in nodes.NODE_CLASS_MAPPINGS:
acn = nodes.NODE_CLASS_MAPPINGS['ACN_AdvancedControlNetApply']()
positive, negative, _ = acn.apply_controlnet(positive=positive, negative=negative, control_net=self.control_net, image=cnet_image,
strength=self.strength, start_percent=0.0, end_percent=1.0)
else:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_TiledKSampler',
"To use 'ControlNetWrapper' for AnimateDiff, 'ComfyUI-Advanced-ControlNet' extension is required.")
raise Exception("'ACN_AdvancedControlNetApply' node isn't installed.")
else:
positive = nodes.ControlNetApply().apply_controlnet(positive, self.control_net, cnet_image, self.strength)[0]
return positive, negative, cnet_image_list
def doit_ipadapter(self, model):
if self.prev_control_net is not None:
return self.prev_control_net.doit_ipadapter(model)
else:
return model, []
class ControlNetAdvancedWrapper:
def __init__(self, control_net, strength, start_percent, end_percent, preprocessor, prev_control_net=None,
original_size=None, crop_region=None, control_image=None, vae=None):
self.control_net = control_net
self.strength = strength
self.preprocessor = preprocessor
self.prev_control_net = prev_control_net
self.start_percent = start_percent
self.end_percent = end_percent
self.vae = vae
if original_size is not None and crop_region is not None and control_image is not None:
self.control_image = utils.tensor_resize(control_image, original_size[1], original_size[0])
self.control_image = torch.tensor(utils.tensor_crop(self.control_image, crop_region))
else:
self.control_image = None
def doit_ipadapter(self, model):
if self.prev_control_net is not None:
return self.prev_control_net.doit_ipadapter(model)
else:
return model, []
def apply(self, positive, negative, image, mask=None, use_acn=False):
cnet_image_list = []
prev_cnet_images = []
if self.prev_control_net is not None:
positive, negative, prev_cnet_images = self.prev_control_net.apply(positive, negative, image, mask)
if self.control_image is not None:
cnet_image = self.control_image
elif self.preprocessor is not None:
cnet_image = self.preprocessor.apply(image, mask)
else:
cnet_image = image
cnet_image_list.extend(prev_cnet_images)
cnet_image_list.append(cnet_image)
if use_acn:
if "ACN_AdvancedControlNetApply" in nodes.NODE_CLASS_MAPPINGS:
acn = nodes.NODE_CLASS_MAPPINGS['ACN_AdvancedControlNetApply']()
positive, negative, _ = acn.apply_controlnet(positive=positive, negative=negative, control_net=self.control_net, image=cnet_image,
strength=self.strength, start_percent=self.start_percent, end_percent=self.end_percent)
else:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_TiledKSampler',
"To use 'ControlNetAdvancedWrapper' for AnimateDiff, 'ComfyUI-Advanced-ControlNet' extension is required.")
raise Exception("'ACN_AdvancedControlNetApply' node isn't installed.")
else:
if self.vae is not None:
apply_controlnet = nodes.ControlNetApplyAdvanced().apply_controlnet
signature = inspect.signature(apply_controlnet)
if 'vae' in signature.parameters:
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive, negative, self.control_net, cnet_image, self.strength, self.start_percent, self.end_percent, vae=self.vae)
else:
logging.error("[Impact Pack] ERROR: The ComfyUI version is outdated. VAE cannot be used in ApplyControlNet.")
raise Exception("[Impact Pack] ERROR: The ComfyUI version is outdated. VAE cannot be used in ApplyControlNet.")
else:
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive, negative, self.control_net, cnet_image, self.strength, self.start_percent, self.end_percent)
return positive, negative, cnet_image_list
# REQUIREMENTS: BlenderNeko/ComfyUI_TiledKSampler
class TiledKSamplerWrapper:
params = None
def __init__(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise,
tile_width, tile_height, tiling_strategy):
self.params = model, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise, tile_width, tile_height, tiling_strategy
def sample(self, latent_image, hook=None):
if "BNK_TiledKSampler" in nodes.NODE_CLASS_MAPPINGS:
TiledKSampler = nodes.NODE_CLASS_MAPPINGS['BNK_TiledKSampler']
else:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_TiledKSampler',
"To use 'TiledKSamplerProvider', 'Tiled sampling for ComfyUI' extension is required.")
raise Exception("'BNK_TiledKSampler' node isn't installed.")
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise, tile_width, tile_height, tiling_strategy = self.params
if hook is not None:
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, upscaled_latent, denoise = \
hook.pre_ksample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
denoise)
return TiledKSampler().sample(model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name,
scheduler, positive, negative, latent_image, denoise)[0]
class PixelTiledKSampleUpscaler:
def __init__(self, scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative,
denoise,
tile_width, tile_height, tiling_strategy,
upscale_model_opt=None, hook_opt=None, tile_cnet_opt=None, tile_size=512, tile_cnet_strength=1.0, overlap=64):
self.params = scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise
self.vae = vae
self.tile_params = tile_width, tile_height, tiling_strategy
self.upscale_model = upscale_model_opt
self.hook = hook_opt
self.tile_cnet = tile_cnet_opt
self.tile_size = tile_size
self.is_tiled = True
self.tile_cnet_strength = tile_cnet_strength
self.overlap = overlap
def tiled_ksample(self, latent, images):
if "BNK_TiledKSampler" in nodes.NODE_CLASS_MAPPINGS:
TiledKSampler = nodes.NODE_CLASS_MAPPINGS['BNK_TiledKSampler']
else:
utils.try_install_custom_node('https://github.com/BlenderNeko/ComfyUI_TiledKSampler',
"To use 'PixelTiledKSampleUpscalerProvider', 'Tiled sampling for ComfyUI' extension is required.")
raise RuntimeError("'BNK_TiledKSampler' node isn't installed.")
scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise = self.params
tile_width, tile_height, tiling_strategy = self.tile_params
if self.tile_cnet is not None:
image_batch, image_w, image_h, _ = images.shape
if image_batch > 1:
warnings.warn('Multiple latents in batch, Tile ControlNet being ignored')
else:
if 'TilePreprocessor' not in nodes.NODE_CLASS_MAPPINGS:
raise RuntimeError("'TilePreprocessor' node (from comfyui_controlnet_aux) isn't installed.")
preprocessor = nodes.NODE_CLASS_MAPPINGS['TilePreprocessor']()
# might add capacity to set pyrUp_iters later, not needed for now though
preprocessed = preprocessor.execute(images, pyrUp_iters=3, resolution=min(image_w, image_h))[0]
positive, negative = nodes.ControlNetApplyAdvanced().apply_controlnet(positive=positive,
negative=negative,
control_net=self.tile_cnet,
image=preprocessed,
strength=self.tile_cnet_strength,
start_percent=0, end_percent=1.0,
vae=self.vae)
return TiledKSampler().sample(model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, sampler_name,
scheduler, positive, negative, latent, denoise)[0]
def upscale(self, step_info, samples, upscale_factor, save_temp_prefix=None):
scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise = self.params
if self.hook is not None:
self.hook.set_steps(step_info)
if self.upscale_model is None:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space2(samples, scale_method, upscale_factor, vae,
use_tile=True, save_temp_prefix=save_temp_prefix,
hook=self.hook, tile_size=self.tile_size)
else:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_with_model2(samples, scale_method, self.upscale_model,
upscale_factor, vae, use_tile=True,
save_temp_prefix=save_temp_prefix,
hook=self.hook, tile_size=self.tile_size)
refined_latent = self.tiled_ksample(upscaled_latent, upscaled_images)
return refined_latent
def upscale_shape(self, step_info, samples, w, h, save_temp_prefix=None):
scale_method, model, vae, seed, steps, cfg, sampler_name, scheduler, positive, negative, denoise = self.params
if self.hook is not None:
self.hook.set_steps(step_info)
if self.upscale_model is None:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_shape2(samples, scale_method, w, h, vae,
use_tile=True, save_temp_prefix=save_temp_prefix,
hook=self.hook, tile_size=self.tile_size)
else:
upscaled_latent, upscaled_images = \
latent_upscale_on_pixel_space_with_model_shape2(samples, scale_method,
self.upscale_model, w, h, vae,
use_tile=True,
save_temp_prefix=save_temp_prefix,
hook=self.hook,
tile_size=self.tile_size)
refined_latent = self.tiled_ksample(upscaled_latent, upscaled_images)
return refined_latent
# REQUIREMENTS: biegert/ComfyUI-CLIPSeg
class BBoxDetectorBasedOnCLIPSeg:
prompt = None
blur = None
threshold = None
dilation_factor = None
aux = None
def __init__(self, prompt, blur, threshold, dilation_factor):
self.prompt = prompt
self.blur = blur
self.threshold = threshold
self.dilation_factor = dilation_factor
def detect(self, image, bbox_threshold, bbox_dilation, bbox_crop_factor, drop_size=1, detailer_hook=None):
mask = self.detect_combined(image, bbox_threshold, bbox_dilation)
mask = utils.make_2d_mask(mask)
segs = mask_to_segs(mask, False, bbox_crop_factor, True, drop_size, detailer_hook=detailer_hook)
if detailer_hook is not None and hasattr(detailer_hook, "post_detection"):
segs = detailer_hook.post_detection(segs)
return segs
def detect_combined(self, image, bbox_threshold, bbox_dilation):
if "CLIPSeg" in nodes.NODE_CLASS_MAPPINGS:
CLIPSeg = nodes.NODE_CLASS_MAPPINGS['CLIPSeg']
else:
utils.try_install_custom_node('https://github.com/biegert/ComfyUI-CLIPSeg/raw/main/custom_nodes/clipseg.py',
"To use 'CLIPSegDetectorProvider', 'CLIPSeg' extension is required.")
raise Exception("'CLIPSeg' node isn't installed.")
if self.threshold is None:
threshold = bbox_threshold
else:
threshold = self.threshold
if self.dilation_factor is None:
dilation_factor = bbox_dilation
else:
dilation_factor = self.dilation_factor
prompt = self.aux if self.prompt == '' and self.aux is not None else self.prompt
mask, _, _ = CLIPSeg().segment_image(image, prompt, self.blur, threshold, dilation_factor)
mask = utils.to_binary_mask(mask)
return mask
def setAux(self, x):
self.aux = x
def update_node_status(node, text, progress=None):
if PromptServer.instance.client_id is None:
return
PromptServer.instance.send_sync("impact/update_status", {
"node": node,
"progress": progress,
"text": text
}, PromptServer.instance.client_id)
def random_mask_raw(mask, bbox, factor):
x1, y1, x2, y2 = bbox
w = x2 - x1
h = y2 - y1
factor = max(6, int(min(w, h) * factor / 4))
def draw_random_circle(center, radius):
i, j = center
for x in range(int(i - radius), int(i + radius)):
for y in range(int(j - radius), int(j + radius)):
if np.linalg.norm(np.array([x, y]) - np.array([i, j])) <= radius:
mask[x, y] = 1
def draw_irregular_line(start, end, pivot, is_vertical):
i = start
while i < end:
base_radius = np.random.randint(5, factor)
radius = int(base_radius)
if is_vertical:
draw_random_circle((i, pivot), radius)
else:
draw_random_circle((pivot, i), radius)
i += radius
def draw_irregular_line_parallel(start, end, pivot, is_vertical):
with ThreadPoolExecutor(max_workers=16) as executor:
futures = []
step = (end - start) // 16
for i in range(start, end, step):
future = executor.submit(draw_irregular_line, i, min(i + step, end), pivot, is_vertical)
futures.append(future)
for future in futures:
future.result()
draw_irregular_line_parallel(y1 + factor, y2 - factor, x1 + factor, True)
draw_irregular_line_parallel(y1 + factor, y2 - factor, x2 - factor, True)
draw_irregular_line_parallel(x1 + factor, x2 - factor, y1 + factor, False)
draw_irregular_line_parallel(x1 + factor, x2 - factor, y2 - factor, False)
mask[y1 + factor:y2 - factor, x1 + factor:x2 - factor] = 1.0
def random_mask(mask, bbox, factor, size=128):
small_mask = np.zeros((size, size)).astype(np.float32)
random_mask_raw(small_mask, (0, 0, size, size), factor)
x1, y1, x2, y2 = bbox
small_mask = torch.tensor(small_mask).unsqueeze(0).unsqueeze(0)
bbox_mask = torch.nn.functional.interpolate(small_mask, size=(y2 - y1, x2 - x1), mode='bilinear', align_corners=False)
bbox_mask = bbox_mask.squeeze(0).squeeze(0)
mask[y1:y2, x1:x2] = bbox_mask
def adaptive_mask_paste(dest_mask, src_mask, bbox):
x1, y1, x2, y2 = bbox
small_mask = torch.tensor(src_mask).unsqueeze(0).unsqueeze(0)
bbox_mask = torch.nn.functional.interpolate(small_mask, size=(y2 - y1, x2 - x1), mode='bilinear', align_corners=False)
bbox_mask = bbox_mask.squeeze(0).squeeze(0)
dest_mask[y1:y2, x1:x2] = bbox_mask
def crop_condition_mask(mask, image, crop_region):
cond_scale = (mask.shape[1] / image.shape[1], mask.shape[2] / image.shape[2])
mask_region = [round(v * cond_scale[i % 2]) for i, v in enumerate(crop_region)]
return utils.crop_ndarray3(mask, mask_region)
class SafeToGPU:
def __init__(self, size):
self.size = size
def to_device(self, obj, device):
if utils.is_same_device(device, 'cpu'):
obj.to(device)
else:
if utils.is_same_device(obj.device, 'cpu'): # cpu to gpu
model_management.free_memory(self.size * 1.3, device)
if model_management.get_free_memory(device) > self.size * 1.3:
try:
obj.to(device)
except Exception:
logging.warning(f"[Impact Pack] The model is not moved to the '{device}' due to insufficient memory. [1]")
else:
logging.warning(f"[Impact Pack] The model is not moved to the '{device}' due to insufficient memory. [2]")
class SafeToGPU_stub():
def to_device(self, obj, device):
pass
from comfy.cli_args import args, LatentPreviewMethod
import folder_paths
from latent_preview import TAESD, TAESDPreviewerImpl, Latent2RGBPreviewer
try:
import comfy.latent_formats as latent_formats
def get_previewer(device, latent_format=latent_formats.SD15(), force=False, method=None):
previewer = None
if method is None:
method = args.preview_method
if method != LatentPreviewMethod.NoPreviews or force:
# TODO previewer methods
taesd_decoder_path = None
if hasattr(latent_format, "taesd_decoder_path"):
taesd_decoder_path = folder_paths.get_full_path("vae_approx", latent_format.taesd_decoder_name)
if method == LatentPreviewMethod.Auto:
method = LatentPreviewMethod.Latent2RGB
if taesd_decoder_path:
method = LatentPreviewMethod.TAESD
if method == LatentPreviewMethod.TAESD:
if taesd_decoder_path:
taesd = TAESD(None, taesd_decoder_path, latent_channels=latent_format.latent_channels).to(device)
previewer = TAESDPreviewerImpl(taesd)
else:
logging.warning("[Impact Pack] TAESD previews enabled, but could not find models/vae_approx/{}".format(
latent_format.taesd_decoder_name))
if previewer is None:
previewer = Latent2RGBPreviewer(latent_format.latent_rgb_factors)
return previewer
except Exception:
logging.error("#########################################################################")
logging.error("[ERROR] ComfyUI-Impact-Pack: Please update ComfyUI to the latest version.")
logging.error("#########################################################################")