From ec509ddc75e72d0acddf5ccab09a2ce43a666acd Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 19 Jul 2025 09:41:22 +0500 Subject: [PATCH] + implemented --- _funcs.py | 18 ----- _funcs_crop_pad.py | 184 +++++++++++++++++++++++++++++++++++++++++++++ enums.py | 4 +- node_crop_pad.py | 2 +- nodes_prims.py | 2 +- 5 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 _funcs_crop_pad.py diff --git a/_funcs.py b/_funcs.py index 6fd750d..7b73c9d 100644 --- a/_funcs.py +++ b/_funcs.py @@ -261,21 +261,3 @@ def upscale_result_from_approx_wh( _show_text_on_node(text, unique_id) return result - - -def upscaled_crop_pad( - upscale: float, - orig_w: int, orig_h: int, up_w: int, up_h: int, - strategy: _t.Union[UpscaledCropPadStrategy, str], - align_x: float, align_y: float, - # show: bool, - unique_id: str = None -) -> ResultUpscaledCropPad: -): - # TODO - result = ResultUpscaledCropPad( - upscale, - False, 1024, 1024, 0, 0, - False, 10, 10, 10, 10, - ) - return result diff --git a/_funcs_crop_pad.py b/_funcs_crop_pad.py new file mode 100644 index 0000000..efc59bb --- /dev/null +++ b/_funcs_crop_pad.py @@ -0,0 +1,184 @@ +# encoding: utf-8 +""" +The actual behavior of "Upscaled Crop/Pad" node. +""" + +import typing as _t + +from ._dataclass import dataclass_with_slots_if_possible as _dataclass_with_slots_if_possible +from ._funcs import round_pos_int as _round_pos_int, _need_post_resize, _show_text_on_node +from .enums import * +from .return_tuples import * + + +@_dataclass_with_slots_if_possible +class _CropPadInput: + """ + For the ease of passing between the functions, the entire set of inputs for "Upscaled Crop/Pad" node + is internally turned into a dataclass. + """ + upscale: float + + orig_w: int + orig_h: int + up_w: int + up_h: int + + strategy: str + align_x: float + align_y: float + + +def _upscaled_crop_xy_offset(extra_w: int, extra_h: int, align_x: float, align_y: float): + crop_x_origin = _round_pos_int(align_x * extra_w) + crop_y_origin = _round_pos_int((1.0 - align_y) * extra_h) + return crop_x_origin, crop_y_origin + + +def _upscaled_crop(_in: _CropPadInput, real_upscale: float) -> ResultUpscaledCropPad: + raw_upscaled_w = _round_pos_int(real_upscale * _in.orig_w) + raw_upscaled_h = _round_pos_int(real_upscale * _in.orig_h) + extra_w = max(raw_upscaled_w - _in.up_w, 0) + extra_h = max(raw_upscaled_h - _in.up_h, 0) + crop_x_offset, crop_y_offset = _upscaled_crop_xy_offset(extra_w, extra_h, _in.align_x, _in.align_y) + return ResultUpscaledCropPad( + real_upscale, + True, _in.up_w, _in.up_h, crop_x_offset, crop_y_offset, + False, 0, 0, 0, 0, + ) + + +def _upscaled_pad_side_values(extra_w: int, extra_h: int, align_x: float, align_y: float): + pad_left = _round_pos_int(align_x * extra_w) + pad_right = extra_w - pad_left + pad_bottom = _round_pos_int(align_y * extra_h) + pad_top = extra_h - pad_bottom + return pad_left, pad_top, pad_right, pad_bottom + + +def _upscaled_pad(_in: _CropPadInput, real_upscale: float) -> ResultUpscaledCropPad: + raw_upscaled_w = _round_pos_int(real_upscale * _in.orig_w) + raw_upscaled_h = _round_pos_int(real_upscale * _in.orig_h) + pad_w = max(_in.up_w - raw_upscaled_w, 0) + pad_h = max(_in.up_h - raw_upscaled_h, 0) + return ResultUpscaledCropPad( + real_upscale, + False, raw_upscaled_w, raw_upscaled_h, 0, 0, + True, *_upscaled_pad_side_values(pad_w, pad_h, _in.align_x, _in.align_y), + ) + + +def _upscaled_crop_delta_pixels(_in: _CropPadInput, result: ResultUpscaledCropPad) -> int: + """Calculate the area of total cropped patch for crop-only mode.""" + raw_upscaled_w = _round_pos_int(result.upscale * _in.orig_w) + raw_upscaled_h = _round_pos_int(result.upscale * _in.orig_h) + cropped_w = min(result.crop_width, raw_upscaled_w) + cropped_h = min(result.crop_height, raw_upscaled_h) + return abs(raw_upscaled_w * raw_upscaled_h - cropped_w * cropped_h) + + +def _upscaled_pad_delta_pixels(_in: _CropPadInput, result: ResultUpscaledCropPad) -> int: + """Calculate the area of total out-painted patch for pad-only mode.""" + raw_upscaled_w = _round_pos_int(result.upscale * _in.orig_w) + raw_upscaled_h = _round_pos_int(result.upscale * _in.orig_h) + padded_w = raw_upscaled_w + max(result.pad_left, 0) + max(result.pad_right, 0) + padded_h = raw_upscaled_h + max(result.pad_top, 0) + max(result.pad_bottom, 0) + return abs(padded_w * padded_h - raw_upscaled_w * raw_upscaled_h) + + +def _upscaled_crop_pad(_in: _CropPadInput) -> ResultUpscaledCropPad: + """The actual function for "Upscaled Crop/Pad" node - extracted to wrap it with displaying the message.""" + needs_resize, real_upscale_avg, real_upscale_x, real_upscale_y = _need_post_resize(_in.orig_w, _in.orig_h, _in.up_w, _in.up_h) + if not needs_resize: + return ResultUpscaledCropPad( + real_upscale_avg, + False, _in.up_w, _in.up_h, 0, 0, + False, 0, 0, 0, 0, + ) + + _in.align_x = min(max(_in.align_x, 0.0), 1.0) + _in.align_y = min(max(_in.align_y, 0.0), 1.0) + + if _in.strategy == UpscaledCropPadStrategy.CROP: + return _upscaled_crop(_in, max(real_upscale_x, real_upscale_y)) + + if _in.strategy == UpscaledCropPadStrategy.PAD: + return _upscaled_pad(_in, min(real_upscale_x, real_upscale_y)) + + if _in.strategy == UpscaledCropPadStrategy.NEAREST: + crop_result = _upscaled_crop(_in, max(real_upscale_x, real_upscale_y)) + pad_result = _upscaled_pad(_in, min(real_upscale_x, real_upscale_y)) + return ( + pad_result + if _upscaled_pad_delta_pixels(_in, pad_result) < _upscaled_crop_delta_pixels(_in, crop_result) + else crop_result + ) + + assert _in.strategy == UpscaledCropPadStrategy.EXACT_UPSCALE + + upscale = _in.upscale + raw_upscaled_w = _round_pos_int(upscale * _in.orig_w) + raw_upscaled_h = _round_pos_int(upscale * _in.orig_h) + # We're in the most complex case. These ^ can be both below and above the target up-res. + # So, we need to follow the whole upscale->crop->pad chain: we'll just get zeroes where there will be nothing to do. + + cropped_w = min(_in.up_w, raw_upscaled_w) + cropped_h = min(_in.up_h, raw_upscaled_h) + crop_extra_w = raw_upscaled_w - cropped_w + crop_extra_h = raw_upscaled_h - cropped_h + crop_x_offset, crop_y_offset = _upscaled_crop_xy_offset(crop_extra_w, crop_extra_h, _in.align_x, _in.align_y) + + pad_extra_w = _in.up_w - cropped_w + pad_extra_h = _in.up_h - cropped_h + pad_side_values = _upscaled_pad_side_values(pad_extra_w, pad_extra_h, _in.align_x, _in.align_y) + return ResultUpscaledCropPad( + upscale, + crop_extra_w > 0 or crop_extra_h > 0, cropped_w, cropped_h, crop_x_offset, crop_y_offset, + pad_extra_w > 0 or pad_extra_h > 0, *pad_side_values, + ) + + +def upscaled_crop_pad( + upscale: float, + orig_w: int, orig_h: int, up_w: int, up_h: int, + strategy: str, + align_x: float, align_y: float, + show: bool = True, + unique_id: str = None +) -> ResultUpscaledCropPad: + """ + Detect, whether some cropping/out-painting needs to be done after upscaling the original image. + And if so - what are the values for them. + """ + # noinspection PyArgumentList + result = _upscaled_crop_pad(_CropPadInput( + upscale, + orig_w, orig_h, up_w, up_h, + strategy, + align_x, align_y, + )) + + if not unique_id: + return result + + if not show: + _show_text_on_node(None, unique_id) + return result + + if not(result.do_crop or result.do_padding): + _show_text_on_node(f"x{result.upscale:.3f}✅", unique_id) + return result + + # Either crop, or pad, or both + text_parts: _t.List[str] = [f"x{result.upscale:.3f}", ] + + if result.do_crop: + crop_r = up_w - (result.crop_x_origin + result.crop_width) + crop_b = up_h - (result.crop_y_origin + result.crop_height) + text_parts.append(f"Crop:\nL {result.crop_x_origin}, R {crop_r}, B {crop_b}, T {result.crop_y_origin}") + + if result.do_padding: + text_parts.append(f"Padding:\nL {result.pad_left}, R {result.pad_right}, B {result.pad_bottom}, T {result.pad_top}") + + _show_text_on_node('\n\n'.join(text_parts), unique_id) + return result diff --git a/enums.py b/enums.py index 11a3d0b..f3fce28 100644 --- a/enums.py +++ b/enums.py @@ -45,9 +45,11 @@ class UpscaledCropPadStrategy(__BaseEnum): • pad - upscale the image to fit it into the desired frame on one side, then add missing pixels on the other one for outpaint. • crop - upscale the image to fill the entire frame, then crop extra pixels on one side. - • nearest - automatically choose one of the above, to crop/pad the least number of pixels. + • nearest - automatically choose one of the above, to crop/pad the least number of pixels (by area). • exact-upscale - follow the provided upscale-value precisely. This is the only option that uses it. Also, it's the only one that might require both outpainting and crop. + + In case when both are required, the output values assume this order: upscale -> crop -> pad. """ PAD = 'pad' CROP = 'crop' diff --git a/node_crop_pad.py b/node_crop_pad.py index b9cac96..97e3d3b 100644 --- a/node_crop_pad.py +++ b/node_crop_pad.py @@ -11,7 +11,7 @@ from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict from comfy.comfy_types.node_typing import IO as _IO -from ._funcs import upscaled_crop_pad as _upscaled_crop_pad +from ._funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .enums import * diff --git a/nodes_prims.py b/nodes_prims.py index 6c83521..3d7fe71 100644 --- a/nodes_prims.py +++ b/nodes_prims.py @@ -77,7 +77,7 @@ _up_strategy_data_type_set = set(_up_strategy_data_type) _up_strategy_in_type = ( _up_strategy_data_type, { - 'default': UpscaledCropPadStrategy.PAD.value, + 'default': UpscaledCropPadStrategy.EXACT_UPSCALE.value, 'tooltip': _format_object_docstring(UpscaledCropPadStrategy), } )