BestResolutionFromAreaUpscale: additional outputs

This commit is contained in:
Lex Darlog (DRL)
2025-07-13 14:34:39 +05:00
parent ecb6f5f292
commit d7d4dab444
3 changed files with 103 additions and 67 deletions
+56 -37
View File
@@ -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 = '<span></span>'
# 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 '<span></span>' # 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 = '<span></span>' # 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
+11 -13
View File
@@ -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"
+36 -17
View File
@@ -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):