From 8afba2e6546d96fc06ea586f127a6019d5d7967f Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Thu, 1 Feb 2024 16:54:18 +0900 Subject: [PATCH] feat: PreviewDetailerHook --- README.md | 3 +- __init__.py | 1 + js/comboBoolMigration.js | 16 +++++----- js/common.js | 13 ++++++++ js/impact-image-util.js | 48 ++++++++++++++--------------- modules/impact/animatediff_nodes.py | 4 +++ modules/impact/config.py | 2 +- modules/impact/hook_nodes.py | 18 +++++++++++ modules/impact/hooks.py | 40 +++++++++++++++++++++++- modules/impact/impact_pack.py | 3 ++ 10 files changed, 113 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f5e2a4c..029cb2f 100644 --- a/README.md +++ b/README.md @@ -136,11 +136,12 @@ This custom node helps to conveniently enhance images through Detector, Detailer * DETAILER_HOOK * NoiseInjectionDetailerHookProvider - The `detailer_hook` is a hook in the `Detailer` that injects noise during the processing of each SEGS. * UnsamplerDetailerHookProvider - Apply Unsampler during each cycle. To use this node, ComfyUI_Noise must be installed. - * There is a bug in applying the noise mask to the current Unsampler, so this [ComfyUI_Noise/PR-13](https://github.com/BlenderNeko/ComfyUI_Noise/pull/13) must be applied for it to be usable. * DenoiseSchedulerDetailerHookProvider - During the progress of the cycle, the detailer's denoise is altered up to the `target_denoise`. * CoreMLDetailerHookProvider - CoreML supports only 512x512, 512x768, 768x512, 768x768 size sampling. CoreMLDetailerHookProvider precisely fixes the upscale of the crop_region to this size. When using this hook, it will always be selected size, regardless of the guide_size. However, if the guide_size is too small, skipping will occur. * DetailerHookCombine - This is used to connect two DETAILER_HOOKs. Similar to PixelKSampleHookCombine. * SEGSOrderedFilterDetailerHook, SEGSRangeFilterDetailerHook, SEGSLabelFilterDetailerHook - There are a wrapper node that provides SEGSFilter nodes to be applied in FaceDetailer or Detector by creating DETAILER_HOOK. + * PreviewDetailerHOok - Connecting this hook node helps provide assistance for viewing previews whenever SEGS Detailing tasks are completed. When working with a large number of SEGS, such as Make Tile SEGS, it allows for monitoring the situation as improvements progress incrementally. + * Since this is the hook applied when pasting onto the original image, it has no effect on nodes like `SEGSDetailer`. * Iterative Upscale (Latent/on Pixel Space) - The upscaler takes the input upscaler and splits the scale_factor into steps, then iteratively performs upscaling. This takes latent as input and outputs latent as the result. diff --git a/__init__.py b/__init__.py index 4750695..5a55f04 100644 --- a/__init__.py +++ b/__init__.py @@ -180,6 +180,7 @@ NODE_CLASS_MAPPINGS = { "NoiseInjectionHookProvider": NoiseInjectionHookProvider, "UnsamplerHookProvider": UnsamplerHookProvider, "CoreMLDetailerHookProvider": CoreMLDetailerHookProvider, + "PreviewDetailerHookProvider": PreviewDetailerHookProvider, "DetailerHookCombine": DetailerHookCombine, "NoiseInjectionDetailerHookProvider": NoiseInjectionDetailerHookProvider, diff --git a/js/comboBoolMigration.js b/js/comboBoolMigration.js index d12313e..fa55216 100644 --- a/js/comboBoolMigration.js +++ b/js/comboBoolMigration.js @@ -9,18 +9,18 @@ app.registerExtension({ for(let i in node.widgets) { let widget = node.widgets[i]; - if(conflict_check == undefined) { - conflict_check = !!app.extensions.find((ext) => ext.name === "Comfy.comboBoolMigration"); - } + if(conflict_check == undefined) { + conflict_check = !!app.extensions.find((ext) => ext.name === "Comfy.comboBoolMigration"); + } - if(conflict_check) - return; + if(conflict_check) + return; if(widget.type == "toggle") { - let value = widget.value; + let value = widget.value; - var v = Object.getOwnPropertyDescriptor(widget, 'value'); - if(!v) { + var v = Object.getOwnPropertyDescriptor(widget, 'value'); + if(!v) { Object.defineProperty(widget, "value", { set: (value) => { delete widget.value; diff --git a/js/common.js b/js/common.js index 3b7dc3e..b60f6c3 100644 --- a/js/common.js +++ b/js/common.js @@ -80,3 +80,16 @@ function addQueue(event) { } api.addEventListener("impact-add-queue", addQueue); + + +function refreshPreview(event) { + let node_id = event.detail.node_id; + let item = event.detail.item; + let img = new Image(); + img.src = `/view?filename=${item.filename}&subfolder=${item.subfolder}&type=${item.type}&no-cache=${Date.now()}`; + let node = app.graph._nodes_by_id[node_id]; + if(node) + node.imgs = [img]; +} + +api.addEventListener("impact-preview", refreshPreview); diff --git a/js/impact-image-util.js b/js/impact-image-util.js index 55c1fb7..216cd16 100644 --- a/js/impact-image-util.js +++ b/js/impact-image-util.js @@ -9,31 +9,31 @@ function load_image(str) { function getFileItem(baseType, path) { try { - let pathType = baseType; + let pathType = baseType; - if (path.endsWith("[output]")) { - pathType = "output"; - path = path.slice(0, -9); - } else if (path.endsWith("[input]")) { - pathType = "input"; - path = path.slice(0, -8); - } else if (path.endsWith("[temp]")) { - pathType = "temp"; - path = path.slice(0, -7); - } + if (path.endsWith("[output]")) { + pathType = "output"; + path = path.slice(0, -9); + } else if (path.endsWith("[input]")) { + pathType = "input"; + path = path.slice(0, -8); + } else if (path.endsWith("[temp]")) { + pathType = "temp"; + path = path.slice(0, -7); + } - const subfolder = path.substring(0, path.lastIndexOf('/')); - const filename = path.substring(path.lastIndexOf('/') + 1); + const subfolder = path.substring(0, path.lastIndexOf('/')); + const filename = path.substring(path.lastIndexOf('/') + 1); - return { - filename: filename, - subfolder: subfolder, - type: pathType - }; - } - catch(exception) { - return null; - } + return { + filename: filename, + subfolder: subfolder, + type: pathType + }; + } + catch(exception) { + return null; + } } async function loadImageFromUrl(image, node_id, v, need_to_load) { @@ -225,5 +225,5 @@ app.registerExtension({ } }); } - } -}) \ No newline at end of file + } +}) diff --git a/modules/impact/animatediff_nodes.py b/modules/impact/animatediff_nodes.py index c26228b..e017502 100644 --- a/modules/impact/animatediff_nodes.py +++ b/modules/impact/animatediff_nodes.py @@ -139,6 +139,10 @@ class DetailerForEachPipeForAnimateDiff: denoise, basic_pipe, refiner_ratio, refiner_basic_pipe_opt, inpaint_model, noise_mask_feather) image_frames = SEGSPaste.doit(image_frames, enhanced_seg, feather, alpha=255)[0] + + if detailer_hook is not None: + detailer_hook.post_paste(image_frames) + enhanced_segs += enhanced_seg[1] new_segs = segs[0], enhanced_segs diff --git a/modules/impact/config.py b/modules/impact/config.py index fad9864..5486a0a 100644 --- a/modules/impact/config.py +++ b/modules/impact/config.py @@ -2,7 +2,7 @@ import configparser import os -version_code = [4, 71] +version_code = [4, 72] version = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') dependency_version = 20 diff --git a/modules/impact/hook_nodes.py b/modules/impact/hook_nodes.py index d67cd95..c218da4 100644 --- a/modules/impact/hook_nodes.py +++ b/modules/impact/hook_nodes.py @@ -63,3 +63,21 @@ class SEGSLabelFilterDetailerHookProvider: def doit(self, preset, labels): hook = hooks.SEGSLabelFilterDetailerHook(labels) return (hook, ) + + +class PreviewDetailerHookProvider: + @classmethod + def INPUT_TYPES(s): + return { + "required": {"quality": ("INT", {"default": 95, "min": 20, "max": 100})}, + "hidden": {"unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("DETAILER_HOOK", ) + FUNCTION = "doit" + + CATEGORY = "ImpactPack/Util" + + def doit(self, quality, unique_id): + hook = hooks.PreviewDetailerHook(unique_id, quality) + return (hook, ) diff --git a/modules/impact/hooks.py b/modules/impact/hooks.py index b3528d3..0eab81e 100644 --- a/modules/impact/hooks.py +++ b/modules/impact/hooks.py @@ -4,7 +4,10 @@ import nodes from impact import utils from . import segs_nodes from thirdparty import noise_nodes - +from server import PromptServer +import asyncio +import folder_paths +import os class PixelKSampleHook: cur_step = 0 @@ -129,6 +132,9 @@ class DetailerHook(PixelKSampleHook): def post_detection(self, segs): return segs + def post_paste(self, image): + return image + class SimpleDetailerDenoiseSchedulerHook(DetailerHook): def __init__(self, target_denoise): @@ -405,3 +411,35 @@ class SEGSLabelFilterDetailerHook(DetailerHook): def post_detection(self, segs): return segs_nodes.SEGSLabelFilter().doit(segs, "", self.labels)[0] + + +class PreviewDetailerHook(DetailerHook): + def __init__(self, node_id, quality): + super().__init__() + self.node_id = node_id + self.quality = quality + + async def send(self, image): + if len(image) > 0: + image = image[0].unsqueeze(0) + img = utils.tensor2pil(image) + + temp_path = os.path.join(folder_paths.get_temp_directory(), 'pvhook') + + if not os.path.exists(temp_path): + os.makedirs(temp_path) + + fullpath = os.path.join(temp_path, f"{self.node_id}.webp") + img.save(fullpath, quality=self.quality) + + item = { + "filename": f"{self.node_id}.webp", + "subfolder": 'pvhook', + "type": 'temp' + } + + PromptServer.instance.send_sync("impact-preview", {'node_id': self.node_id, 'item': item}) + + def post_paste(self, image): + asyncio.run(self.send(image)) + return image diff --git a/modules/impact/impact_pack.py b/modules/impact/impact_pack.py index 7d3c613..6f8b520 100644 --- a/modules/impact/impact_pack.py +++ b/modules/impact/impact_pack.py @@ -265,6 +265,9 @@ class DetailerForEach: tensor_paste(image, enhanced_image, (seg.crop_region[0], seg.crop_region[1]), mask) enhanced_list.append(enhanced_image) + if detailer_hook is not None: + detailer_hook.post_paste(image) + if not (enhanced_image is None): # Convert enhanced_pil_alpha to RGBA mode enhanced_image_alpha = tensor_convert_rgba(enhanced_image)