diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index f340cd70a7..1a539f8f21 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -6,7 +6,7 @@ import { getControlLayerWarnings } from 'features/controlLayers/store/validators import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; import { serializeError } from 'serialize-error'; -import type { ImageDTO, Invocation } from 'services/api/types'; +import type { ImageDTO, Invocation, MainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; const log = logger('system'); @@ -114,7 +114,7 @@ type AddControlLoRAArg = { entities: CanvasControlLayerState[]; g: Graph; rect: Rect; - model: ParameterModel; + model: MainModelConfig; denoise: Invocation<'flux_denoise'>; }; @@ -129,9 +129,9 @@ export const addControlLoRA = async ({ manager, entities, g, rect, model, denois // No valid control LoRA found return; } - if (validControlLayers.length > 1) { - throw new Error('Cannot add more than one FLUX control LoRA.'); - } + + assert(model.variant !== 'inpaint', 'FLUX Control LoRA is not compatible with FLUX Fill.'); + assert(validControlLayers.length <= 1, 'Cannot add more than one FLUX control LoRA.'); const getImageDTOResult = await withResultAsync(() => { const adapter = manager.adapters.controlLayers.get(validControlLayer.id); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index baf6974d22..fb541b2ee9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -39,7 +39,7 @@ export const addInpaint = async ({ scaledSize, denoising_start, fp32, -}: AddInpaintArg): Promise> => { +}: AddInpaintArg): Promise> => { denoise.denoising_start = denoising_start; const params = selectParamsSlice(state); @@ -104,10 +104,10 @@ export const addInpaint = async ({ edge_radius: params.canvasCoherenceEdgeSize, fp32, }); - const canvasPasteBack = g.addNode({ - id: getPrefixedId('canvas_v2_mask_and_crop'), - type: 'canvas_v2_mask_and_crop', - mask_blur: params.maskBlur, + const expandMask = g.addNode({ + type: 'expand_mask_with_fade', + id: getPrefixedId('expand_mask_with_fade'), + fade_size_px: params.maskBlur, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -128,18 +128,31 @@ export const addInpaint = async ({ // After denoising, resize the image and mask back to original size g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); + g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask'); - // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); - g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - + // After denoising, resize the image and mask back to original size // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending // to canvas but not outputting only masked regions if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) { - canvasPasteBack.source_image = { image_name: initialImage.image_name }; + const imageLayerBlend = g.addNode({ + type: 'invokeai_img_blend', + id: getPrefixedId('image_layer_blend'), + layer_base: { image_name: initialImage.image_name }, + }); + g.addEdge(resizeImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper'); + g.addEdge(resizeMaskToOriginalSize, 'image', imageLayerBlend, 'mask'); + return imageLayerBlend; + } else { + // Otherwise, just apply the mask + const applyMaskToImage = g.addNode({ + type: 'apply_mask_to_image', + id: getPrefixedId('apply_mask_to_image'), + invert_mask: true, + }); + g.addEdge(resizeMaskToOriginalSize, 'image', applyMaskToImage, 'mask'); + g.addEdge(resizeImageToOriginalSize, 'image', applyMaskToImage, 'image'); + return applyMaskToImage; } - - return canvasPasteBack; } else { // No scale before processing, much simpler const i2l = g.addNode({ @@ -164,11 +177,6 @@ export const addInpaint = async ({ fp32, image: { image_name: initialImage.image_name }, }); - const canvasPasteBack = g.addNode({ - id: getPrefixedId('canvas_v2_mask_and_crop'), - type: 'canvas_v2_mask_and_crop', - mask_blur: params.maskBlur, - }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); @@ -178,16 +186,35 @@ export const addInpaint = async ({ g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); } g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); - g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); + const expandMask = g.addNode({ + type: 'expand_mask_with_fade', + id: getPrefixedId('expand_mask_with_fade'), + fade_size_px: params.maskBlur, + }); + g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask'); // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending // to canvas but not outputting only masked regions if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) { - canvasPasteBack.source_image = { image_name: initialImage.image_name }; + const imageLayerBlend = g.addNode({ + type: 'invokeai_img_blend', + id: getPrefixedId('image_layer_blend'), + layer_base: { image_name: initialImage.image_name }, + }); + g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper'); + g.addEdge(expandMask, 'image', imageLayerBlend, 'mask'); + return imageLayerBlend; + } else { + // Otherwise, just apply the mask + const applyMaskToImage = g.addNode({ + type: 'apply_mask_to_image', + id: getPrefixedId('apply_mask_to_image'), + invert_mask: true, + }); + g.addEdge(expandMask, 'image', applyMaskToImage, 'mask'); + g.addEdge(l2i, 'image', applyMaskToImage, 'image'); + return applyMaskToImage; } - - return canvasPasteBack; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index f1a08faab1..405e778352 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types'; export const addNSFWChecker = ( g: Graph, imageOutput: Invocation< - 'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i' + | 'l2i' + | 'img_nsfw' + | 'img_watermark' + | 'img_resize' + | 'invokeai_img_blend' + | 'apply_mask_to_image' + | 'flux_vae_decode' + | 'sd3_l2i' > ): Invocation<'img_nsfw'> => { const nsfw = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index a1d6ddb738..98f40b76c2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -40,7 +40,7 @@ export const addOutpaint = async ({ scaledSize, denoising_start, fp32, -}: AddOutpaintArg): Promise> => { +}: AddOutpaintArg): Promise> => { denoise.denoising_start = denoising_start; const params = selectParamsSlice(state); @@ -142,29 +142,39 @@ export const addOutpaint = async ({ type: 'img_resize', ...originalSize, }); - const canvasPasteBack = g.addNode({ - id: getPrefixedId('canvas_v2_mask_and_crop'), - type: 'canvas_v2_mask_and_crop', - mask_blur: params.maskBlur, + const expandMask = g.addNode({ + type: 'expand_mask_with_fade', + id: getPrefixedId('expand_mask_with_fade'), + fade_size_px: params.maskBlur, }); - // Resize initial image and mask to scaled size, feed into to gradient mask // After denoising, resize the image and mask back to original size g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image'); - g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image'); - - // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); - g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - + g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask'); + g.addEdge(expandMask, 'image', resizeOutputMaskToOriginalSize, 'image'); // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending // to canvas but not outputting only masked regions if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) { - canvasPasteBack.source_image = { image_name: initialImage.image_name }; + const imageLayerBlend = g.addNode({ + type: 'invokeai_img_blend', + id: getPrefixedId('image_layer_blend'), + layer_base: { image_name: initialImage.image_name }, + }); + g.addEdge(resizeOutputImageToOriginalSize, 'image', imageLayerBlend, 'layer_upper'); + g.addEdge(resizeOutputMaskToOriginalSize, 'image', imageLayerBlend, 'mask'); + return imageLayerBlend; + } else { + // Otherwise, just apply the mask + const applyMaskToImage = g.addNode({ + type: 'apply_mask_to_image', + id: getPrefixedId('apply_mask_to_image'), + invert_mask: true, + }); + g.addEdge(resizeOutputMaskToOriginalSize, 'image', applyMaskToImage, 'mask'); + g.addEdge(resizeOutputImageToOriginalSize, 'image', applyMaskToImage, 'image'); + return applyMaskToImage; } - - return canvasPasteBack; } else { infill.image = { image_name: initialImage.image_name }; // No scale before processing, much simpler @@ -197,11 +207,6 @@ export const addOutpaint = async ({ fp32, image: { image_name: initialImage.image_name }, }); - const canvasPasteBack = g.addNode({ - id: getPrefixedId('canvas_v2_mask_and_crop'), - type: 'canvas_v2_mask_and_crop', - mask_blur: params.maskBlur, - }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); g.addEdge(maskCombine, 'image', createGradientMask, 'mask'); @@ -214,15 +219,35 @@ export const addOutpaint = async ({ } g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); - g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); + + const expandMask = g.addNode({ + type: 'expand_mask_with_fade', + id: getPrefixedId('expand_mask_with_fade'), + fade_size_px: params.maskBlur, + }); + g.addEdge(createGradientMask, 'expanded_mask_area', expandMask, 'mask'); // Do the paste back if we are sending to gallery (in which case we want to see the full image), or if we are sending // to canvas but not outputting only masked regions if (!canvasSettings.sendToCanvas || !canvasSettings.outputOnlyMaskedRegions) { - canvasPasteBack.source_image = { image_name: initialImage.image_name }; + const imageLayerBlend = g.addNode({ + type: 'invokeai_img_blend', + id: getPrefixedId('image_layer_blend'), + layer_base: { image_name: initialImage.image_name }, + }); + g.addEdge(l2i, 'image', imageLayerBlend, 'layer_upper'); + g.addEdge(expandMask, 'image', imageLayerBlend, 'mask'); + return imageLayerBlend; + } else { + // Otherwise, just apply the mask + const applyMaskToImage = g.addNode({ + type: 'apply_mask_to_image', + id: getPrefixedId('apply_mask_to_image'), + invert_mask: true, + }); + g.addEdge(expandMask, 'image', applyMaskToImage, 'mask'); + g.addEdge(l2i, 'image', applyMaskToImage, 'image'); + return applyMaskToImage; } - - return canvasPasteBack; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index 4672169393..a61b9a1cdb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -11,7 +11,14 @@ import type { Invocation } from 'services/api/types'; export const addWatermarker = ( g: Graph, imageOutput: Invocation< - 'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i' + | 'l2i' + | 'img_nsfw' + | 'img_watermark' + | 'img_resize' + | 'invokeai_img_blend' + | 'apply_mask_to_image' + | 'flux_vae_decode' + | 'sd3_l2i' > ): Invocation<'img_watermark'> => { const watermark = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 7cb3119162..2c76f98f16 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -170,7 +170,14 @@ export const buildSD1Graph = async ( const denoising_start = 1 - params.img2imgStrength; let canvasOutput: Invocation< - 'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i' + | 'l2i' + | 'img_nsfw' + | 'img_watermark' + | 'img_resize' + | 'invokeai_img_blend' + | 'apply_mask_to_image' + | 'flux_vae_decode' + | 'sd3_l2i' > = l2i; if (generationMode === 'txt2img') { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts index 8c0c8edf4b..ccd677cfdd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts @@ -135,7 +135,14 @@ export const buildSD3Graph = async ( } let canvasOutput: Invocation< - 'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i' + | 'l2i' + | 'img_nsfw' + | 'img_watermark' + | 'img_resize' + | 'invokeai_img_blend' + | 'apply_mask_to_image' + | 'flux_vae_decode' + | 'sd3_l2i' > = l2i; if (generationMode === 'txt2img') { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 02fb7e7035..25ba96b2ce 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -175,7 +175,14 @@ export const buildSDXLGraph = async ( : 1 - params.img2imgStrength; let canvasOutput: Invocation< - 'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop' | 'flux_vae_decode' | 'sd3_l2i' + | 'l2i' + | 'img_nsfw' + | 'img_watermark' + | 'img_resize' + | 'invokeai_img_blend' + | 'apply_mask_to_image' + | 'flux_vae_decode' + | 'sd3_l2i' > = l2i; if (generationMode === 'txt2img') {