diff --git a/_funcs.py b/_funcs.py index 1fe93c5..f442baf 100644 --- a/_funcs.py +++ b/_funcs.py @@ -92,6 +92,17 @@ def float_width_height_from_area(square_size: _t_number, landscape: bool, aspect return width_f, height_f +def _show_text_on_node(text: str = None, unique_id: str = None): + if not text: + # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway + # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed + text = '' + # 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) + + def _format_report_square_part( width_f: float, height_f: _t_number, width: int, height: int, @@ -161,16 +172,29 @@ def simple_result_from_approx_wh( 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 + else None ) - # 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) - + _show_text_on_node(text, unique_id) return result +def _need_post_resize(width: int, height: int, up_width: int, up_height: int): + """ + Detect whether the final init-res CAN NOT be uniformly scaled to the higher res, + and thus would require cropping/out-painting. + """ + real_upscale_x: float = float(up_width) / width + real_upscale_y: float = float(up_height) / height + real_upscale_avg: float = (real_upscale_x + real_upscale_y) * 0.5 + + needs_resize: bool = ( + round_pos_int(real_upscale_avg * width) != up_width or + round_pos_int(real_upscale_avg * height) != up_height + ) + return needs_resize, real_upscale_avg, real_upscale_x, real_upscale_y + + + 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, @@ -204,41 +228,36 @@ def upscale_result_from_approx_wh( float(up_width) / upscale, float(up_height) / upscale, step ) - result = (width, height, up_width, up_height) + needs_resize, real_upscale_avg, real_upscale_x, real_upscale_y = _need_post_resize(width, height, up_width, up_height) + out_upscale: float = upscale if needs_resize else real_upscale_avg + + result = (width, height, out_upscale, up_width, up_height, needs_resize) 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 + if not show: + _show_text_on_node(None, unique_id) + return result - # 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) + report_parts: _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) + report_parts.append('\n'.join( + f"{prefix}{x}" for x in cur_report.split('\n') + )) + separator_line: str = ( + # Real upscale factor in X and Y differ enough so we can't get the up-res from init-res with ANY uniform value: + f"\n--- ⚠️ x{real_upscale_x:.3f} / x{real_upscale_y:.3f} ---\n" + if needs_resize + # The upscale is uniform: + else f"\n--- x{real_upscale_x:.3f} ---\n" + ) + text = separator_line.join(report_parts) + + _show_text_on_node(text, unique_id) return result diff --git a/nodes_simple.py b/nodes_simple.py index af4a442..1418ae9 100644 --- a/nodes_simple.py +++ b/nodes_simple.py @@ -9,7 +9,7 @@ from inspect import cleandoc as _cleandoc from math import sqrt as _sqrt import sys as _sys -from frozendict import deepfreeze, frozendict +from frozendict import deepfreeze as _deepfreeze from comfy.comfy_types.node_typing import IO as _IO @@ -17,16 +17,19 @@ 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, - upscale_result_from_approx_wh as _upscale_result_from_approx_wh + simple_result_from_approx_wh as _simple_result_from_approx_wh ) -from .enums import * from .node_types import * # ---------------------------------------------------------- +# _return_types_simple = (_IO.INT, _IO.INT, _IO.STRING) +# _return_names_simple = ('width', 'height', 'report') +_return_types_simple = (_IO.INT, _IO.INT) +_return_names_simple = ('width', 'height') + # A tiny optimization by reusing the same immutable dict: -_input_types_simple = deepfreeze({ +_input_types_simple = _deepfreeze({ 'required': { 'width': (_IO.INT, dict(type_dict_res, **{'tooltip': "Approximate width"})), 'height': (_IO.INT, dict(type_dict_res, **{'tooltip': "Approximate height"})), @@ -52,11 +55,6 @@ _input_types_simple = deepfreeze({ # 'optional': {}, }) -# _return_types_simple = (_IO.INT, _IO.INT, _IO.STRING) -# _return_names_simple = ('width', 'height', 'report') -_return_types_simple = (_IO.INT, _IO.INT) -_return_names_simple = ('width', 'height') - class BestResolutionSimple: """ @@ -71,7 +69,7 @@ class BestResolutionSimple: FUNCTION = 'main' RETURN_TYPES = _return_types_simple - # RETURN_TYPES_TOOLTIPS = {} + # OUTPUT_TOOLTIPS = {} RETURN_NAMES = _return_names_simple @classmethod @@ -97,7 +95,7 @@ _tooltip_aspect = ( "The specified aspect ratio is APPROXIMATE: step parameter has priority over the exact image proportions." ) -_input_types_orient = deepfreeze({ +_input_types_orient = _deepfreeze({ 'required': { 'size': (_IO.INT, dict(type_dict_res, **{'tooltip': ( "Approximate size of one of the image sides.\nWhich one - see the 'size_is_big' and 'landscape' toggles." @@ -177,7 +175,7 @@ class BestResolutionFromAspectRatio: # ---------------------------------------------------------- -_input_types_area = deepfreeze({ +_input_types_area = _deepfreeze({ 'required': { 'square_size': (_IO.INT, dict(type_dict_res, **{'tooltip': ( "The total resolution of the image would be the same as of a square with this side.\n" diff --git a/nodes_upscale.py b/nodes_upscale.py index 0c03198..28a4b69 100644 --- a/nodes_upscale.py +++ b/nodes_upscale.py @@ -6,10 +6,9 @@ Advanced versions of nodes - with upscale. import typing as _t from inspect import cleandoc as _cleandoc -from math import sqrt as _sqrt import sys as _sys -from frozendict import deepfreeze, frozendict +from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict from comfy.comfy_types.node_typing import IO as _IO @@ -17,7 +16,6 @@ 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, upscale_result_from_approx_wh as _upscale_result_from_approx_wh ) from .enums import * @@ -26,18 +24,24 @@ from .nodes_simple import _input_types_area # ---------------------------------------------------------- -_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_types_upscale = (_IO.INT, _IO.INT, _IO.FLOAT, _IO.INT, _IO.INT, _IO.BOOLEAN) +_return_ttips_upscale = _frozendict({ + 'orig_w': "Width for original/initial image", + 'orig_h': "Height for original/initial image", + 'upscale': ( + "If 'needs_resize' is FALSE, this would be the actual uniform value to scale your initial-res in order " + "to get the upscaled-res.\n" + "If 'needs_resize' is TRUE, this slot simply outputs the same upscale-value you've set on the node." + ), + 'up_w': "Width for the (main) upscaled image", + 'up_h': "Height for the (main) upscaled image", + 'needs_resize': ( + "This will be FALSE if you can get the exact upscaled resolution by simply scaling the initial-res uniformly " + "(by the value output into 'upscale' slot).\n\n" + "Otherwise, it will be TRUE — indicating that you'll need to do some cropping/out-painting right AFTER " + "the actual upscale (increasing the resolution itself) but BEFORE post-upscale sampling (\"HD-fix\")." + ), }) -_return_names_upscale = tuple(_return_ttips_upscale.keys()) - - - - # __priority_type = (_IO.BOOLEAN, { # 'default': True, 'label_on': 'upscaled', 'label_off': 'original', @@ -77,7 +81,7 @@ __extra_inputs_for_upscale_only = { })), } -_input_types_area_upscale = deepfreeze({ +_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, @@ -91,6 +95,21 @@ _input_types_area_upscale = deepfreeze({ class BestResolutionFromAreaUpscale: + """ + The most efficient way of selecting an optimal resolution: + image size selected indirectly - by the total desired resolution (area) + aspect ratio... + + ... PLUS, account for the immediate upscale right away. + + Desired resolution (aka image area/megapixels/pixel count) is specified with a side of a square image. This isn't + accidental: most models disclose what image resolution they're trained on, and usually they're square: + + - SD 1.5 - 512x512 pixels + - SDXL - 1024x1024 pixels + + By simply providing this single number and setting your aspect ratio/orientation, you get the width and height to + produce the closest total resolution to the training set, while also respecting image proportions and step-rounding. + """ NODE_NAME = 'BestResolutionFromAreaUpscale' CATEGORY = "utils/resolution" DESCRIPTION = _cleandoc(__doc__) @@ -99,8 +118,8 @@ class BestResolutionFromAreaUpscale: FUNCTION = 'main' RETURN_TYPES = _return_types_upscale - RETURN_NAMES = _return_names_upscale - RETURN_TYPES_TOOLTIPS = _return_ttips_upscale + RETURN_NAMES = tuple(_return_ttips_upscale.keys()) + OUTPUT_TOOLTIPS = tuple(_return_ttips_upscale.values()) @classmethod def INPUT_TYPES(cls):