Add aspect ratios configuration option (#15)

* Add aspect ratios configuration option

- Add new component for configurable aspect ratios
- Add options allowing for aspect ratios to use max option
- Update documentation to reflect aspect ratio functionality
- Refactor options to include in each relevant component
This commit is contained in:
thomas
2023-03-22 19:01:24 +00:00
committed by GitHub
parent 18d4199854
commit 0e89df9266
9 changed files with 334 additions and 127 deletions

View File

@@ -4,50 +4,64 @@ Simple extension to easily maintain aspect ratio while changing dimensions.
Install via the extensions tab on the [AUTOMATIC1111 webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui).
## Main features:
## Features
- Scale to maximum width or height
- Upon clicking, the dimensions will scale according to the configured maximum value
- Scale to maximum dimension
- Upon clicking, the width and height will scale according to the configured maximum value
- Aspect ratio will be retained, the smaller or equivalent dimension will be scaled to match
- Scale to aspect ratio
- Upon clicking, the current dimensions will be scaled to the given aspect ratio, using the highest width or height
- i.e `4:3 of 256x512 = 512x384` `9:16 of 512x256 = 288x512` `1:1 of 256x300 = 300x300`
- You can optionally toggle this to use the "Maximum dimension" slider value
- i.e `4:3 of 512 = 512x384` `9:16 of 512 = 288x512` `1:1 of 300 = 300x300`
- Scale by percentage
- Upon clicking, the current dimensions will be multiplied by the given percentage, with aspect ratio maintained
- i.e `150% of 512x512 = 768x768` `75% of 512x256 = 384x192` etc.
- i.e `-25% of 512x256 = 384x192` `+50% of 512x512 = 768x768`
- You can also change the display of these if you find it more intuitive
- i.e `75% of 512x256 = 384x192` `150% of 512x512 = 768x768`
- i.e `x0.75 of 512x256 = 384x192` `x1.5 of 512x512 = 768x768`
![user-interface.png](docs%2Fuser-interface.png)
## Settings:
## Settings
- Expand by default
- Expand by default (`False`)
- Determines whether the 'Aspect Ratio Helper' accordion expands by default
- Show maximum width or height button
- Maximum width or height default
- Show predefined percentage buttons
- Predefined percentage buttons
- Comma separated list of percentages
- i.e `25, 50, 75, 125, 150, 175, 200` `50, 125, 300` etc.
- Predefined percentage display format
- UI Component order (`MaxDimensionScaler, PredefinedAspectRatioButtons, PredefinedPercentageButtons`)
- Determines the order in which the UI components will render
- Show maximum dimension button (`True`)
- Maximum dimension default (`1024`)
- Show pre-defined aspect ratio buttons (`True`)
- Use "Maximum dimension" for aspect ratio buttons (`False`)
- Pre-defined aspect ratio buttons (`1:1, 4:3, 16:9, 9:16, 21:9`)
- i.e `1:1, 4:3, 16:9, 9:16, 21:9` `2:3, 1:5, 3:5`
- Show pre-defined percentage buttons (`True`)
- Pre-defined percentage buttons (`25, 50, 75, 125, 150, 175, 200`)
- i.e `25, 50, 75, 125, 150, 175, 200` `50, 125, 300`
- Pre-defined percentage display format (`Incremental/decremental percentage (-50%, +50%)`)
- `Incremental/decremental percentage (-50%, +50%)`
- `Raw percentage (50%, 150%)`
- `Multiplication (x0.5, x1.5)`
![settings.png](docs%2Fsettings.png)
## Contributing:
## Contributing
- Open to suggestions
- Pull requests are appreciated
- Write tests if possible and useful
- Run pre-commit
- Open an issue for suggestions
- Raise a pull request
## Dependencies:
## Dependencies
Developed using existing [AUTOMATIC1111 webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) dependencies.
However - for running unit tests, we use pytest:
However - for running unit tests, we use pytest.
```bash
pip install pytest
```
## Testing:
From the root of the repository run:
## Testing
From the root of the repository.
```bash
pytest
```

View File

@@ -21,23 +21,53 @@ class ArhUIComponent(ABC):
@abstractmethod
def should_show() -> bool: ...
@staticmethod
@abstractmethod
def add_options(): ...
class MaxDimensionScaler(ArhUIComponent):
def render(self):
max_dim_default = _settings.safe_opt(
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
)
self.script.max_dimension = float(max_dim_default)
inputs = outputs = [self.script.wc, self.script.hc]
with gr.Row():
max_dimension = gr.inputs.Slider(
with gr.Row(
visible=self.should_show(),
):
max_dim_default = _settings.safe_opt(
_constants.ARH_MAX_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.
max_dimension_slider = gr.inputs.Slider(
minimum=_constants.MIN_DIMENSION,
maximum=_constants.MAX_DIMENSION,
step=1,
default=_settings.safe_opt(
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
),
label='Maximum width or height (whichever is higher)',
default=max_dim_default,
label='Maximum dimension',
)
gr.Button(value='Scale to maximum width or height').click(
fn=_util.scale_dimensions_to,
inputs=[*inputs, max_dimension],
def _update_max_dimension(_max_dimension):
self.script.max_dimension = _max_dimension
max_dimension_slider.change(
_update_max_dimension,
inputs=[max_dimension_slider],
show_progress=False,
)
gr.Button(
value='Scale to maximum dimension',
visible=self.should_show(),
).click(
fn=_util.scale_dimensions_to_max_dim,
inputs=[*inputs, max_dimension_slider],
outputs=outputs,
)
@@ -45,17 +75,145 @@ class MaxDimensionScaler(ArhUIComponent):
def should_show() -> bool:
return _settings.safe_opt(_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY)
@staticmethod
def add_options(shared):
shared.opts.add_option(
key=_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY,
),
label='Show maximum dimension button',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
),
label='Maximum dimension default',
component=gr.Slider,
component_args={
'minimum': _constants.MIN_DIMENSION,
'maximum': _constants.MAX_DIMENSION,
'step': 1,
},
section=_constants.SECTION,
),
)
class PredefinedAspectRatioButtons(ArhUIComponent):
def render(self):
use_max_dim_op = _settings.safe_opt(
_constants.ARH_PREDEFINED_ASPECT_RATIO_USE_MAX_DIM_KEY,
)
aspect_ratios = _settings.safe_opt(
_constants.ARH_PREDEFINED_ASPECT_RATIOS_KEY,
).split(',')
with gr.Column(
variant='panel',
visible=self.should_show(),
), gr.Row(
variant='compact',
visible=self.should_show(),
):
for ar_str in aspect_ratios:
w, h, *_ = [abs(float(d)) for d in ar_str.split(':')]
inputs = []
if use_max_dim_op:
ar_func = partial(
_util.scale_dimensions_to_max_dim_func,
width=w, height=h,
max_dim=lambda: self.script.max_dimension,
)
else:
inputs.extend([self.script.wc, self.script.hc])
ar_func = partial(
_util.scale_dimensions_to_ui_width_or_height,
arw=w, arh=h,
)
gr.Button(
value=self.display_func(ar_str) or ar_str,
visible=self.should_show(),
).click(
fn=ar_func,
inputs=inputs,
outputs=[self.script.wc, self.script.hc],
)
@staticmethod
def should_show() -> bool:
return _settings.safe_opt(
_constants.ARH_SHOW_PREDEFINED_ASPECT_RATIOS_KEY,
)
@staticmethod
def add_options(shared):
shared.opts.add_option(
key=_constants.ARH_SHOW_PREDEFINED_ASPECT_RATIOS_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_SHOW_PREDEFINED_ASPECT_RATIOS_KEY,
),
label='Show pre-defined aspect ratio buttons',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_ASPECT_RATIO_USE_MAX_DIM_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_ASPECT_RATIO_USE_MAX_DIM_KEY,
),
label='Use "Maximum dimension" for aspect ratio buttons (by '
'default we use the max width or height)',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_ASPECT_RATIOS_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_ASPECT_RATIOS_KEY,
),
label='Pre-defined aspect ratio buttons '
'(1:1, 4:3, 16:9, 9:16, 21:9)',
section=_constants.SECTION,
),
)
@property
def display_func(self) -> callable:
return lambda _: None # todo: different displays for aspect ratios.
class PredefinedPercentageButtons(ArhUIComponent):
def render(self):
inputs = outputs = [self.script.wc, self.script.hc]
with gr.Column(variant='panel'), gr.Row(variant='compact'):
with gr.Column(
variant='panel',
visible=self.should_show(),
), gr.Row(
variant='compact',
visible=self.should_show(),
):
pps = _settings.safe_opt(_constants.ARH_PREDEFINED_PERCENTAGES_KEY)
percentages = [abs(int(x)) for x in pps.split(',')]
for percentage in percentages:
display = self.display_func(percentage)
gr.Button(value=display).click(
gr.Button(
value=display,
visible=self.should_show(),
).click(
fn=partial(
_util.scale_by_percentage,
pct=percentage / 100,
@@ -70,6 +228,45 @@ class PredefinedPercentageButtons(ArhUIComponent):
_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY,
)
@staticmethod
def add_options(shared):
shared.opts.add_option(
key=_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY,
),
label='Show pre-defined percentage buttons',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_PERCENTAGES_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_PERCENTAGES_KEY,
),
label='Pre-defined percentage buttons (75, 125, 150)',
section=_constants.SECTION,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY,
info=shared.OptionInfo(
default=_settings.OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY,
),
label='Pre-defined percentage display format',
component=gr.Dropdown,
component_args=lambda: {
'choices': tuple(
_settings.PREDEFINED_PERCENTAGES_DISPLAY_MAP.keys(),
),
},
section=_constants.SECTION,
),
)
@property
def display_func(self) -> callable:
return _settings.PREDEFINED_PERCENTAGES_DISPLAY_MAP.get(

View File

@@ -9,7 +9,12 @@ ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_show_max_width_or_height'
ARH_MAX_WIDTH_OR_HEIGHT_KEY = 'arh_max_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'
ARH_PREDEFINED_ASPECT_RATIOS_KEY = 'arh_predefined_aspect_ratios'
ARH_PREDEFINED_ASPECT_RATIO_USE_MAX_DIM_KEY \
= 'arh_predefined_aspect_ratio_use_max_dim'
ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY \
= 'arh_predefined_percentages_display_key'
DEFAULT_PERCENTAGES_DISPLAY_KEY \
= 'Incremental/decremental percentage (-50%, +50%)'
SECTION = 'aspect_ratio_helper', EXTENSION_NAME

View File

@@ -1,3 +1,5 @@
import itertools
import gradio as gr
from modules import shared
@@ -11,13 +13,15 @@ PREDEFINED_PERCENTAGES_DISPLAY_MAP = {
'Multiplication (x0.5, x1.5)': _util.display_multiplication,
}
ELEMENTS = (
COMPONENTS = (
_components.MaxDimensionScaler,
_components.PredefinedAspectRatioButtons,
_components.PredefinedPercentageButtons,
)
DEFAULT_UI_COMPONENT_ORDER_KEY_LIST = [e.__name__ for e in COMPONENTS]
DEFAULT_UI_COMPONENT_ORDER_KEY = ', '.join(
[e.__name__ for e in ELEMENTS], # noqa
DEFAULT_UI_COMPONENT_ORDER_KEY_LIST,
)
OPT_KEY_TO_DEFAULT_MAP = {
_constants.ARH_EXPAND_BY_DEFAULT_KEY: False,
@@ -31,6 +35,10 @@ OPT_KEY_TO_DEFAULT_MAP = {
'25, 50, 75, 125, 150, 175, 200',
_constants.ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY:
_constants.DEFAULT_PERCENTAGES_DISPLAY_KEY,
_constants.ARH_SHOW_PREDEFINED_ASPECT_RATIOS_KEY: True,
_constants.ARH_PREDEFINED_ASPECT_RATIO_USE_MAX_DIM_KEY: False,
_constants.ARH_PREDEFINED_ASPECT_RATIOS_KEY:
'1:1, 4:3, 16:9, 9:16, 21:9',
}
@@ -38,12 +46,20 @@ def safe_opt(key):
return _util.safe_opt_util(shared.opts, key, OPT_KEY_TO_DEFAULT_MAP)
def sort_elements_by_keys(
elements: list[_components.ArhUIComponent],
def sort_components_by_keys(
components: list[_components.ArhUIComponent],
) -> list[_components.ArhUIComponent]:
ordered_component_keys = safe_opt(
_constants.ARH_UI_COMPONENT_ORDER_KEY,
).split(',')
# this can happen if we add new components, but the user has old settings.
# if this happens, we find the missing components, and append them.
if len(ordered_component_keys) != len(COMPONENTS):
all_components = set(DEFAULT_UI_COMPONENT_ORDER_KEY_LIST)
missing_components = all_components - set(ordered_component_keys)
ordered_component_keys.extend(missing_components)
try:
component_key_to_order_dict = {
key: order for order, key in enumerate(
@@ -51,7 +67,7 @@ def sort_elements_by_keys(
)
}
return sorted(
elements,
components,
key=lambda c: component_key_to_order_dict.get(
c.__class__.__name__,
),
@@ -63,11 +79,13 @@ def sort_elements_by_keys(
f'the intended syntax for the setting, i.e '
f'"{DEFAULT_UI_COMPONENT_ORDER_KEY}"',
)
return elements
return components
def on_ui_settings():
section = 'aspect_ratio_helper', _constants.EXTENSION_NAME
# default ui options
shared.opts.add_option(
key=_constants.ARH_EXPAND_BY_DEFAULT_KEY,
info=shared.OptionInfo(
@@ -85,80 +103,17 @@ def on_ui_settings():
_constants.ARH_UI_COMPONENT_ORDER_KEY,
),
label='UI Component order',
# todo: temporary drop-down to avoid user error!
# we only have two components so 2 possible orders.
# however, this will exponentially grow with more components.
# if that happens, permutations is impractical, revisit then.
component=gr.Dropdown,
component_args=lambda: {
'choices': (
DEFAULT_UI_COMPONENT_ORDER_KEY,
', '.join(
DEFAULT_UI_COMPONENT_ORDER_KEY.split(',')[::-1],
),
),
},
section=section,
),
)
shared.opts.add_option(
key=_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_SHOW_MAX_WIDTH_OR_HEIGHT_KEY,
),
label='Show maximum width or height button',
section=section,
),
)
shared.opts.add_option(
key=_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_MAX_WIDTH_OR_HEIGHT_KEY,
),
label='Maximum width or height default',
component=gr.Slider,
component_args={
'minimum': _constants.MIN_DIMENSION,
'maximum': _constants.MAX_DIMENSION,
'step': 1,
},
section=section,
),
)
shared.opts.add_option(
key=_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_SHOW_PREDEFINED_PERCENTAGES_KEY,
),
label='Show predefined percentage buttons',
section=section,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_PERCENTAGES_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_PERCENTAGES_KEY,
),
label='Predefined percentage buttons, applied to dimensions (75, '
'125, 150)',
section=section,
),
)
shared.opts.add_option(
key=_constants.ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY,
info=shared.OptionInfo(
default=OPT_KEY_TO_DEFAULT_MAP.get(
_constants.ARH_PREDEFINED_PERCENTAGES_DISPLAY_KEY,
),
label='Predefined percentage display format',
component=gr.Dropdown,
component_args=lambda: {
'choices': tuple(PREDEFINED_PERCENTAGES_DISPLAY_MAP.keys()),
'choices': [
', '.join(p) for p in itertools.permutations(
DEFAULT_UI_COMPONENT_ORDER_KEY_LIST,
)
],
},
section=section,
),
)
for component in COMPONENTS:
component.add_options(shared)

View File

@@ -37,10 +37,28 @@ def scale_by_percentage(width, height, pct) -> tuple[int, int]:
return clamp_to_boundaries(new_width, new_height, aspect_ratio)
def scale_dimensions_to(
def scale_dimensions_to_ui_width_or_height(
width, height, arw, arh,
) -> tuple[int, int]:
return scale_dimensions_to_max_dim(arw, arh, max(width, height))
def scale_dimensions_to_max_dim_func(
width, height, max_dim: callable,
) -> tuple[int, int]:
return scale_dimensions_to_max_dim(width, height, max_dim())
def scale_dimensions_to_max_dim(
width, height, max_dim,
) -> tuple[int, int]:
aspect_ratio = float(width) / float(height)
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]:
if width > height:
new_width = max_dim
new_height = int(round(max_dim / aspect_ratio))
@@ -63,4 +81,19 @@ def clamp_to_boundaries(width, height, aspect_ratio) -> tuple[int, int]:
if height < _const.MIN_DIMENSION:
height = _const.MIN_DIMENSION
width = int(round(height * aspect_ratio))
# 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
# on the ui until they change to another reasonable aspect ratio.
# todo: figure out why ^ and handle it better.
if width < _const.MIN_DIMENSION:
width = _const.MIN_DIMENSION
elif width > _const.MAX_DIMENSION:
width = _const.MAX_DIMENSION
if height < _const.MIN_DIMENSION:
height = _const.MIN_DIMENSION
elif height > _const.MAX_DIMENSION:
height = _const.MAX_DIMENSION
return width, height

View File

@@ -9,12 +9,13 @@ import aspect_ratio_helper._settings as _settings
class AspectRatioStepScript(scripts.Script):
def __init__(self):
self.t2i_w = None
self.t2i_h = None
self.i2i_w = None
self.i2i_h = None
self.wc = None
self.hc = None
self.t2i_w: gr.components.Slider | None = None
self.t2i_h: gr.components.Slider | None = None
self.i2i_w: gr.components.Slider | None = None
self.i2i_h: gr.components.Slider | None = None
self.wc: gr.components.Slider
self.hc: gr.components.Slider
self.max_dimension: float
def title(self) -> str:
return _constants.EXTENSION_NAME
@@ -26,14 +27,14 @@ class AspectRatioStepScript(scripts.Script):
if is_img2img:
self.wc, self.hc = self.i2i_w, self.i2i_h
else:
self.wc, self.hc = self.t2i_h, self.t2i_w
self.wc, self.hc = self.t2i_w, self.t2i_h # noqa
elements = _settings.sort_elements_by_keys(
[element(self) for element in _settings.ELEMENTS],
components = _settings.sort_components_by_keys(
[component(self) for component in _settings.COMPONENTS],
)
if not any(element.should_show() for element in elements):
return # no elements should render, so just return.
if not any(component.should_show() for component in components):
return # no components should render, so just return.
start_expanded: bool = _settings.safe_opt(
_constants.ARH_EXPAND_BY_DEFAULT_KEY,
@@ -42,9 +43,11 @@ class AspectRatioStepScript(scripts.Script):
_constants.EXTENSION_NAME,
open=start_expanded,
):
for element in elements:
if element.should_show():
element.render()
for component in components:
# we deliberately DON'T check component.should_show() here.
# we need to call render to instantiate the components, we use
# the visible property on each component to hide them.
component.render()
def after_component(self, component: gr.components.Component, **kwargs):
element_id = kwargs.get('elem_id')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -142,10 +142,10 @@ def test_scale_by_percentage(
),
],
)
def test_scale_dimensions_to(
def test_scale_dimensions_to_max_dim(
width, height, max_dim, expected,
):
assert _util.scale_dimensions_to(
assert _util.scale_dimensions_to_max_dim(
width, height, max_dim,
) == expected