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:
thomas
2023-05-28 21:31:15 +01:00
committed by GitHub
parent e0e09b06ad
commit 0bccd48788
6 changed files with 252 additions and 57 deletions

View File

@@ -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

View File

@@ -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,
),

View File

@@ -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'

View File

@@ -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,
)
],

View File

@@ -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

View File

@@ -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