diff --git a/__init__.py b/__init__.py index d80379d..9760949 100644 --- a/__init__.py +++ b/__init__.py @@ -6,11 +6,13 @@ from .nodes import * NODE_CLASS_MAPPINGS = { "BestResolutionFromArea": BestResolutionFromArea, + "BestResolutionFromAreaUpscale": BestResolutionFromAreaUpscale, "BestResolutionFromAspectRatio": BestResolutionFromAspectRatio, "BestResolutionSimple": BestResolutionSimple, } NODE_DISPLAY_NAME_MAPPINGS = { "BestResolutionFromArea": "Best-Res (area)", + "BestResolutionFromAreaUpscale": "Best-Res (area+scale)", "BestResolutionFromAspectRatio": "Best-Res (ratio)", "BestResolutionSimple": "Best-Res (simple)", } diff --git a/_funcs.py b/_funcs.py index 53704ee..1fe93c5 100644 --- a/_funcs.py +++ b/_funcs.py @@ -10,6 +10,8 @@ from math import sqrt as _sqrt from server import PromptServer as _PromptServer +from .enums import * + _t_number = _t.Union[int, float] @@ -120,7 +122,7 @@ def _format_report_square_part( return f"~={actual_square_side_f:.2f}🔳" -def format_report( +def format_report_simple( width_f: float, height_f: _t_number, step: int, width: int, n_steps_x: int, height: int, n_steps_y: int, target_square_size: _t_number = None @@ -143,22 +145,100 @@ def format_report( def simple_result_from_approx_wh( - width_f: float, height_f: _t_number, step: int, show: bool = True, + width_f: float, height_f: _t_number, step: int, + show: bool = True, unique_id: str = None, target_square_size: _t_number = None ): - """Final part of the main func for simple (non-upscale) nodes - when desired float/height are already calculated""" + """Final part of the main func for simple (non-upscale) nodes - when desired width/height are already calculated.""" step = number_to_int(step) width, n_steps_x, height, n_steps_y = round_width_and_height_closest_to_the_ratio(width_f, height_f, step) - if unique_id: - text = ( - format_report(width_f, height_f, step, width, n_steps_x, height, n_steps_y, target_square_size) - if show - # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway - else '' # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed - ) - # print(f"{unique_id} text: {text!r}") - # Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582 - _PromptServer.instance.send_progress_text(text, unique_id) + result = (width, height) - return width, height + if not unique_id: + return result + + text = ( + format_report_simple(width_f, height_f, step, width, n_steps_x, height, n_steps_y, target_square_size) + if show + # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway + else '' # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed + ) + # print(f"{unique_id} text: {text!r}") + # Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582 + _PromptServer.instance.send_progress_text(text, unique_id) + + return result + + +def upscale_result_from_approx_wh( + width_f: float, height_f: _t_number, step: int, + priority: _t.Union[RoundingPriority, str], upscale: float, up_step:int, + show: bool = True, + unique_id: str = None, target_square_size: _t_number = None +): + """Primary part of the main func for nodes with upscaling - when desired initial-width/height are already calculated.""" + upscale = max(float(upscale), 1.0) + up_width_f: float = upscale * width_f + up_height_f: float = upscale * height_f + width_f = float(width_f) + + step = number_to_int(step) + up_step = number_to_int(up_step) + + if priority == RoundingPriority.DESIRED: + width, n_steps_x, height, n_steps_y = round_width_and_height_closest_to_the_ratio(width_f, height_f, step) + up_width, up_steps_x, up_height, up_steps_y = round_width_and_height_closest_to_the_ratio( + up_width_f, up_height_f, up_step + ) + elif priority == RoundingPriority.ORIGINAL: + width, n_steps_x, height, n_steps_y = round_width_and_height_closest_to_the_ratio(width_f, height_f, step) + up_width, up_steps_x, up_height, up_steps_y = round_width_and_height_closest_to_the_ratio( + upscale * width, upscale * height, up_step + ) + else: + up_width, up_steps_x, up_height, up_steps_y = round_width_and_height_closest_to_the_ratio( + up_width_f, up_height_f, up_step + ) + width, n_steps_x, height, n_steps_y = round_width_and_height_closest_to_the_ratio( + float(up_width) / upscale, float(up_height) / upscale, step + ) + + result = (width, height, up_width, up_height) + + if not unique_id: + return result + + # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway + text = '' # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed + if show: + reports: _t.List[str] = list() + for prefix, w_f, h_f, s, w, n_x, h, n_y, trg_sq in [ + ('◻️ ', width_f, height_f, step, width, n_steps_x, height, n_steps_y, target_square_size), + ('🔲 ', up_width_f, up_height_f, up_step, up_width, up_steps_x, up_height, up_steps_y, _sqrt(up_width_f * up_height_f)) + ]: + cur_report = format_report_simple(w_f, h_f, s, w, n_x, h, n_y, trg_sq) + reports.append('\n'.join( + f"{prefix}{x}" for x in cur_report.split('\n') + )) + real_upscale_x = float(up_width) / width + real_upscale_y = float(up_height) / height + + # Pixel size relative to the upscaled image: + up_half_pixel_x = 0.5 / up_width + up_half_pixel_y = 0.5 / up_height + # Threshold for upscale-multiplier to be considered the same res: + almost_equal_rel_delta_x = real_upscale_x * up_half_pixel_x + almost_equal_rel_delta_y = real_upscale_y * up_half_pixel_y + almost_equal_rel_delta = max(almost_equal_rel_delta_x, almost_equal_rel_delta_y) + if abs(real_upscale_x - real_upscale_y) < almost_equal_rel_delta: + # The x/y delta between upscale multipliers is below a threshold to contribute even 1 pixel. + # So, basically, the upscale is uniform: + divider_line = f"\n--- x{real_upscale_x:.3f} ---\n" + else: + divider_line = f"\n--- ⚠️ x{real_upscale_x:.3f} / x{real_upscale_y:.3f} ---\n" + text = divider_line.join(reports) + # Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582 + _PromptServer.instance.send_progress_text(text, unique_id) + + return result diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..3fb70af --- /dev/null +++ b/enums.py @@ -0,0 +1,18 @@ +# encoding: utf-8 +""" +Internal enums. +""" + +import typing as _t + +from enum import StrEnum as _StrEnum + + +class RoundingPriority(_StrEnum): + DESIRED = 'desired' + ORIGINAL = 'original' + UPSCALED = 'upscaled' + + @classmethod + def all_values(cls) -> _t.Tuple[str, ...]: + return tuple(x.value for x in cls) diff --git a/nodes.py b/nodes.py index 9641d92..8a0dfba 100644 --- a/nodes.py +++ b/nodes.py @@ -9,16 +9,18 @@ from inspect import cleandoc as _cleandoc from math import sqrt as _sqrt import sys as _sys -from frozendict import deepfreeze +from frozendict import deepfreeze, frozendict from comfy.comfy_types.node_typing import IO as _IO from .node_types import * +from .enums import * from ._funcs import ( aspect_ratios_sorted as _aspect_ratios_sorted, number_to_int as _number_to_int, + float_width_height_from_area as _float_width_height_from_area, simple_result_from_approx_wh as _simple_result_from_approx_wh, - float_width_height_from_area as _float_width_height_from_area + upscale_result_from_approx_wh as _upscale_result_from_approx_wh ) @@ -235,3 +237,99 @@ class BestResolutionFromArea: # show, unique_id=unique_id, target_square_size=square_size ) + + +_return_types_upscale = (_IO.INT, _IO.INT, _IO.INT, _IO.INT) +_return_ttips_upscale = frozendict({ + 'orig_w': "Width for original image", + 'orig_h': "Height for original image", + 'up_w': "Width for upscaled image", + 'up_h': "Height for upscaled image" +}) +_return_names_upscale = tuple(_return_ttips_upscale.keys()) + + + + + +# __priority_type = (_IO.BOOLEAN, { +# 'default': True, 'label_on': 'upscaled', 'label_off': 'original', +# 'tooltip': ( +# "Which of the resolutions is more important to be as close to the desired as possible:\n\n" +# "When ON, the rounded with/height are first calculated for the UPSCALED resolution - and " +# "only then the original ones are back-tracked from them.\n" +# "When OFF, they're first calculated for the ORIGINAL resolution - and " +# "the upscaled ones are tracked forward from them." +# ) +# }) +__priority_type = ( + RoundingPriority.all_values(), + { + 'default': RoundingPriority.DESIRED.value, + 'tooltip': ( + "Defines what resolution to prioritize, as well as which order to perform rounding in:\n\n" + "• desired - first, approximate resolutions are calculated for both initial and upscaled size; " + "then, both are rounded. In both cases, the rounded resolution is closest to the desired one, " + "but aspect ratio might differ the most between sizes.\n\n" + "• original - first, initial resolution is calculated and rounded; then, upscaled one is detected from it. " + "Upscaled resolution might differ the most from the desired aspect ratio, but it follows the ratio from " + "initial size as much as possible.\n\n" + "• upscaled - vice versa: first, the rounded upscaled resolution is calculated; then, the initial one " + "back-tracked from it." + ) + } +) +__extra_inputs_for_upscale_only = { + 'priority': __priority_type, + 'upscale': (_IO.FLOAT, { + 'default': 1.5, 'min': 1.0, 'max': _sys.float_info.max, 'step': 0.25, 'round': 0.001, + # 'tooltip': "", # TODO + }), + 'up_step': (_IO.INT, dict(type_dict_step_upscale1, **{ + 'tooltip': "Same as the main `step`, but for the upscaled resolution.", + })), +} + +_input_types_area_upscale = deepfreeze({ + 'required': dict( + (k_v for k_v in _input_types_area['required'].items() if k_v[0] != 'show'), + **__extra_inputs_for_upscale_only, + # show=_input_types_area['required']['show'], + ), + 'hidden': { + 'unique_id': 'UNIQUE_ID', + }, + # 'optional': {}, +}) + + +class BestResolutionFromAreaUpscale: + NODE_NAME = 'BestResolutionFromAreaUpscale' + CATEGORY = "utils/resolution" + DESCRIPTION = _cleandoc(__doc__) + + OUTPUT_NODE = True + + FUNCTION = 'main' + RETURN_TYPES = _return_types_upscale + RETURN_NAMES = _return_names_upscale + RETURN_TYPES_TOOLTIPS = _return_ttips_upscale + + @classmethod + def INPUT_TYPES(cls): + return _input_types_area_upscale + + def main( + self, square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, + priority: _t.Union[RoundingPriority, str], upscale: float, up_step:int, + # show: bool, + unique_id: str = None + ): + square_size: int = _number_to_int(square_size) + width_f, height_f = _float_width_height_from_area(square_size, landscape, aspect_a, aspect_b) + return _upscale_result_from_approx_wh( + width_f, height_f, step, + priority, upscale, up_step, + # show, + unique_id=unique_id, target_square_size=square_size + )