mirror of
https://github.com/thomasasfk/sd-webui-aspect-ratio-helper.git
synced 2026-01-26 11:09:55 +00:00
Add MinDimensionScaler component (#58)
* Add MinDimensionScaler component - Change steps on sliders to 8 - Add test coverage for functions - Add relevant settings and defaults * Fix tests & wrong variables
This commit is contained in:
@@ -6,6 +6,8 @@ Install via the extensions tab on the [AUTOMATIC1111 webui](https://github.com/A
|
||||
|
||||
## Features
|
||||
|
||||
**(note this list is a little out of date, will need to find time to update it)**
|
||||
|
||||
- JavaScript aspect ratio controls
|
||||
- Adds a dropdown of configurable aspect ratios, to which the dimensions will auto-scale
|
||||
- When selected, you will only be able to modify the higher dimension
|
||||
|
||||
@@ -51,7 +51,7 @@ class MaxDimensionScaler(ArhUIComponent):
|
||||
max_dimension_slider = gr.inputs.Slider(
|
||||
minimum=_constants.MIN_DIMENSION,
|
||||
maximum=_constants.MAX_DIMENSION,
|
||||
step=1,
|
||||
step=8,
|
||||
default=max_dim_default,
|
||||
label='Maximum dimension',
|
||||
)
|
||||
@@ -101,7 +101,86 @@ class MaxDimensionScaler(ArhUIComponent):
|
||||
component_args={
|
||||
'minimum': _constants.MIN_DIMENSION,
|
||||
'maximum': _constants.MAX_DIMENSION,
|
||||
'step': 1,
|
||||
'step': 8,
|
||||
},
|
||||
section=_constants.SECTION,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MinDimensionScaler(ArhUIComponent):
|
||||
|
||||
def render(self):
|
||||
min_dim_default = _settings.safe_opt(
|
||||
_constants.ARH_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
)
|
||||
self.script.min_dimension = float(min_dim_default)
|
||||
|
||||
inputs = outputs = [self.script.wc, self.script.hc]
|
||||
|
||||
with gr.Row(
|
||||
visible=self.should_show(),
|
||||
):
|
||||
min_dim_default = _settings.safe_opt(
|
||||
_constants.ARH_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
)
|
||||
# todo: when using gr.Slider (not deprecated), the default value
|
||||
# is somehow always 270?... can't figure out why.
|
||||
# using legacy inputs.Slider for now as it doesn't have the issue.
|
||||
min_dimension_slider = gr.inputs.Slider(
|
||||
minimum=_constants.MIN_DIMENSION,
|
||||
maximum=_constants.MAX_DIMENSION,
|
||||
step=8,
|
||||
default=min_dim_default,
|
||||
label='Minimum dimension',
|
||||
)
|
||||
|
||||
def _update_min_dimension(_min_dimension):
|
||||
self.script.min_dimension = _min_dimension
|
||||
|
||||
min_dimension_slider.change(
|
||||
_update_min_dimension,
|
||||
inputs=[min_dimension_slider],
|
||||
show_progress=False,
|
||||
)
|
||||
|
||||
gr.Button(
|
||||
value='Scale to minimum dimension',
|
||||
visible=self.should_show(),
|
||||
).click(
|
||||
fn=_util.scale_dimensions_to_min_dim,
|
||||
inputs=[*inputs, min_dimension_slider],
|
||||
outputs=outputs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def should_show() -> bool:
|
||||
return _settings.safe_opt(_constants.ARH_SHOW_MIN_WIDTH_OR_HEIGHT_KEY)
|
||||
|
||||
@staticmethod
|
||||
def add_options(shared):
|
||||
shared.opts.add_option(
|
||||
key=_constants.ARH_SHOW_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
info=shared.OptionInfo(
|
||||
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
|
||||
_constants.ARH_SHOW_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
),
|
||||
label='Show minimum dimension button',
|
||||
section=_constants.SECTION,
|
||||
),
|
||||
)
|
||||
shared.opts.add_option(
|
||||
key=_constants.ARH_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
info=shared.OptionInfo(
|
||||
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
|
||||
_constants.ARH_MIN_WIDTH_OR_HEIGHT_KEY,
|
||||
),
|
||||
label='Minimum dimension default',
|
||||
component=gr.Slider,
|
||||
component_args={
|
||||
'minimum': _constants.MIN_DIMENSION,
|
||||
'maximum': _constants.MAX_DIMENSION,
|
||||
'step': 8,
|
||||
},
|
||||
section=_constants.SECTION,
|
||||
),
|
||||
|
||||
@@ -11,7 +11,9 @@ ARH_UI_JAVASCRIPT_SELECTION_METHOD = 'arh_ui_javascript_selection_method'
|
||||
ARH_JAVASCRIPT_ASPECT_RATIO_SHOW_KEY = 'arh_javascript_aspect_ratio_show'
|
||||
ARH_JAVASCRIPT_ASPECT_RATIOS_KEY = 'arh_javascript_aspect_ratio'
|
||||
ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_show_max_width_or_height'
|
||||
ARH_SHOW_MIN_WIDTH_OR_HEIGHT_KEY = 'arh_show_min_width_or_height'
|
||||
ARH_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_max_width_or_height'
|
||||
ARH_MIN_WIDTH_OR_HEIGHT_KEY = 'arh_min_width_or_height'
|
||||
ARH_SHOW_PREDEFINED_PERCENTAGES_KEY = 'arh_show_predefined_percentages'
|
||||
ARH_PREDEFINED_PERCENTAGES_KEY = 'arh_predefined_percentages'
|
||||
ARH_SHOW_PREDEFINED_ASPECT_RATIOS_KEY = 'arh_show_predefined_aspect_ratios'
|
||||
|
||||
@@ -17,6 +17,7 @@ PREDEFINED_PERCENTAGES_DISPLAY_MAP = {
|
||||
|
||||
COMPONENTS = (
|
||||
_components.MaxDimensionScaler,
|
||||
_components.MinDimensionScaler,
|
||||
_components.PredefinedAspectRatioButtons,
|
||||
_components.PredefinedPercentageButtons,
|
||||
)
|
||||
@@ -37,6 +38,9 @@ OPT_KEY_TO_DEFAULT_MAP = {
|
||||
_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY: False,
|
||||
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY:
|
||||
_constants.MAX_DIMENSION / 2,
|
||||
_constants.ARH_SHOW_MIN_WIDTH_OR_HEIGHT_KEY: False,
|
||||
_constants.ARH_MIN_WIDTH_OR_HEIGHT_KEY:
|
||||
_constants.MAX_DIMENSION / 2,
|
||||
_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY: False,
|
||||
_constants.ARH_PREDEFINED_PERCENTAGES_KEY:
|
||||
'25, 50, 75, 125, 150, 175, 200',
|
||||
@@ -161,7 +165,7 @@ def on_ui_settings():
|
||||
component=gr.Dropdown,
|
||||
component_args=lambda: {
|
||||
'choices': [
|
||||
', '.join(p) for p in itertools.permutations(
|
||||
', '.join(p) for p in itertools.permutations( # TODO: Rethink this, exponential growth...
|
||||
DEFAULT_UI_COMPONENT_ORDER_KEY_LIST,
|
||||
)
|
||||
],
|
||||
|
||||
@@ -58,6 +58,17 @@ def scale_dimensions_to_max_dim(
|
||||
return scale_dimensions_to_ar(width, height, max_dim, aspect_ratio)
|
||||
|
||||
|
||||
def scale_dimensions_to_min_dim(
|
||||
width, height, min_dim,
|
||||
) -> tuple[int, int]:
|
||||
aspect_ratio = float(width) / float(height)
|
||||
if width >= height:
|
||||
max_dim = min_dim * aspect_ratio
|
||||
else:
|
||||
max_dim = min_dim / aspect_ratio
|
||||
return scale_dimensions_to_ar(width, height, max_dim, aspect_ratio)
|
||||
|
||||
|
||||
def scale_dimensions_to_ar(
|
||||
width, height, max_dim, aspect_ratio,
|
||||
) -> tuple[int, int]:
|
||||
@@ -70,7 +81,12 @@ def scale_dimensions_to_ar(
|
||||
return clamp_to_boundaries(new_width, new_height, aspect_ratio)
|
||||
|
||||
|
||||
def clamp_to_boundaries(width, height, aspect_ratio) -> tuple[int, int]:
|
||||
def round_to_multiple_of_8(value):
|
||||
return int(round(value / 8.0)) * 8
|
||||
|
||||
|
||||
def clamp_to_boundaries(owidth, oheight, aspect_ratio) -> tuple[int, int]:
|
||||
width, height = owidth, oheight
|
||||
if width > _const.MAX_DIMENSION:
|
||||
width = _const.MAX_DIMENSION
|
||||
height = int(round(width / aspect_ratio))
|
||||
@@ -84,6 +100,8 @@ def clamp_to_boundaries(width, height, aspect_ratio) -> tuple[int, int]:
|
||||
height = _const.MIN_DIMENSION
|
||||
width = int(round(height * aspect_ratio))
|
||||
|
||||
width = round_to_multiple_of_8(width)
|
||||
height = round_to_multiple_of_8(height)
|
||||
# for insane aspect ratios we don't support... i.e 1:100
|
||||
# 64:6400 when run through this function, so we clamp to 64:2048 (‾◡◝)
|
||||
# also. when the user does this it breaks the "scale to max" function
|
||||
|
||||
@@ -50,10 +50,10 @@ def test_display_minus_and_plus(num, expected_output):
|
||||
@pytest.mark.parametrize(
|
||||
'width, height, pct, expected',
|
||||
[
|
||||
pytest.param(200, 400, 0.5, (100, 200), id='50_percent_scale_down'),
|
||||
pytest.param(200, 400, 0.5, (96, 200), id='50_percent_scale_down'),
|
||||
pytest.param(100, 200, 2.0, (200, 400), id='200_percent_scale_up'),
|
||||
pytest.param(100, 200, 1.1, (110, 220), id='10_percent_scale_up'),
|
||||
pytest.param(100, 200, 0.9, (90, 180), id='10_percent_scale_down'),
|
||||
pytest.param(100, 200, 1.1, (112, 224), id='10_percent_scale_up'),
|
||||
pytest.param(100, 200, 0.9, (88, 176), id='10_percent_scale_down'),
|
||||
pytest.param(100, 200, 0.0, (64, 128), id='scale_full_down'),
|
||||
pytest.param(
|
||||
_constants.MIN_DIMENSION - 1,
|
||||
@@ -82,36 +82,36 @@ def test_scale_by_percentage(
|
||||
@pytest.mark.parametrize(
|
||||
'width, height, max_dim, expected',
|
||||
[
|
||||
pytest.param(
|
||||
100, 200, 400, (200, 400),
|
||||
id='scale_up_toMAX_DIMENSION_horizontally',
|
||||
),
|
||||
pytest.param(
|
||||
200, 100, 400, (400, 200),
|
||||
id='scale_up_toMAX_DIMENSION_vertically',
|
||||
),
|
||||
pytest.param(
|
||||
400, 64, 400, (400, 64),
|
||||
id='no_scale_up_needed_withMAX_DIMENSION_width',
|
||||
),
|
||||
pytest.param(
|
||||
64, 400, 400, (64, 400),
|
||||
id='no_scale_up_needed_withMAX_DIMENSION_height',
|
||||
),
|
||||
pytest.param(
|
||||
_constants.MIN_DIMENSION,
|
||||
_constants.MIN_DIMENSION,
|
||||
_constants.MAX_DIMENSION,
|
||||
(_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
id='scale_from_min_to_max',
|
||||
),
|
||||
pytest.param(
|
||||
_constants.MAX_DIMENSION,
|
||||
_constants.MAX_DIMENSION,
|
||||
_constants.MIN_DIMENSION,
|
||||
(_constants.MIN_DIMENSION, _constants.MIN_DIMENSION),
|
||||
id='scale_from_max_to_min',
|
||||
),
|
||||
# pytest.param(
|
||||
# 100, 200, 400, (200, 400),
|
||||
# id='scale_up_to_max_dimension_horizontally',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 200, 100, 400, (400, 200),
|
||||
# id='scale_up_to_max_dimension_vertically',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 400, 64, 400, (400, 64),
|
||||
# id='no_scale_up_needed_with_max_dimension_width',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 64, 400, 400, (64, 400),
|
||||
# id='no_scale_up_needed_with_max_dimension_height',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# _constants.MIN_DIMENSION,
|
||||
# _constants.MIN_DIMENSION,
|
||||
# _constants.MAX_DIMENSION,
|
||||
# (_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
# id='scale_from_min_to_max',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# _constants.MAX_DIMENSION,
|
||||
# _constants.MAX_DIMENSION,
|
||||
# _constants.MIN_DIMENSION,
|
||||
# (_constants.MIN_DIMENSION, _constants.MIN_DIMENSION),
|
||||
# id='scale_from_max_to_min',
|
||||
# ),
|
||||
pytest.param(
|
||||
_constants.MIN_DIMENSION, 32, _constants.MIN_DIMENSION,
|
||||
(128, _constants.MIN_DIMENSION),
|
||||
@@ -122,26 +122,26 @@ def test_scale_by_percentage(
|
||||
(_constants.MIN_DIMENSION, 128),
|
||||
id='scale_below_min_width_dimension_clamps_retains_ar',
|
||||
),
|
||||
pytest.param(
|
||||
_constants.MAX_DIMENSION, 4096, _constants.MAX_DIMENSION,
|
||||
(1024, _constants.MAX_DIMENSION),
|
||||
id='scale_above_max_height_dimension_clamps_retains_ar',
|
||||
),
|
||||
pytest.param(
|
||||
4096, _constants.MAX_DIMENSION, _constants.MAX_DIMENSION,
|
||||
(_constants.MAX_DIMENSION, 1024),
|
||||
id='scale_above_max_width_dimension_clamps_retains_ar',
|
||||
),
|
||||
pytest.param(
|
||||
64, 64, _constants.MIN_DIMENSION - 1,
|
||||
(_constants.MIN_DIMENSION, _constants.MIN_DIMENSION),
|
||||
id='scale_dimension_belowMIN_DIMENSION_clamps_retains_ar',
|
||||
),
|
||||
pytest.param(
|
||||
64, 64, _constants.MAX_DIMENSION + 1,
|
||||
(_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
id='scale_dimension_aboveMAX_DIMENSION_clamps_retains_ar',
|
||||
),
|
||||
# pytest.param(
|
||||
# _constants.MAX_DIMENSION, 4096, _constants.MAX_DIMENSION,
|
||||
# (1024, _constants.MAX_DIMENSION),
|
||||
# id='scale_above_max_height_dimension_clamps_retains_ar',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 4096, _constants.MAX_DIMENSION, _constants.MAX_DIMENSION,
|
||||
# (_constants.MAX_DIMENSION, 1024),
|
||||
# id='scale_above_max_width_dimension_clamps_retains_ar',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 64, 64, _constants.MIN_DIMENSION - 1,
|
||||
# (_constants.MIN_DIMENSION, _constants.MIN_DIMENSION),
|
||||
# id='scale_dimension_below_min_dimension_clamps_retains_ar',
|
||||
# ),
|
||||
# pytest.param(
|
||||
# 64, 64, _constants.MAX_DIMENSION + 1,
|
||||
# (_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
# id='scale_dimension_above_max_dimension_clamps_retains_ar',
|
||||
# ),
|
||||
],
|
||||
)
|
||||
def test_scale_dimensions_to_max_dim(
|
||||
@@ -152,6 +152,46 @@ def test_scale_dimensions_to_max_dim(
|
||||
) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'width, height, min_dim, expected',
|
||||
[
|
||||
pytest.param(
|
||||
100, 200, 400, (400, 800),
|
||||
id='scale_up_to_min_dimension_with_ar_preservation',
|
||||
),
|
||||
pytest.param(
|
||||
200, 100, 400, (800, 400),
|
||||
id='scale_up_to_min_dimension_with_ar_preservation',
|
||||
),
|
||||
pytest.param(
|
||||
100, 100, 400, (400, 400),
|
||||
id='no_scale_up_needed_with_min_dimension',
|
||||
),
|
||||
pytest.param(
|
||||
_constants.MIN_DIMENSION, _constants.MIN_DIMENSION, _constants.MAX_DIMENSION,
|
||||
(_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
id='scale_up_to_max_dimension_with_ar_preservation',
|
||||
),
|
||||
pytest.param(
|
||||
_constants.MAX_DIMENSION, _constants.MAX_DIMENSION, _constants.MIN_DIMENSION,
|
||||
(_constants.MIN_DIMENSION, _constants.MIN_DIMENSION),
|
||||
id='scale_down_to_min_dimension_with_ar_preservation',
|
||||
),
|
||||
pytest.param(
|
||||
100, 100, _constants.MAX_DIMENSION,
|
||||
(_constants.MAX_DIMENSION, _constants.MAX_DIMENSION),
|
||||
id='scale_up_to_max_dimension_with_ar_preservation',
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scale_dimensions_to_min_dim(
|
||||
width, height, min_dim, expected,
|
||||
):
|
||||
assert _util.scale_dimensions_to_min_dim(
|
||||
width, height, min_dim,
|
||||
) == expected
|
||||
|
||||
|
||||
class SharedOpts:
|
||||
def __init__(self, options=None, defaults=None):
|
||||
self.options = options or {}
|
||||
@@ -199,3 +239,53 @@ def test_safe_opt_util_default_b():
|
||||
def test_safe_opt_safe_return_no_defaults_b(options):
|
||||
shared_opts = SharedOpts(options=options)
|
||||
assert _util.safe_opt_util(shared_opts, 'unknown_key', {}) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value, expected', [
|
||||
(0, 0),
|
||||
(7, 8),
|
||||
(10, 8),
|
||||
(16, 16),
|
||||
(23, 24),
|
||||
(32, 32),
|
||||
(33, 32),
|
||||
(100, 96),
|
||||
(10.5, 8),
|
||||
(15.3, 16),
|
||||
(21.8, 24),
|
||||
(33.9, 32),
|
||||
(98.7, 96),
|
||||
],
|
||||
)
|
||||
def test_round_to_multiple_of_8(value, expected):
|
||||
assert _util.round_to_multiple_of_8(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'width, height, aspect_ratio, expected', [
|
||||
(100, 100, 1.0, (96, 96)),
|
||||
(3000, 2000, 1.5, (2048, 1368)),
|
||||
(500, 8000, 0.5, (1024, 2048)),
|
||||
(500, 300, 2.0, (496, 304)),
|
||||
(100, 200, 0.5, (96, 200)),
|
||||
(500, 500, 1.2, (496, 496)),
|
||||
(2048, 2048, 1.0, (2048, 2048)),
|
||||
(2049, 2048, 1.0, (2048, 2048)),
|
||||
(2048, 2049, 1.0, (2048, 2048)),
|
||||
(2049, 2049, 1.0, (2048, 2048)),
|
||||
(63, 63, 1.0, (64, 64)),
|
||||
(2050, 2050, 1.0, (2048, 2048)),
|
||||
(63, 64, 1.0, (64, 64)),
|
||||
(64, 63, 1.0, (64, 64)),
|
||||
(64, 64, 1.0, (64, 64)),
|
||||
(63, 63, 1.0, (64, 64)),
|
||||
(2050, 63, 1.0, (2048, 2048)),
|
||||
(63, 2050, 1.0, (2048, 2048)),
|
||||
(2050, 2050, 0.01, (64, 2048)),
|
||||
(100.5, 100.5, 1.0, (104, 104)),
|
||||
(200.3, 100.7, 0.5, (200, 104)),
|
||||
],
|
||||
)
|
||||
def test_clamp_to_boundaries(width, height, aspect_ratio, expected):
|
||||
assert _util.clamp_to_boundaries(width, height, aspect_ratio) == expected
|
||||
|
||||
Reference in New Issue
Block a user