From 65f1363a103b20d3b0f7104326fdc327b7c2612e Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 17 Jun 2025 23:40:50 +0900 Subject: [PATCH] feat: LamaRemoverDetailerHookProvider is added --- README.md | 5 ++ __init__.py | 1 + modules/impact/config.py | 2 +- modules/impact/core.py | 107 +++++++++++++++++++---------------- modules/impact/hook_nodes.py | 23 ++++++++ modules/impact/hooks.py | 35 ++++++++++-- pyproject.toml | 2 +- 7 files changed, 119 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 6284753..1c5b1a9 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,9 @@ NOTE: The UltralyticsDetectorProvider node is not part of the ComfyUI-Impact-Pac * Since this is the hook applied when pasting onto the original image, it has no effect on nodes like `SEGSDetailer`. * `VariationNoiseDetailerHookProvider` - Apply variation seed to the detailer. It can be applied in multiple stages through combine. * `CustomSamplerDetailerHookProvider` - Apply a hook that allows you to use a custom sampler in the Detailer nodes. When using `DetailerHookCombine`, the sampler from the first hook is applied. + * `LamaRemoverDetailerHookProvider` – Applies Lama Remover to the upscaled image during the detailing stage. If `skip_sampling` is set to True, Lama Remover can be used alone without the detailing stage, allowing it to simply remove detected regions. + * Not applicable for **AnimateDiff** detailers. When using `DetailerHookCombine`, `skip_sampling` is only applied if it is set to `True` for all hooks. + * To use this node, the node pack at [Layer-norm/comfyui-lama-remover](https://github.com/Layer-norm/comfyui-lama-remover) must be installed. ### Iterative Upscale nodes * `Iterative Upscale (Latent/on Pixel Space)` - The upscaler takes the input upscaler and splits the scale_factor into steps, then iteratively performs upscaling. @@ -489,3 +492,5 @@ BlenderNeok/[ComfyUI_Noise](https://github.com/BlenderNeko/ComfyUI_Noise) - The WASasquatch/[was-node-suite-comfyui](https://github.com/WASasquatch/was-node-suite-comfyui) - A powerful custom node extensions of ComfyUI. Trung0246/[ComfyUI-0246](https://github.com/Trung0246/ComfyUI-0246) - Nice bypass hack! + +Layer-norm/[comfyui-lama-remover](https://github.com/Layer-norm/comfyui-lama-remover) - Required for using `LamaRemoverDetailerHook`. diff --git a/__init__.py b/__init__.py index 8f577c0..7e31379 100644 --- a/__init__.py +++ b/__init__.py @@ -123,6 +123,7 @@ NODE_CLASS_MAPPINGS = { "CoreMLDetailerHookProvider": CoreMLDetailerHookProvider, "PreviewDetailerHookProvider": PreviewDetailerHookProvider, "CustomSamplerDetailerHookProvider": CustomSamplerDetailerHookProvider, + "LamaRemoverDetailerHookProvider": LamaRemoverDetailerHookProvider, "DetailerHookCombine": DetailerHookCombine, "NoiseInjectionDetailerHookProvider": NoiseInjectionDetailerHookProvider, diff --git a/modules/impact/config.py b/modules/impact/config.py index ecd26e5..394360d 100644 --- a/modules/impact/config.py +++ b/modules/impact/config.py @@ -1,7 +1,7 @@ import configparser import os -version_code = [8, 16, 1] +version_code = [8, 17] version = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') dependency_version = 24 diff --git a/modules/impact/core.py b/modules/impact/core.py index 0f9469e..140f9ee 100644 --- a/modules/impact/core.py +++ b/modules/impact/core.py @@ -320,6 +320,9 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max # upscale upscaled_image = 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) @@ -327,65 +330,69 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max cnet_pils.extend(cnet_pils2) # prepare mask - 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) + 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: + print(f"[Impact Pack] ComfyUI is an outdated version.") + positive, negative, latent_image = imc_encode(positive, negative, upscaled_image, vae, noise_mask) else: - print(f"[Impact Pack] ComfyUI is an outdated version.") - positive, negative, latent_image = imc_encode(positive, negative, upscaled_image, vae, noise_mask) - else: - latent_image = to_latent_image(upscaled_image, vae, vae_tiled_encode=vae_tiled_encode) - if noise_mask is not None: - latent_image['noise_mask'] = noise_mask + latent_image = 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: + 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: - detailer_hook.set_steps((i, cycle)) + if detailer_hook is not None: + detailer_hook.set_steps((i, cycle)) - refined_latent = detailer_hook.cycle_latent(refined_latent) + 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: + 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, upscaled_latent2, 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 + print(f"[Impact Pack] vae decoded (tiled) in {time.time() - start:.1f}s") else: - model2, seed2, steps2, cfg2, sampler_name2, scheduler2, positive2, negative2, upscaled_latent2, 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 - print(f"[Impact Pack] vae decoded (tiled) in {time.time() - start:.1f}s") + try: + refined_image = vae.decode(refined_latent['samples']) + except Exception as e: + # usually an out-of-memory exception from the decode, so try a tiled approach + print(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, ) + print(f"[Impact Pack] vae decoded in {time.time() - start:.1f}s") else: - try: - refined_image = vae.decode(refined_latent['samples']) - except Exception as e: - # usually an out-of-memory exception from the decode, so try a tiled approach - print(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, ) - print(f"[Impact Pack] vae decoded in {time.time() - start:.1f}s") + # skipped + refined_image = upscaled_image if detailer_hook is not None: refined_image = detailer_hook.post_decode(refined_image) diff --git a/modules/impact/hook_nodes.py b/modules/impact/hook_nodes.py index ff97fa1..483cfe5 100644 --- a/modules/impact/hook_nodes.py +++ b/modules/impact/hook_nodes.py @@ -1,6 +1,8 @@ import sys from . import hooks from . import defs +from . import utils +import nodes class SEGSOrderedFilterDetailerHookProvider: @@ -83,3 +85,24 @@ class PreviewDetailerHookProvider: def doit(self, quality, unique_id): hook = hooks.PreviewDetailerHook(unique_id, quality) return hook, hook + + +class LamaRemoverDetailerHookProvider: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask_threshold":("INT", {"default": 250, "min": 0, "max": 255, "step": 1, "display": "slider"}), + "gaussblur_radius": ("INT", {"default": 8, "min": 0, "max": 20, "step": 1, "display": "slider"}), + "skip_sampling": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("DETAILER_HOOK", ) + FUNCTION = "doit" + + CATEGORY = "ImpactPack/Util" + + def doit(self, mask_threshold, gaussblur_radius, skip_sampling): + hook = hooks.LamaRemoverDetailerHook(mask_threshold, gaussblur_radius, skip_sampling) + return (hook, ) diff --git a/modules/impact/hooks.py b/modules/impact/hooks.py index b150dde..d7989be 100644 --- a/modules/impact/hooks.py +++ b/modules/impact/hooks.py @@ -25,7 +25,7 @@ class PixelKSampleHook: def post_decode(self, pixels): return pixels - def post_upscale(self, pixels): + def post_upscale(self, pixels, mask=None): return pixels def post_encode(self, samples): @@ -64,8 +64,8 @@ class PixelKSampleHookCombine(PixelKSampleHook): def post_decode(self, pixels): return self.hook2.post_decode(self.hook1.post_decode(pixels)) - def post_upscale(self, pixels): - return self.hook2.post_upscale(self.hook1.post_upscale(pixels)) + def post_upscale(self, pixels, mask=None): + return self.hook2.post_upscale(self.hook1.post_upscale(pixels, mask), mask) def post_encode(self, samples): return self.hook2.post_encode(self.hook1.post_encode(samples)) @@ -109,12 +109,15 @@ class DetailerHookCombine(PixelKSampleHookCombine): noise_2nd, is_touched = self.hook2.get_custom_noise(seed, noise, is_touched) return noise, is_touched - def get_custom_sampler(): + def get_custom_sampler(self): if self.hook1.get_custom_sampler() is not None: return self.hook1.get_custom_sampler() else: return self.hook2.get_custom_sampler() + def get_skip_sampling(self): + return self.hook1.get_skip_sampling() and self.hook2.get_skip_sampling() + class SimpleCfgScheduleHook(PixelKSampleHook): target_cfg = 0 @@ -182,6 +185,9 @@ class DetailerHook(PixelKSampleHook): def get_custom_sampler(self): return None + def get_skip_sampling(self): + return False + class CustomSamplerDetailerHookProvider(DetailerHook): def __init__(self, sampler): @@ -504,6 +510,27 @@ class SEGSLabelFilterDetailerHook(DetailerHook): return segs_nodes.SEGSLabelFilter().doit(segs, "", self.labels)[0] +class LamaRemoverDetailerHook(DetailerHook): + def __init__(self, mask_threshold, gaussblur_radius, skip_sampling): + super().__init__() + self.mask_threshold = mask_threshold + self.gaussblur_radius = gaussblur_radius + self.skip_sampling = skip_sampling + + def post_upscale(self, img, mask=None): + if "LamaRemover" in nodes.NODE_CLASS_MAPPINGS: + lama_remover_obj = nodes.NODE_CLASS_MAPPINGS['LamaRemover']() + else: + utils.try_install_custom_node('https://github.com/Layer-norm/comfyui-lama-remover', + "To use 'LAMARemoverDetailerHookProvider', 'comfyui-lama-remover' nodepack is required.") + raise Exception("'LamaRemover' node is not installed.") + + return lama_remover_obj.lama_remover(img, masks=mask, mask_threshold=self.mask_threshold, gaussblur_radius=self.gaussblur_radius, invert_mask=False)[0] + + def get_skip_sampling(self): + return self.skip_sampling + + class PreviewDetailerHook(DetailerHook): def __init__(self, node_id, quality): super().__init__() diff --git a/pyproject.toml b/pyproject.toml index aaa2943..96c2d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-impact-pack" description = "This node pack offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler." -version = "8.16.1" +version = "8.17" license = { file = "LICENSE.txt" } dependencies = ["segment-anything", "scikit-image", "piexif", "transformers", "opencv-python-headless", "GitPython", "scipy>=1.11.4"]