Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: control adapters in control layers canvas #6287

Merged
merged 50 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
91a13b6
feat(ui): add 'control_layer' type
psychedelicious Apr 24, 2024
feb9373
refactor(ui): move positive and negative prompt to regional
psychedelicious Apr 25, 2024
e463c02
refactor(ui): move positive2 and negative2 prompt to regional
psychedelicious Apr 25, 2024
b78648d
refactor(ui): move size state to regional
psychedelicious Apr 25, 2024
3f0d132
chore(ui): lint
psychedelicious Apr 25, 2024
bb4fae2
feat(ui): revise internal state for RCC
psychedelicious Apr 26, 2024
0efe289
tidy(ui): minor ca component tidy
psychedelicious Apr 29, 2024
e482b87
fix(backend): do not round image dims to 64 in controlnet processor r…
psychedelicious Apr 29, 2024
e876133
feat(nodes): add prototype heuristic image resize node
psychedelicious Apr 29, 2024
869b5c2
WIP control adapters in regional
psychedelicious Apr 29, 2024
5f62e6b
perf(ui): do not cache controlnet images unless required
psychedelicious Apr 29, 2024
a0f9208
fix(ui): fix canvas scaling when window is zoomed
psychedelicious Apr 29, 2024
4f3f097
feat(ui): reset controlnet model to null instead of disabling when ba…
psychedelicious Apr 29, 2024
b2e675e
perf(ui): reduce control image processing to when it is needed
psychedelicious Apr 29, 2024
eb6113b
fix(ui): toggle control adapter layer vis
psychedelicious Apr 29, 2024
048340c
fix(ui): exclude disabled control adapters on control layers
psychedelicious Apr 30, 2024
6b12d95
fix(ui): use optimal size when using control image dims
psychedelicious Apr 30, 2024
e91b740
fix(ui): select default control/ip adapter models in control layers
psychedelicious Apr 30, 2024
eb5f1e3
fix(ui): delete control layers correctly
psychedelicious Apr 30, 2024
9e8e42d
feat(ui): make control layer ui exclusive to txt2img tab
psychedelicious Apr 30, 2024
80269c5
feat(ui): update layer menus
psychedelicious Apr 30, 2024
497d505
feat(ui): fix layer arranging
psychedelicious Apr 30, 2024
d9615d1
feat(ui): separate ca layer opacity
psychedelicious Apr 30, 2024
e5f4cdc
feat(ui): hold shift to use control image size w/o model constraints
psychedelicious Apr 30, 2024
f6a6b35
fix(ui): add missed ca layer opacity logic
psychedelicious Apr 30, 2024
0c04b90
fix(ui): unlink control adapter opaicty from global mask opacity
psychedelicious Apr 30, 2024
2f89697
feat(ui): remove select layer on click in canvas
psychedelicious Apr 30, 2024
b40ac2c
feat(ui): tweak layer list styling to better indicate selectablility
psychedelicious Apr 30, 2024
84925f5
fix(ui): tool preview/cursor when non-interactable layer selected
psychedelicious Apr 30, 2024
776e6d7
fix(ui): deselect other layers when new layer added
psychedelicious Apr 30, 2024
62b4848
feat(ui): move global mask opacity to settings popover
psychedelicious Apr 30, 2024
d23f8ae
fix(ui): ip adapter layers always at bottom of list
psychedelicious Apr 30, 2024
5b5d43c
feat(ui): layer layout tweaks
psychedelicious Apr 30, 2024
3582cc5
fix(ui): filter layers based on tab when disabling invoke button
psychedelicious Apr 30, 2024
65bc2b8
perf(ui): reset maskobjects when layer has no bbox (all objects erased)
psychedelicious Apr 30, 2024
b9ad792
fix(ui): ip adapter layers are not selectable
psychedelicious Apr 30, 2024
40d8663
chore(ui): lint
psychedelicious Apr 30, 2024
63b4ece
tidy(ui): move regionalPrompts files to controlLayers
psychedelicious Apr 30, 2024
f2d5dbe
tidy(ui): "regional prompts" -> "control layers"
psychedelicious Apr 30, 2024
a8ba560
tidy(ui): more renaming of components
psychedelicious Apr 30, 2024
a655a77
tidy(ui): more renaming of components
psychedelicious Apr 30, 2024
2fc8e74
tidy(ui): more renaming of components
psychedelicious Apr 30, 2024
1bd7aca
feat(ui): make control image opacity filter toggleable
psychedelicious Apr 30, 2024
7841182
chore(ui): lint
psychedelicious Apr 30, 2024
d97acd3
feat(ui): collapsible layers
psychedelicious Apr 30, 2024
93c2f76
fix(ui): layers default to expanded
psychedelicious Apr 30, 2024
c8508af
fix(ui): "Global Settings" -> "Settings"
psychedelicious Apr 30, 2024
89a85cd
fix(ui): layer layout orientation
psychedelicious Apr 30, 2024
81e9d6e
feat(ui): display message when no layers are added
psychedelicious Apr 30, 2024
4d4c250
feat(ui): border radius on canvas
psychedelicious Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions invokeai/app/invocations/controlnet_image_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES, heuristic_resize
from invokeai.backend.image_util.canny import get_canny_edges
from invokeai.backend.image_util.depth_anything import DepthAnythingDetector
from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector
from invokeai.backend.image_util.hed import HEDProcessor
from invokeai.backend.image_util.lineart import LineartProcessor
from invokeai.backend.image_util.lineart_anime import LineartAnimeProcessor
from invokeai.backend.image_util.util import np_to_pil, pil_to_np

from .baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from .baseinvocation import BaseInvocation, BaseInvocationOutput, Classification, invocation, invocation_output


class ControlField(BaseModel):
Expand Down Expand Up @@ -634,3 +635,27 @@ def run_processor(self, image: Image.Image):
resolution=self.image_resolution,
)
return processed_image


@invocation(
"heuristic_resize",
title="Heuristic Resize",
tags=["image, controlnet"],
category="image",
version="1.0.0",
classification=Classification.Prototype,
)
class HeuristicResizeInvocation(BaseInvocation):
"""Resize an image using a heuristic method. Preserves edge maps."""

image: ImageField = InputField(description="The image to resize")
width: int = InputField(default=512, gt=0, description="The width to resize to (px)")
height: int = InputField(default=512, gt=0, description="The height to resize to (px)")

def invoke(self, context: InvocationContext) -> ImageOutput:
image = context.images.get_pil(self.image.image_name, "RGB")
np_img = pil_to_np(image)
np_resized = heuristic_resize(np_img, (self.width, self.height))
resized = np_to_pil(np_resized)
image_dto = context.images.save(image=resized)
return ImageOutput.build(image_dto)
6 changes: 2 additions & 4 deletions invokeai/backend/image_util/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,8 @@ def resize_image_to_resolution(input_image: np.ndarray, resolution: int) -> np.n
h = float(input_image.shape[0])
w = float(input_image.shape[1])
scaling_factor = float(resolution) / min(h, w)
h *= scaling_factor
w *= scaling_factor
h = int(np.round(h / 64.0)) * 64
w = int(np.round(w / 64.0)) * 64
h = int(h * scaling_factor)
w = int(w * scaling_factor)
if scaling_factor > 1:
return cv2.resize(input_image, (w, h), interpolation=cv2.INTER_LANCZOS4)
else:
Expand Down
1 change: 1 addition & 0 deletions invokeai/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.5",
"use-debounce": "^10.0.0",
"use-device-pixel-ratio": "^1.1.2",
"use-image": "^1.1.1",
"uuid": "^9.0.1",
"zod": "^3.22.4",
Expand Down
11 changes: 11 additions & 0 deletions invokeai/frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 16 additions & 5 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"balanced": "Balanced",
"base": "Base",
"beginEndStepPercent": "Begin / End Step Percentage",
"beginEndStepPercentShort": "Begin/End %",
"bgth": "bg_th",
"canny": "Canny",
"cannyDescription": "Canny edge detection",
Expand Down Expand Up @@ -227,7 +228,8 @@
"scribble": "scribble",
"selectModel": "Select a model",
"selectCLIPVisionModel": "Select a CLIP Vision model",
"setControlImageDimensions": "Set Control Image Dimensions To W/H",
"setControlImageDimensions": "Copy size to W/H (optimize for model)",
"setControlImageDimensionsForce": "Copy size to W/H (ignore model)",
"showAdvanced": "Show Advanced",
"small": "Small",
"toggleControlNet": "Toggle this ControlNet",
Expand Down Expand Up @@ -1511,16 +1513,15 @@
"app": {
"storeNotInitialized": "Store is not initialized"
},
"regionalPrompts": {
"controlLayers": {
"deleteAll": "Delete All",
"addLayer": "Add Layer",
"moveToFront": "Move to Front",
"moveToBack": "Move to Back",
"moveForward": "Move Forward",
"moveBackward": "Move Backward",
"brushSize": "Brush Size",
"regionalControl": "Regional Control (ALPHA)",
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
"controlLayers": "Control Layers (BETA)",
"globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative",
"toggleVisibility": "Toggle Layer Visibility",
Expand All @@ -1531,6 +1532,16 @@
"maskPreviewColor": "Mask Preview Color",
"addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)"
"addIPAdapter": "Add $t(common.ipAdapter)",
"regionalGuidance": "Regional Guidance",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)",
"ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)",
"opacity": "Opacity",
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"opacityFilter": "Opacity Filter"
}
}
2 changes: 1 addition & 1 deletion invokeai/frontend/web/src/app/logging/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type LoggerNamespace =
| 'session'
| 'queue'
| 'dnd'
| 'regionalPrompts';
| 'controlLayers';

export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
import { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
Expand Down Expand Up @@ -157,3 +158,5 @@ addUpscaleRequestedListener(startAppListening);
addDynamicPromptsListener(startAppListening);

addSetDefaultSettingsListener(startAppListening);

addControlLayersToControlAdapterBridge(startAppListening);
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
})
).unwrap();

const { image_name } = imageDTO;

dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
controlImage: imageDTO,
})
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,10 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
})
).unwrap();

const { image_name } = imageDTO;

dispatch(
controlAdapterImageChanged({
id,
controlImage: image_name,
controlImage: imageDTO,
})
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types';
import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types';
import {
controlAdapterLayerAdded,
ipAdapterLayerAdded,
layerDeleted,
maskLayerIPAdapterAdded,
maskLayerIPAdapterDeleted,
regionalGuidanceLayerAdded,
} from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import { isControlNetModelConfig, isIPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';

export const guidanceLayerAdded = createAction<Layer['type']>('controlLayers/guidanceLayerAdded');
export const guidanceLayerDeleted = createAction<string>('controlLayers/guidanceLayerDeleted');
export const allLayersDeleted = createAction('controlLayers/allLayersDeleted');
export const guidanceLayerIPAdapterAdded = createAction<string>('controlLayers/guidanceLayerIPAdapterAdded');
export const guidanceLayerIPAdapterDeleted = createAction<{ layerId: string; ipAdapterId: string }>(
'controlLayers/guidanceLayerIPAdapterDeleted'
);

export const addControlLayersToControlAdapterBridge = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: guidanceLayerAdded,
effect: (action, { dispatch, getState }) => {
const type = action.payload;
const layerId = uuidv4();
if (type === 'regional_guidance_layer') {
dispatch(regionalGuidanceLayerAdded({ layerId }));
return;
}

const state = getState();
const baseModel = state.generation.model?.base;
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;

if (type === 'ip_adapter_layer') {
const ipAdapterId = uuidv4();
const overrides: Partial<IPAdapterConfig> = {
id: ipAdapterId,
};

// Find and select the first matching model
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
overrides.model = models.find((m) => m.base === baseModel) ?? null;
}
dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
dispatch(ipAdapterLayerAdded({ layerId, ipAdapterId }));
return;
}

if (type === 'control_adapter_layer') {
const controlNetId = uuidv4();
const overrides: Partial<ControlNetConfig> = {
id: controlNetId,
};

// Find and select the first matching model
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isControlNetModelConfig);
const model = models.find((m) => m.base === baseModel) ?? null;
overrides.model = model;
const defaultPreprocessor = model?.default_settings?.preprocessor;
overrides.processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none';
overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel);
}
dispatch(controlAdapterAdded({ type: 'controlnet', overrides }));
dispatch(controlAdapterLayerAdded({ layerId, controlNetId }));
return;
}
},
});

startAppListening({
actionCreator: guidanceLayerDeleted,
effect: (action, { getState, dispatch }) => {
const layerId = action.payload;
const state = getState();
const layer = state.controlLayers.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);

if (layer.type === 'ip_adapter_layer') {
dispatch(controlAdapterRemoved({ id: layer.ipAdapterId }));
} else if (layer.type === 'control_adapter_layer') {
dispatch(controlAdapterRemoved({ id: layer.controlNetId }));
} else if (layer.type === 'regional_guidance_layer') {
for (const ipAdapterId of layer.ipAdapterIds) {
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
}
}
dispatch(layerDeleted(layerId));
},
});

startAppListening({
actionCreator: allLayersDeleted,
effect: (action, { dispatch, getOriginalState }) => {
const state = getOriginalState();
for (const layer of state.controlLayers.present.layers) {
dispatch(guidanceLayerDeleted(layer.id));
}
},
});

startAppListening({
actionCreator: guidanceLayerIPAdapterAdded,
effect: (action, { dispatch, getState }) => {
const layerId = action.payload;
const ipAdapterId = uuidv4();
const overrides: Partial<IPAdapterConfig> = {
id: ipAdapterId,
};

// Find and select the first matching model
const state = getState();
const baseModel = state.generation.model?.base;
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
overrides.model = models.find((m) => m.base === baseModel) ?? null;
}

dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
dispatch(maskLayerIPAdapterAdded({ layerId, ipAdapterId }));
},
});

startAppListening({
actionCreator: guidanceLayerIPAdapterDeleted,
effect: (action, { dispatch }) => {
const { layerId, ipAdapterId } = action.payload;
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
dispatch(maskLayerIPAdapterDeleted({ layerId, ipAdapterId }));
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { isEqual } from 'lodash-es';

type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessorParamsChanged>
Expand Down Expand Up @@ -52,6 +53,11 @@ const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) =>
return false;
}

if (prevCA.controlImage === ca.controlImage && isEqual(prevCA.processorNode, ca.processorNode)) {
// Don't re-process if the processor hasn't changed
return false;
}

const isProcessorSelected = processorType !== 'none';

const hasControlImage = Boolean(controlImage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL
dispatch(
controlAdapterProcessedImageChanged({
id,
processedControlImage: processedControlImage.image_name,
processedControlImage,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
dispatch(
controlAdapterImageChanged({
id,
controlImage: activeData.payload.imageDTO.image_name,
controlImage: activeData.payload.imageDTO,
})
);
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO.image_name,
controlImage: imageDTO,
})
);
dispatch(
Expand Down
Loading
Loading