From 6e0771918afc220bd1fbbaff71e7e7144d24e7ae Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:01:47 +0500 Subject: [PATCH] `ImageUpscaleByWithModel` --- CHANGELOG.md | 4 ++ __init__.py | 5 ++ node_upscale_by.py | 124 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 node_upscale_by.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f5d91..19857ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Add options for `crop/pad` node - to determine the order of [crop/pad] -> upscale -> [crop/pad]. - Add node for auto-detecting USDU-padding from initial res. +# v1.1.3 + +- New node: `Upscale Image By (with Model)` + # v1.1.2 - New node: `Scale (Best-Res)` diff --git a/__init__.py b/__init__.py index 05c566b..ab64279 100644 --- a/__init__.py +++ b/__init__.py @@ -6,6 +6,7 @@ import typing as _t from .node_crop_pad import BestResolutionUpscaledCropPad from .node_scale import BestResolutionScale +from .node_upscale_by import ImageUpscaleByWithModel from .nodes_prims import * from .nodes_simple import * from .nodes_upscale import * @@ -22,6 +23,8 @@ NODE_CLASS_MAPPINGS: _t.Dict[str, type] = { "BestResolutionPrimResPriority": BestResolutionPrimResPriority, "BestResolutionUpscaledCropPad": BestResolutionUpscaledCropPad, + + "ImageUpscaleByWithModel": ImageUpscaleByWithModel, } NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = { "BestResolutionFromArea": "Best-Res (area)", @@ -35,6 +38,8 @@ NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = { "BestResolutionPrimResPriority": "Priority (Best-Res)", "BestResolutionUpscaledCropPad": "Upscaled Crop/Pad (Best-Res)", + + "ImageUpscaleByWithModel": "Upscale Image By (with Model)" } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/node_upscale_by.py b/node_upscale_by.py new file mode 100644 index 0000000..aae43d2 --- /dev/null +++ b/node_upscale_by.py @@ -0,0 +1,124 @@ +# encoding: utf-8 +""" +""" + +import typing as _t + +from inspect import cleandoc as _cleandoc + +from frozendict import deepfreeze as _deepfreeze + +from comfy.comfy_types.node_typing import IO as _IO +from comfy_extras.nodes_upscale_model import ImageUpscaleWithModel as _ImageUpscaleWithModel +from nodes import ImageScaleBy as _ImageScaleBy + +from . import _meta +from .docstring_formatter import format_docstring as _format_docstring +from .node_scale import _scale_type_dict as __scale_type_dict_base +from ._funcs import _show_text_on_node + + +_ImageUpscaleWithModel_instance = _ImageUpscaleWithModel() +__ImageUpscaleWithModel_input_types: _t.Dict[str, dict] = _ImageUpscaleWithModel_instance.INPUT_TYPES() +__ImageUpscaleWithModel_input_types_required = __ImageUpscaleWithModel_input_types.get('required', dict()) + +_ImageScaleBy_instance = _ImageScaleBy() + +__scale_type_dict: _t.Dict[str, _t.Any] = dict(__scale_type_dict_base, round=0.00001) + +_input_types = _deepfreeze({ + 'required': { + 'upscale_model': __ImageUpscaleWithModel_input_types_required.get('upscale_model', (_IO.UPSCALE_MODEL, )), + 'image': __ImageUpscaleWithModel_input_types_required.get('image', (_IO.IMAGE, )), + 'model_scale': ( + _IO.FLOAT, + dict(__scale_type_dict, default=2.0, tooltip='The upscale factor a model natively increases image by'), + ), + 'scale_method': (_ImageScaleBy.upscale_methods, {'default': 'bicubic'}), + 'scale': ( + _IO.FLOAT, + dict(__scale_type_dict, default=1.5, tooltip='The actual factor you want to upscale by'), + ), + 'show_status': ( + _IO.BOOLEAN, + { + 'default': False, 'label_on': 'scales performed', 'label_off': 'no', + 'tooltip': "Show the extracted string on the node itself?" + }, + ), + }, + # 'optional': {}, + 'hidden': { + 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node + }, +}) + +# Threshold for "almost equal to 1.0". +# Supposedly, no image would go beyond 250k pixels in either side (even polygraphy), +# and the double of that equals change of less than a half-pixel. +_epsilon = 1.0 / 500_000 +_half_upper_threshold = 0.5 + _epsilon * 0.5 + + +def _status_message(no_model_scale: bool, do_downscale: bool, model_scale: float, scale: float, second_downscale: float): + if no_model_scale: + return f"🤔❔ x{scale:.3f} (no model)" + if not do_downscale: + return f"x{scale:.3f} (no second scale)" + + base_msg = f"x{scale:.3f} = x{model_scale:.3f} → x{second_downscale:.3f}" + if scale > model_scale: + return f"‼️ VERY blurry output\n{base_msg}" + if scale > (model_scale - 0.5 + _epsilon): + return f"⚠️ Blurry output\n{base_msg}" + return base_msg + + +class ImageUpscaleByWithModel: + """ + A simple wrapper over "Upscale Image (using Model)" and "Upscale Image By". + + First, up-scales with model. Then, (down)scales to get to the desired scale factor. + """ + NODE_NAME = 'ImageUpscaleByWithModel' + CATEGORY = _meta.category + DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + + FUNCTION = 'main' + RETURN_TYPES = (_IO.IMAGE, ) + RETURN_NAMES = ('image', ) + # OUTPUT_TOOLTIPS = tuple() + + @classmethod + def INPUT_TYPES(cls): + return _input_types + + @staticmethod + def main( + upscale_model, image, model_scale: float, scale_method, scale: float, + show_status: bool = False, + unique_id: str = None, + ) -> _t.Tuple[str]: + """ + A simple wrapper over ``ImageUpscaleWithModel`` and ``ImageScaleBy``. + + First, up-scales with model. Then, (down)scales to get to the desired scale factor. + """ + second_downscale = scale / model_scale + do_downscale = abs(second_downscale - 1.0) > _epsilon + no_model_scale = ( + scale <= _half_upper_threshold + or abs(model_scale - 1.0) <= _epsilon + ) + if no_model_scale: + out_image = _ImageScaleBy_instance.upscale(image, scale_method, scale) + else: + out_image = _ImageUpscaleWithModel_instance.upscale(upscale_model, image)[0] + if do_downscale: + out_image = _ImageScaleBy_instance.upscale(out_image, scale_method, second_downscale) + + if show_status and unique_id: + msg = _status_message(no_model_scale, do_downscale, model_scale, scale, second_downscale) + _show_text_on_node(msg, unique_id) + + return (out_image, )