feat: Add Aspect Ratio (#3709)

- Adds Aspect Ratio functionality to the UI
- The ratios are placeholder. Feel free to add any ratios you want.
 

https://github.com/invoke-ai/InvokeAI/assets/54517381/43921f57-fe0a-457f-baf2-b003310d4f85

- I did not add the same to Bounding Box width and height on the canvas.
But its very easy to extend it to that too. So feel free to add if you
want to.
This commit is contained in:
blessedcoolant 2023-07-10 18:12:12 +12:00 committed by GitHub
commit b6fabe5146
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 180 additions and 37 deletions

View File

@ -528,7 +528,8 @@
"hidePreview": "Hide Preview", "hidePreview": "Hide Preview",
"showPreview": "Show Preview", "showPreview": "Show Preview",
"controlNetControlMode": "Control Mode", "controlNetControlMode": "Control Mode",
"clipSkip": "Clip Skip" "clipSkip": "Clip Skip",
"aspectRatio": "Ratio"
}, },
"settings": { "settings": {
"models": "Models", "models": "Models",
@ -671,6 +672,7 @@
}, },
"ui": { "ui": {
"showProgressImages": "Show Progress Images", "showProgressImages": "Show Progress Images",
"hideProgressImages": "Hide Progress Images" "hideProgressImages": "Hide Progress Images",
"swapSizes": "Swap Sizes"
} }
} }

View File

@ -0,0 +1,37 @@
import { ButtonGroup, Flex } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { setAspectRatio } from 'features/ui/store/uiSlice';
const aspectRatios = [
{ name: 'Free', value: null },
{ name: 'Portrait', value: 0.67 / 1 },
{ name: 'Wide', value: 16 / 9 },
{ name: 'Square', value: 1 / 1 },
];
export default function ParamAspectRatio() {
const aspectRatio = useAppSelector(
(state: RootState) => state.ui.aspectRatio
);
const dispatch = useAppDispatch();
return (
<Flex gap={2} flexGrow={1}>
<ButtonGroup isAttached>
{aspectRatios.map((ratio) => (
<IAIButton
key={ratio.name}
size="sm"
isChecked={aspectRatio === ratio.value}
onClick={() => dispatch(setAspectRatio(ratio.value))}
>
{ratio.name}
</IAIButton>
))}
</ButtonGroup>
</Flex>
);
}

View File

@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setHeight } from 'features/parameters/store/generationSlice'; import { setHeight, setWidth } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
[generationSelector, hotkeysSelector, configSelector], [generationSelector, hotkeysSelector, configSelector, uiSelector],
(generation, hotkeys, config) => { (generation, hotkeys, config, ui) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.height; config.sd.height;
const { height } = generation; const { height } = generation;
const { aspectRatio } = ui;
const step = hotkeys.shift ? fineStep : coarseStep; const step = hotkeys.shift ? fineStep : coarseStep;
@ -25,6 +28,7 @@ const selector = createSelector(
sliderMax, sliderMax,
inputMax, inputMax,
step, step,
aspectRatio,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -36,7 +40,7 @@ type ParamHeightProps = Omit<
>; >;
const ParamHeight = (props: ParamHeightProps) => { const ParamHeight = (props: ParamHeightProps) => {
const { height, initial, min, sliderMax, inputMax, step } = const { height, initial, min, sliderMax, inputMax, step, aspectRatio } =
useAppSelector(selector); useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -44,13 +48,21 @@ const ParamHeight = (props: ParamHeightProps) => {
const handleChange = useCallback( const handleChange = useCallback(
(v: number) => { (v: number) => {
dispatch(setHeight(v)); dispatch(setHeight(v));
if (aspectRatio) {
const newWidth = roundToMultiple(v * aspectRatio, 8);
dispatch(setWidth(newWidth));
}
}, },
[dispatch] [dispatch, aspectRatio]
); );
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
dispatch(setHeight(initial)); dispatch(setHeight(initial));
}, [dispatch, initial]); if (aspectRatio) {
const newWidth = roundToMultiple(initial * aspectRatio, 8);
dispatch(setWidth(newWidth));
}
}, [dispatch, initial, aspectRatio]);
return ( return (
<IAISlider <IAISlider

View File

@ -0,0 +1,64 @@
import { Flex, Spacer, Text } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { toggleSize } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
import { MdOutlineSwapVert } from 'react-icons/md';
import ParamAspectRatio from './ParamAspectRatio';
import ParamHeight from './ParamHeight';
import ParamWidth from './ParamWidth';
export default function ParamSize() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const shouldFitToWidthHeight = useAppSelector(
(state: RootState) => state.generation.shouldFitToWidthHeight
);
return (
<Flex
sx={{
gap: 2,
p: 4,
borderRadius: 4,
flexDirection: 'column',
w: 'full',
bg: 'base.150',
_dark: {
bg: 'base.750',
},
}}
>
<Flex alignItems="center" gap={2}>
<Text
sx={{
fontSize: 'sm',
width: 'full',
color: 'base.700',
_dark: {
color: 'base.300',
},
}}
>
{t('parameters.aspectRatio')}
</Text>
<Spacer />
<ParamAspectRatio />
<IAIIconButton
tooltip={t('ui.swapSizes')}
aria-label={t('ui.swapSizes')}
size="sm"
icon={<MdOutlineSwapVert />}
fontSize={20}
onClick={() => dispatch(toggleSize())}
/>
</Flex>
<Flex gap={2} alignItems="center">
<Flex gap={2} flexDirection="column" width="full">
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
</Flex>
</Flex>
</Flex>
);
}

View File

@ -2,19 +2,22 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setWidth } from 'features/parameters/store/generationSlice'; import { setHeight, setWidth } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
[generationSelector, hotkeysSelector, configSelector], [generationSelector, hotkeysSelector, configSelector, uiSelector],
(generation, hotkeys, config) => { (generation, hotkeys, config, ui) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.width; config.sd.width;
const { width } = generation; const { width } = generation;
const { aspectRatio } = ui;
const step = hotkeys.shift ? fineStep : coarseStep; const step = hotkeys.shift ? fineStep : coarseStep;
@ -25,6 +28,7 @@ const selector = createSelector(
sliderMax, sliderMax,
inputMax, inputMax,
step, step,
aspectRatio,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -33,7 +37,7 @@ const selector = createSelector(
type ParamWidthProps = Omit<IAIFullSliderProps, 'label' | 'value' | 'onChange'>; type ParamWidthProps = Omit<IAIFullSliderProps, 'label' | 'value' | 'onChange'>;
const ParamWidth = (props: ParamWidthProps) => { const ParamWidth = (props: ParamWidthProps) => {
const { width, initial, min, sliderMax, inputMax, step } = const { width, initial, min, sliderMax, inputMax, step, aspectRatio } =
useAppSelector(selector); useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -41,13 +45,21 @@ const ParamWidth = (props: ParamWidthProps) => {
const handleChange = useCallback( const handleChange = useCallback(
(v: number) => { (v: number) => {
dispatch(setWidth(v)); dispatch(setWidth(v));
if (aspectRatio) {
const newHeight = roundToMultiple(v / aspectRatio, 8);
dispatch(setHeight(newHeight));
}
}, },
[dispatch] [dispatch, aspectRatio]
); );
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
dispatch(setWidth(initial)); dispatch(setWidth(initial));
}, [dispatch, initial]); if (aspectRatio) {
const newHeight = roundToMultiple(initial / aspectRatio, 8);
dispatch(setHeight(newHeight));
}
}, [dispatch, initial, aspectRatio]);
return ( return (
<IAISlider <IAISlider

View File

@ -1,8 +1,12 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { DEFAULT_SCHEDULER_NAME } from 'app/constants'; import { DEFAULT_SCHEDULER_NAME } from 'app/constants';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { configChanged } from 'features/system/store/configSlice'; import { configChanged } from 'features/system/store/configSlice';
import { setShouldShowAdvancedOptions } from 'features/ui/store/uiSlice'; import {
setAspectRatio,
setShouldShowAdvancedOptions,
} from 'features/ui/store/uiSlice';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip'; import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip';
@ -139,6 +143,11 @@ export const generationSlice = createSlice({
setWidth: (state, action: PayloadAction<number>) => { setWidth: (state, action: PayloadAction<number>) => {
state.width = action.payload; state.width = action.payload;
}, },
toggleSize: (state) => {
const [width, height] = [state.width, state.height];
state.width = height;
state.height = width;
},
setScheduler: (state, action: PayloadAction<SchedulerParam>) => { setScheduler: (state, action: PayloadAction<SchedulerParam>) => {
state.scheduler = action.payload; state.scheduler = action.payload;
}, },
@ -262,6 +271,12 @@ export const generationSlice = createSlice({
const advancedOptionsStatus = action.payload; const advancedOptionsStatus = action.payload;
if (!advancedOptionsStatus) state.clipSkip = 0; if (!advancedOptionsStatus) state.clipSkip = 0;
}); });
builder.addCase(setAspectRatio, (state, action) => {
const ratio = action.payload;
if (ratio) {
state.height = roundToMultiple(state.width / ratio, 8);
}
});
}, },
}); });
@ -271,7 +286,9 @@ export const {
resetParametersState, resetParametersState,
resetSeed, resetSeed,
setCfgScale, setCfgScale,
setWidth,
setHeight, setHeight,
toggleSize,
setImg2imgStrength, setImg2imgStrength,
setInfillMethod, setInfillMethod,
setIterations, setIterations,
@ -292,7 +309,6 @@ export const {
setThreshold, setThreshold,
setTileSize, setTileSize,
setVariationAmount, setVariationAmount,
setWidth,
setShouldUseSymmetry, setShouldUseSymmetry,
setHorizontalSymmetrySteps, setHorizontalSymmetrySteps,
setVerticalSymmetrySteps, setVerticalSymmetrySteps,

View File

@ -4,11 +4,10 @@ import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICollapse from 'common/components/IAICollapse'; import IAICollapse from 'common/components/IAICollapse';
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler';
import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize';
import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps';
import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth';
import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit';
import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength';
import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull';
@ -47,15 +46,14 @@ const ImageToImageTabCoreParameters = () => {
> >
{shouldUseSliders ? ( {shouldUseSliders ? (
<> <>
<ParamIterations />
<ParamSteps />
<ParamCFGScale />
<ParamModelandVAEandScheduler /> <ParamModelandVAEandScheduler />
<Box pt={2}> <Box pt={2}>
<ParamSeedFull /> <ParamSeedFull />
</Box> </Box>
<ParamIterations /> <ParamSize />
<ParamSteps />
<ParamCFGScale />
<ParamWidth isDisabled={!shouldFitToWidthHeight} />
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
</> </>
) : ( ) : (
<> <>
@ -68,8 +66,7 @@ const ImageToImageTabCoreParameters = () => {
<Box pt={2}> <Box pt={2}>
<ParamSeedFull /> <ParamSeedFull />
</Box> </Box>
<ParamWidth isDisabled={!shouldFitToWidthHeight} /> <ParamSize />
<ParamHeight isDisabled={!shouldFitToWidthHeight} />
</> </>
)} )}
<ImageToImageStrength /> <ImageToImageStrength />

View File

@ -5,11 +5,10 @@ import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAICollapse from 'common/components/IAICollapse'; import IAICollapse from 'common/components/IAICollapse';
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler';
import ParamSize from 'features/parameters/components/Parameters/Core/ParamSize';
import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps';
import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth';
import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull'; import ParamSeedFull from 'features/parameters/components/Parameters/Seed/ParamSeedFull';
import { memo } from 'react'; import { memo } from 'react';
@ -43,15 +42,14 @@ const TextToImageTabCoreParameters = () => {
> >
{shouldUseSliders ? ( {shouldUseSliders ? (
<> <>
<ParamIterations />
<ParamSteps />
<ParamCFGScale />
<ParamModelandVAEandScheduler /> <ParamModelandVAEandScheduler />
<Box pt={2}> <Box pt={2}>
<ParamSeedFull /> <ParamSeedFull />
</Box> </Box>
<ParamIterations /> <ParamSize />
<ParamSteps />
<ParamCFGScale />
<ParamWidth />
<ParamHeight />
</> </>
) : ( ) : (
<> <>
@ -64,8 +62,7 @@ const TextToImageTabCoreParameters = () => {
<Box pt={2}> <Box pt={2}>
<ParamSeedFull /> <ParamSeedFull />
</Box> </Box>
<ParamWidth /> <ParamSize />
<ParamHeight />
</> </>
)} )}
</Flex> </Flex>

View File

@ -44,13 +44,13 @@ const UnifiedCanvasCoreParameters = () => {
> >
{shouldUseSliders ? ( {shouldUseSliders ? (
<> <>
<ParamIterations />
<ParamSteps />
<ParamCFGScale />
<ParamModelandVAEandScheduler /> <ParamModelandVAEandScheduler />
<Box pt={2}> <Box pt={2}>
<ParamSeedFull /> <ParamSeedFull />
</Box> </Box>
<ParamIterations />
<ParamSteps />
<ParamCFGScale />
<ParamBoundingBoxWidth /> <ParamBoundingBoxWidth />
<ParamBoundingBoxHeight /> <ParamBoundingBoxHeight />
</> </>

View File

@ -21,6 +21,7 @@ export const initialUIState: UIState = {
shouldShowProgressInViewer: true, shouldShowProgressInViewer: true,
shouldShowEmbeddingPicker: false, shouldShowEmbeddingPicker: false,
shouldShowAdvancedOptions: false, shouldShowAdvancedOptions: false,
aspectRatio: null,
favoriteSchedulers: [], favoriteSchedulers: [],
}; };
@ -104,6 +105,9 @@ export const uiSlice = createSlice({
setShouldShowAdvancedOptions: (state, action: PayloadAction<boolean>) => { setShouldShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
state.shouldShowAdvancedOptions = action.payload; state.shouldShowAdvancedOptions = action.payload;
}, },
setAspectRatio: (state, action: PayloadAction<number | null>) => {
state.aspectRatio = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
builder.addCase(initialImageChanged, (state) => { builder.addCase(initialImageChanged, (state) => {
@ -132,6 +136,7 @@ export const {
favoriteSchedulersChanged, favoriteSchedulersChanged,
toggleEmbeddingPicker, toggleEmbeddingPicker,
setShouldShowAdvancedOptions, setShouldShowAdvancedOptions,
setAspectRatio,
} = uiSlice.actions; } = uiSlice.actions;
export default uiSlice.reducer; export default uiSlice.reducer;

View File

@ -29,5 +29,6 @@ export interface UIState {
shouldShowProgressInViewer: boolean; shouldShowProgressInViewer: boolean;
shouldShowEmbeddingPicker: boolean; shouldShowEmbeddingPicker: boolean;
shouldShowAdvancedOptions: boolean; shouldShowAdvancedOptions: boolean;
aspectRatio: number | null;
favoriteSchedulers: SchedulerParam[]; favoriteSchedulers: SchedulerParam[];
} }