diff --git a/_funcs.py b/_funcs.py index e8498e5..53704ee 100644 --- a/_funcs.py +++ b/_funcs.py @@ -21,7 +21,7 @@ def aspect_ratios_sorted(aspect_a: float, aspect_b: float, min_clamp: float = 1. return (aspect_b, aspect_a) if aspect_b > aspect_a else (aspect_a, aspect_b) -def number_to_int(value: _t.Union[int, float], min: int = 1) -> int: +def number_to_int(value: _t_number, min: int = 1) -> int: if not isinstance(value, int): if isinstance(value, float): sign = -1 if value < 0.0 else 1 @@ -31,12 +31,17 @@ def number_to_int(value: _t.Union[int, float], min: int = 1) -> int: return max(value, min) -def round_abs(abs_value: _t_number, step: int): +def round_pos_int(value: float) -> int: + """Assuming a positive float is provided, rounds it to the nearest integer.""" + return int(value + 0.5) + + +def round_abs_to_step(abs_value: _t_number, step: int): """ Assuming both args are positive and ``step`` is already an int, detect the closest positive (non-zero) value which is also divisible by step. """ - n_steps = int(float(abs_value) / step + 0.5) + n_steps = round_pos_int(float(abs_value) / step) n_steps = max(n_steps, 1) return step * n_steps, n_steps @@ -46,12 +51,12 @@ def round_width_and_height_closest_to_the_ratio(width_f: _t_number, height_f: _t desired_width_to_height_ratio = float(width_f) / height_f # First pass: directly from width_f and height_f - width, n_steps_x = round_abs(width_f, step) - height, n_steps_y = round_abs(height_f, step) + width, n_steps_x = round_abs_to_step(width_f, step) + height, n_steps_y = round_abs_to_step(height_f, step) # Second pass: try calculating one side from already rounded another one: - height_from_width, n_steps_y_from_x = round_abs(float(width) / desired_width_to_height_ratio, step) - width_from_height, n_steps_x_from_y = round_abs(float(height) * desired_width_to_height_ratio, step) + height_from_width, n_steps_y_from_x = round_abs_to_step(float(width) / desired_width_to_height_ratio, step) + width_from_height, n_steps_x_from_y = round_abs_to_step(float(height) * desired_width_to_height_ratio, step) # ... and select one of three options, closest to the perfect ratio: closest_delta = abs((float(width) / height) - desired_width_to_height_ratio) @@ -68,6 +73,23 @@ def round_width_and_height_closest_to_the_ratio(width_f: _t_number, height_f: _t return width, n_steps_x, height, n_steps_y +def float_width_height_from_area(square_size: _t_number, landscape: bool, aspect_a: float, aspect_b: float): + """The main function for the regular (non-upscale) ``area``-subtype node.""" + # square_size = 1024; step = 48; landscape = True; aspect_a = 9.0; aspect_b = 16.0 + aspect_big, aspect_small = aspect_ratios_sorted(aspect_a, aspect_b) + aspect_x, aspect_y = (aspect_big, aspect_small) if landscape else (aspect_small, aspect_big) + + aspect_area = aspect_x * aspect_y + aspect_norm_scale = 1.0 / _sqrt(aspect_area) + aspect_x *= aspect_norm_scale + aspect_y *= aspect_norm_scale + # Now the two aspects produce a normalized rectangle - i.e., it's area is 1 + + width_f: float = aspect_x * square_size + height_f: float = aspect_y * square_size + return width_f, height_f + + def _format_report_square_part( width_f: float, height_f: _t_number, width: int, height: int, @@ -76,23 +98,23 @@ def _format_report_square_part( area_output = width * height if target_square_size is not None: target_square_size = max(abs(target_square_size), 1) - target_square_size_i = int(target_square_size + 0.5) + target_square_size_i = round_pos_int(target_square_size) if target_square_size_i * target_square_size_i == area_output: return f"=šŸ‘‘{target_square_size_i}šŸ”³" # return f"=šŸ’Æ{target_square_size_i}šŸ”³" # return f"=✨{target_square_size_i}šŸ”³" - if int(target_square_size * target_square_size + 0.5) == area_output: + if round_pos_int(target_square_size * target_square_size) == area_output: return f"=✨{target_square_size:.2f}šŸ”³" # return f"=🌟{target_square_size:.2f}šŸ”³" desired_square_area_f = float(width_f) * height_f desired_square_side_f = _sqrt(desired_square_area_f) - desired_square_side_i = int(desired_square_side_f + 0.5) + desired_square_side_i = round_pos_int(desired_square_side_f) if desired_square_side_i * desired_square_side_i == area_output: return f"=āœ…{desired_square_side_i}šŸ”³" actual_square_side_f = _sqrt(float(area_output)) - actual_square_side_i = int(actual_square_side_f + 0.5) + actual_square_side_i = round_pos_int(actual_square_side_f) if abs(actual_square_side_f - actual_square_side_i) < 0.005: return f"~={actual_square_side_i}šŸ”³" return f"~={actual_square_side_f:.2f}šŸ”³" @@ -121,7 +143,7 @@ def format_report( def simple_result_from_approx_wh( - width_f: float, height_f: _t_number, step: int, + 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""" @@ -129,7 +151,13 @@ def simple_result_from_approx_wh( 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) + 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) diff --git a/nodes.py b/nodes.py index 03661a2..9641d92 100644 --- a/nodes.py +++ b/nodes.py @@ -17,16 +17,17 @@ from .node_types import * from ._funcs import ( aspect_ratios_sorted as _aspect_ratios_sorted, number_to_int as _number_to_int, - simple_result_from_approx_wh as _simple_result_from_approx_wh + simple_result_from_approx_wh as _simple_result_from_approx_wh, + float_width_height_from_area as _float_width_height_from_area ) # Tiny optimization by reusing the same immutable dict: _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"})), - "step": (_IO.INT, dict(type_dict_step_init, **{"tooltip": ( + 'required': { + 'width': (_IO.INT, dict(type_dict_res, **{'tooltip': "Approximate width"})), + 'height': (_IO.INT, dict(type_dict_res, **{'tooltip': "Approximate height"})), + 'step': (_IO.INT, dict(type_dict_step_init, **{'tooltip': ( "Both width and height will be divisible by this value - by rounding them " "to the closest appropriate resolution.\n\n" "The default 48 is (8 * 3 * 2), so it's a safe choice because:\n" @@ -36,17 +37,22 @@ _input_types_simple = deepfreeze({ "by 3 AND 9, which might become handy at that point, where you'll probably use UltimateSDUpscale.\n\n" "Other values worth trying first: 64, 96, 128." )})), + # 'show': (_IO.BOOLEAN, { + # 'default': True, + # # 'label_on': 'on', 'label_off': 'off', + # 'tooltip': "Show the result on the node itself?", + # }), }, - "hidden": { - "unique_id": "UNIQUE_ID", # used for text display at the bottom of the node + 'hidden': { + 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node }, - # "optional": {}, + # 'optional': {}, }) # _return_types_simple = (_IO.INT, _IO.INT, _IO.STRING) -# _return_names_simple = ("width", "height", "report") +# _return_names_simple = ('width', 'height', 'report') _return_types_simple = (_IO.INT, _IO.INT) -_return_names_simple = ("width", "height") +_return_names_simple = ('width', 'height') class BestResolutionSimple: @@ -54,13 +60,13 @@ class BestResolutionSimple: The simplest node for easier selection of an optimal resolution: only rounding performed, desired image size specified directly. """ - NODE_NAME = "BestResolutionSimple" + NODE_NAME = 'BestResolutionSimple' CATEGORY = "utils/resolution" DESCRIPTION = _cleandoc(__doc__) OUTPUT_NODE = True - FUNCTION = "main" + FUNCTION = 'main' RETURN_TYPES = _return_types_simple # RETURN_TYPES_TOOLTIPS = {} RETURN_NAMES = _return_names_simple @@ -69,8 +75,16 @@ class BestResolutionSimple: def INPUT_TYPES(cls): return _input_types_simple - def main(self, width: int, height: int, step: int, unique_id: str = None): - return _simple_result_from_approx_wh(float(width), height, step, unique_id, _sqrt(width * height)) + def main( + self, width: int, height: int, step: int, + # show: bool, + unique_id: str = None + ): + return _simple_result_from_approx_wh( + float(width), height, step, + # show, + unique_id=unique_id, target_square_size=_sqrt(width * height) + ) _tooltip_aspect = ( @@ -80,36 +94,37 @@ _tooltip_aspect = ( ) _input_types_orient = deepfreeze({ - "required": { - "size": (_IO.INT, dict(type_dict_res, **{"tooltip": ( + '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." )})), - "step": _input_types_simple["required"]["step"], - "size_is_big": (_IO.BOOLEAN, { - "default": True, "label_on": "bigger", "label_off": "smaller", - "tooltip": "When ON, size parameter represents the bigger image side.\nWhen OFF, it specifies the smaller one." + 'step': _input_types_simple['required']['step'], + 'size_is_big': (_IO.BOOLEAN, { + 'default': True, 'label_on': 'bigger', 'label_off': 'smaller', + 'tooltip': "When ON, size parameter represents the bigger image side.\nWhen OFF, it specifies the smaller one." }), - "landscape": (_IO.BOOLEAN, { - "default": True, "label_on": "landscape", "label_off": "portrait", - "tooltip": ( + 'landscape': (_IO.BOOLEAN, { + 'default': True, 'label_on': 'landscape', 'label_off': 'portrait', + 'tooltip': ( "Specifies image orientation:\n\n" "When ON, width is bigger (image is horizontal).\n" "When OFF, height is bigger (image is vertical)." - ) + ), }), - "aspect_a": (_IO.FLOAT, { - "default": 16.0, "min": 1.0, "max": _sys.float_info.max, "step": 1.0, "round": 0.001, - "tooltip": _tooltip_aspect + 'aspect_a': (_IO.FLOAT, { + 'default': 16.0, 'min': 1.0, 'max': _sys.float_info.max, 'step': 1.0, 'round': 0.001, + 'tooltip': _tooltip_aspect }), - "aspect_b": (_IO.FLOAT, { - "default": 9.0, "min": 1.0, "max": _sys.float_info.max, "step": 1.0, "round": 0.001, - "tooltip": _tooltip_aspect + 'aspect_b': (_IO.FLOAT, { + 'default': 9.0, 'min': 1.0, 'max': _sys.float_info.max, 'step': 1.0, 'round': 0.001, + 'tooltip': _tooltip_aspect }), + # 'show': _input_types_simple['required']['show'], }, - "hidden": { - "unique_id": "UNIQUE_ID", + 'hidden': { + 'unique_id': 'UNIQUE_ID', }, - # "optional": {}, + # 'optional': {}, }) @@ -118,13 +133,13 @@ class BestResolutionFromAspectRatio: A more advanced node for easier selection of an optimal resolution: image size selected indirectly - by one of the sides + aspect ratio. """ - NODE_NAME = "BestResolutionFromAspectRatio" + NODE_NAME = 'BestResolutionFromAspectRatio' CATEGORY = "utils/resolution" DESCRIPTION = _cleandoc(__doc__) OUTPUT_NODE = True - FUNCTION = "main" + FUNCTION = 'main' RETURN_TYPES = _return_types_simple RETURN_NAMES = _return_names_simple @@ -132,7 +147,12 @@ class BestResolutionFromAspectRatio: def INPUT_TYPES(cls): return _input_types_orient - def main(self, size: int, step: int, size_is_big: bool, landscape: bool, aspect_a: float, aspect_b: float, unique_id: str = None): + def main( + self, + size: int, step: int, size_is_big: bool, landscape: bool, aspect_a: float, aspect_b: float, + # show: bool, + unique_id: str = None + ): aspect_big, aspect_small = _aspect_ratios_sorted(aspect_a, aspect_b) side_main_f = float(size) @@ -145,27 +165,32 @@ class BestResolutionFromAspectRatio: # The opposite: height is bigger width_f, height_f = height_f, width_f - return _simple_result_from_approx_wh(width_f, height_f, step, unique_id, _sqrt(width_f * height_f)) + return _simple_result_from_approx_wh( + width_f, height_f, step, + # show, + unique_id=unique_id, target_square_size=_sqrt(width_f * height_f) + ) _input_types_area = deepfreeze({ - "required": { - "square_size": (_IO.INT, dict(type_dict_res, **{"tooltip": ( + '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" "The width and height would be such to respect aspect ratio, but also be as close as possible to the " "total number of pixels as in this square image.\n\n" "- 512x512 square (SD 1.5): ~0.25 megapixels\n" "- 1024x1024 square (SDXL): ~1 megapixel" )})), - "step": _input_types_simple["required"]["step"], - "landscape": _input_types_orient["required"]["landscape"], - "aspect_a": _input_types_orient["required"]["aspect_a"], - "aspect_b": _input_types_orient["required"]["aspect_b"], + 'step': _input_types_simple['required']['step'], + 'landscape': _input_types_orient['required']['landscape'], + 'aspect_a': _input_types_orient['required']['aspect_a'], + 'aspect_b': _input_types_orient['required']['aspect_b'], + # 'show': _input_types_simple['required']['show'], }, - "hidden": { - "unique_id": "UNIQUE_ID", + 'hidden': { + 'unique_id': 'UNIQUE_ID', }, - # "optional": {}, + # 'optional': {}, }) @@ -183,13 +208,13 @@ class BestResolutionFromArea: 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 = "BestResolutionFromArea" + NODE_NAME = 'BestResolutionFromArea' CATEGORY = "utils/resolution" DESCRIPTION = _cleandoc(__doc__) OUTPUT_NODE = True - FUNCTION = "main" + FUNCTION = 'main' RETURN_TYPES = _return_types_simple RETURN_NAMES = _return_names_simple @@ -197,19 +222,16 @@ class BestResolutionFromArea: def INPUT_TYPES(cls): return _input_types_area - def main(self, square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, unique_id: str = None): - # square_size = 1024; step = 48; landscape = True; aspect_a = 9.0; aspect_b = 16.0 - aspect_big, aspect_small = _aspect_ratios_sorted(aspect_a, aspect_b) - aspect_x, aspect_y = (aspect_big, aspect_small) if landscape else (aspect_small, aspect_big) - - aspect_area = aspect_x * aspect_y - aspect_norm_scale = 1.0 / _sqrt(aspect_area) - aspect_x *= aspect_norm_scale - aspect_y *= aspect_norm_scale - # Now the two aspects produce a normalized rectangle - i.e., it's area is 1 - - square_size = _number_to_int(square_size) - width_f = aspect_x * square_size - height_f = aspect_y * square_size - - return _simple_result_from_approx_wh(width_f, height_f, step, unique_id, square_size) + def main( + self, + square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, + # 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 _simple_result_from_approx_wh( + width_f, height_f, step, + # show, + unique_id=unique_id, target_square_size=square_size + ) diff --git a/pyproject.toml b/pyproject.toml index f86ac51..fa9ee77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "best-resolution" -description = "QoL nodes for semi-automatic calculation of the best (most optimal) sampling resolution \n-compatible with ANY model (from now or the future), \n-accounting for upscale \n-and pixel-step." -version = "0.0.3" +description = "QoL nodes for semi-automatic calculation of the best (most optimal) sampling resolution \n• compatible with ANY model (from now or the future), \n• accounting for upscale \n• and pixel-step." +version = "0.0.4" authors = [ {name = "Lex Darlog"} ]