improve: add support wildcard for FaceDetailer

improve: add max_size for Detailers
- breaking changes
improve: placeholder for detailer wildcard
bugfix: side-effect when cropping region are overlapped between SEGS
bugfix: crash when bitwise operation with empty mask
This commit is contained in:
Dr.Lt.Data
2023-07-02 11:32:08 +09:00
parent 70aed3bd9b
commit 1ec55f5b54
6 changed files with 1095 additions and 1031 deletions
+17
View File
@@ -173,6 +173,23 @@ app.registerExtension({
});
}
switch(node.comfyClass) {
case "ToDetailerPipe":
case "BasicPipeToDetailerPipe":
case "EditDetailerPipe":
case "FaceDetailer":
{
for(let i in node.widgets) {
let widget = node.widgets[i];
if(widget.type === "customtext") {
widget.dynamicPrompts = false;
widget.inputEl.placeholder = "wildcard spec: if kept empty, this option will be ignored";
}
}
}
break;
}
if(node.comfyClass == "ImpactWildcardProcessor") {
node.widgets[0].inputEl.placeholder = "Wildcard Prompt (User input)";
node.widgets[1].inputEl.placeholder = "Populated Prompt (Will be generated automatically)";
+1 -1
View File
@@ -1,7 +1,7 @@
import configparser
import os
version = "V2.19"
version = "V2.20"
dependency_version = 1
+21 -4
View File
@@ -167,9 +167,8 @@ def gen_negative_hints(w, h, x1, y1, x2, y2):
return npoints, nplabs
def enhance_detail(image, model, clip, vae, guide_size, guide_size_for, bbox, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, noise_mask, force_inpaint, wildcard_opt=None):
def enhance_detail(image, model, clip, vae, guide_size, guide_size_for, max_size, bbox, seed, steps, cfg, sampler_name,
scheduler, positive, negative, denoise, noise_mask, force_inpaint, wildcard_opt=None):
if wildcard_opt is not None and wildcard_opt != "":
model, positive = wildcards.process_with_loras(wildcard_opt, model, clip)
@@ -194,6 +193,12 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for, bbox, se
new_w = int(w * upscale)
new_h = int(h * upscale)
# safeguard
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:
print(f"Detailer: segment skip [determined upscale factor={upscale}]")
@@ -211,6 +216,12 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for, bbox, se
print(f"Detailer: segment upscale for ({bbox_w, bbox_h}) | crop region {w, h} x {upscale} -> {new_w, new_h}")
# noise_mask
is_mask_all_zeros = (noise_mask == 0).all().item()
if is_mask_all_zeros:
print(f"Detailer: segment skip [empty mask]")
return None
# upscale
upscaled_image = scale_tensor(new_w, new_h, torch.from_numpy(image))
@@ -615,10 +626,16 @@ def mask_to_segs(mask, combined, crop_factor, bbox_fill, drop_size=1):
crop_region = make_crop_region(mask.shape[1], mask.shape[0], bbox, crop_factor)
if x2 - x1 > drop_size and y2 - y1 > drop_size: # minimum dimension must be (2,2) to avoid squeeze issue
cropped_mask = np.array(mask[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]])
cropped_mask = np.zeros_like(mask[crop_region[1]:crop_region[3], crop_region[0]:crop_region[2]])
if bbox_fill:
cropped_mask.fill(1.0)
else:
cropped_mask_bbox = mask[y1:y2, x1:x2]
bbox_offset_y = y1 - crop_region[1]
bbox_offset_x = x1 - crop_region[0]
cropped_mask[bbox_offset_y:bbox_offset_y + cropped_mask_bbox.shape[0],
bbox_offset_x:bbox_offset_x + cropped_mask_bbox.shape[1]] = cropped_mask_bbox
item = SEG(None, cropped_mask, 1.0, crop_region, bbox, 'A')
+35 -29
View File
@@ -173,6 +173,7 @@ class SEGSDetailer:
"segs": ("SEGS", ),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": (["bbox", "crop_region"],),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
@@ -191,7 +192,7 @@ class SEGSDetailer:
CATEGORY = "ImpactPack/Detailer"
@staticmethod
def do_detail(image, segs, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def do_detail(image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, noise_mask, force_inpaint, basic_pipe):
model, clip, vae, positive, negative = basic_pipe
@@ -207,8 +208,8 @@ class SEGSDetailer:
else:
cropped_mask = None
enhanced_pil = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, seg.bbox,
seed, steps, cfg, sampler_name, scheduler,
enhanced_pil = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, cropped_mask, force_inpaint == "enabled")
new_seg = SEG(enhanced_pil, seg.cropped_mask, seg.confidence, seg.crop_region, seg.bbox, seg.label)
@@ -216,10 +217,10 @@ class SEGSDetailer:
return segs[0], new_segs
def doit(self, image, segs, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def doit(self, image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, noise_mask, force_inpaint, basic_pipe):
segs = SEGSDetailer.do_detail(image, segs, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
segs = SEGSDetailer.do_detail(image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, noise_mask, force_inpaint, basic_pipe)
return (segs, )
@@ -357,6 +358,7 @@ class DetailerForEach:
"vae": ("VAE",),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": (["bbox", "crop_region"],),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
@@ -377,7 +379,7 @@ class DetailerForEach:
CATEGORY = "ImpactPack/Detailer"
@staticmethod
def do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint, wildcard_opt=None):
image_pil = tensor2pil(image).convert('RGBA')
@@ -396,8 +398,8 @@ class DetailerForEach:
else:
cropped_mask = None
enhanced_pil = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, seg.bbox,
seed, steps, cfg, sampler_name, scheduler,
enhanced_pil = core.enhance_detail(cropped_image, model, clip, vae, guide_size, guide_size_for, max_size,
seg.bbox, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, cropped_mask, force_inpaint == "enabled", wildcard_opt)
if not (enhanced_pil is None):
@@ -415,12 +417,12 @@ class DetailerForEach:
return image_tensor, cropped_list, enhanced_list
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint):
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name,
scheduler, positive, negative, denoise, feather, noise_mask, force_inpaint):
enhanced_img, cropped, cropped_enhanced = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg,
sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps,
cfg, sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint)
return (enhanced_img, )
@@ -434,6 +436,7 @@ class DetailerForEachPipe:
"segs": ("SEGS", ),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": (["bbox", "crop_region"],),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
@@ -452,12 +455,12 @@ class DetailerForEachPipe:
CATEGORY = "ImpactPack/Detailer"
def doit(self, image, segs, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def doit(self, image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, feather, noise_mask, force_inpaint, basic_pipe):
model, clip, vae, positive, negative = basic_pipe
enhanced_img, cropped, cropped_enhanced = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg,
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg,
sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint)
@@ -615,6 +618,7 @@ class FaceDetailer:
"vae": ("VAE",),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": (["bbox", "crop_region"],),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
@@ -641,6 +645,7 @@ class FaceDetailer:
"drop_size": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 1, "default": 10}),
"bbox_detector": ("BBOX_DETECTOR", ),
"wildcard": ("STRING", {"multiline": True}),
},
"optional": {
"sam_model_opt": ("SAM_MODEL", ),
@@ -654,7 +659,7 @@ class FaceDetailer:
CATEGORY = "ImpactPack/Simple"
@staticmethod
def enhance_face(image, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def enhance_face(image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint,
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
@@ -673,7 +678,7 @@ class FaceDetailer:
segs = core.segs_bitwise_and_mask(segs, sam_mask)
enhanced_img, _, cropped_enhanced = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg,
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg,
sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint, wildcard_opt)
@@ -682,20 +687,20 @@ class FaceDetailer:
return enhanced_img, cropped_enhanced, mask
def doit(self, image, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def doit(self, image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint,
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
sam_mask_hint_use_negative, drop_size, bbox_detector, sam_model_opt=None):
sam_mask_hint_use_negative, drop_size, bbox_detector, wildcard, sam_model_opt=None):
enhanced_img, cropped_enhanced, mask = FaceDetailer.enhance_face(
image, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint,
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
sam_mask_hint_use_negative, drop_size, bbox_detector, None, sam_model_opt)
sam_mask_hint_use_negative, drop_size, bbox_detector, wildcard, sam_model_opt)
pipe = (model, clip, vae, positive, negative, bbox_detector, None, sam_model_opt)
pipe = (model, clip, vae, positive, negative, bbox_detector, wildcard, sam_model_opt)
return enhanced_img, cropped_enhanced, mask, pipe
@@ -1168,6 +1173,7 @@ class FaceDetailerPipe:
"detailer_pipe": ("DETAILER_PIPE",),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"guide_size_for": (["bbox", "crop_region"],),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": nodes.MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
@@ -1200,7 +1206,7 @@ class FaceDetailerPipe:
CATEGORY = "ImpactPack/Simple"
def doit(self, image, detailer_pipe, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def doit(self, image, detailer_pipe, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, feather, noise_mask, force_inpaint, bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion,
sam_mask_hint_threshold, sam_mask_hint_use_negative, drop_size):
@@ -1208,7 +1214,7 @@ class FaceDetailerPipe:
model, clip, vae, positive, negative, bbox_detector, wildcard, sam_model_opt = detailer_pipe
enhanced_img, cropped_enhanced, mask = FaceDetailer.enhance_face(
image, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint,
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
@@ -1226,12 +1232,12 @@ class DetailerForEachTest(DetailerForEach):
CATEGORY = "ImpactPack/Detailer"
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint):
def doit(self, image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name,
scheduler, positive, negative, denoise, feather, noise_mask, force_inpaint):
enhanced_img, cropped, cropped_enhanced = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg,
sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps,
cfg, sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint)
# set fallback image
@@ -1253,12 +1259,12 @@ class DetailerForEachTestPipe(DetailerForEachPipe):
CATEGORY = "ImpactPack/Detailer"
def doit(self, image, segs, guide_size, guide_size_for, seed, steps, cfg, sampler_name, scheduler,
def doit(self, image, segs, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler,
denoise, feather, noise_mask, force_inpaint, basic_pipe):
model, clip, vae, positive, negative = basic_pipe
enhanced_img, cropped, cropped_enhanced = \
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, seed, steps, cfg,
DetailerForEach.do_detail(image, segs, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg,
sampler_name, scheduler, positive, negative, denoise, feather, noise_mask,
force_inpaint)
+26 -8
View File
@@ -28,7 +28,12 @@ def combine_masks(masks):
for i in range(1, len(masks)):
cv2_mask = np.array(masks[i][1])
combined_cv2_mask = cv2.bitwise_or(combined_cv2_mask, cv2_mask)
if combined_cv2_mask.shape == cv2_mask.shape:
combined_cv2_mask = cv2.bitwise_or(combined_cv2_mask, cv2_mask)
else:
# do nothing - incompatible mask
pass
mask = torch.from_numpy(combined_cv2_mask)
return mask
@@ -43,7 +48,12 @@ def combine_masks2(masks):
for i in range(1, len(masks)):
cv2_mask = np.array(masks[i]).astype(np.uint8)
combined_cv2_mask = cv2.bitwise_or(combined_cv2_mask, cv2_mask)
if combined_cv2_mask.shape == cv2_mask.shape:
combined_cv2_mask = cv2.bitwise_or(combined_cv2_mask, cv2_mask)
else:
# do nothing - incompatible mask
pass
mask = torch.from_numpy(combined_cv2_mask)
return mask
@@ -54,9 +64,13 @@ def bitwise_and_masks(mask1, mask2):
mask2 = mask2.cpu()
cv2_mask1 = np.array(mask1)
cv2_mask2 = np.array(mask2)
cv2_mask = cv2.bitwise_and(cv2_mask1, cv2_mask2)
mask = torch.from_numpy(cv2_mask)
return mask
if cv2_mask1.shape == cv2_mask2.shape:
cv2_mask = cv2.bitwise_and(cv2_mask1, cv2_mask2)
return torch.from_numpy(cv2_mask)
else:
# do nothing - incompatible mask shape: mostly empty mask
return mask1
def to_binary_mask(mask):
@@ -102,9 +116,13 @@ def subtract_masks(mask1, mask2):
mask2 = mask2.cpu()
cv2_mask1 = np.array(mask1) * 255
cv2_mask2 = np.array(mask2) * 255
cv2_mask = cv2.subtract(cv2_mask1, cv2_mask2)
mask = torch.from_numpy(cv2_mask) / 255.0
return mask
if cv2_mask1.shape == cv2_mask2.shape:
cv2_mask = cv2.subtract(cv2_mask1, cv2_mask2)
return torch.from_numpy(cv2_mask) / 255.0
else:
# do nothing - incompatible mask shape: mostly empty mask
return mask1
def normalize_region(limit, startp, size):
File diff suppressed because it is too large Load Diff