diff --git a/backend/invoke_ai_web_server.py b/backend/invoke_ai_web_server.py index 91af9ed17f..5b15ffc12b 100644 --- a/backend/invoke_ai_web_server.py +++ b/backend/invoke_ai_web_server.py @@ -12,7 +12,7 @@ import os from werkzeug.utils import secure_filename from flask import Flask, redirect, send_from_directory, flash, request, url_for, jsonify from flask_socketio import SocketIO -from PIL import Image +from PIL import Image, ImageOps from PIL.Image import Image as ImageType from uuid import uuid4 from threading import Event @@ -698,11 +698,11 @@ class InvokeAIWebServer: So we need to convert each into a PIL Image. """ + truncated_outpaint_image_b64 = generation_parameters["init_img"][:64] truncated_outpaint_mask_b64 = generation_parameters["init_mask"][:64] init_img_url = generation_parameters["init_img"] - - init_img_url = generation_parameters["init_img"] + init_mask_url = generation_parameters["init_mask"] init_img_path = self.get_image_path_from_url(init_img_url) @@ -723,6 +723,9 @@ class InvokeAIWebServer: generation_parameters["init_mask"] ).convert("L") + if generation_parameters.invert_mask: + mask_image = ImageOps.invert(mask_image) + """ Apply the mask to the init image, creating a "mask" image with transparency where inpainting should occur. This is the kind of diff --git a/frontend/src/common/util/parameterTranslation.ts b/frontend/src/common/util/parameterTranslation.ts index f73da43898..4a816b86db 100644 --- a/frontend/src/common/util/parameterTranslation.ts +++ b/frontend/src/common/util/parameterTranslation.ts @@ -118,6 +118,7 @@ export const frontendToBackendParameters = ( shouldUseInpaintReplace, stageScale, isMaskEnabled, + shouldInvertMask, } = canvasState[canvasState.currentCanvas]; const boundingBox = { @@ -137,6 +138,10 @@ export const frontendToBackendParameters = ( generationParameters.init_img = imageToProcessUrl; generationParameters.strength = img2imgStrength; + if (shouldInvertMask) { + generationParameters.invert_mask = true; + } + if (shouldUseInpaintReplace) { generationParameters.inpaint_replace = inpaintReplace; } diff --git a/frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx b/frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx index b5525d279d..e55127a23e 100644 --- a/frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx +++ b/frontend/src/features/canvas/IAICanvasMaskButtonPopover.tsx @@ -6,6 +6,7 @@ import { setIsMaskEnabled, setLayer, setMaskColor, + setShouldInvertMask, } from './canvasSlice'; import { useAppDispatch, useAppSelector } from 'app/store'; import _ from 'lodash'; @@ -19,12 +20,13 @@ import IAIButton from 'common/components/IAIButton'; export const selector = createSelector( [currentCanvasSelector], (currentCanvas) => { - const { maskColor, layer, isMaskEnabled } = currentCanvas; + const { maskColor, layer, isMaskEnabled, shouldInvertMask } = currentCanvas; return { layer, maskColor, isMaskEnabled, + shouldInvertMask, }; }, { @@ -35,7 +37,8 @@ export const selector = createSelector( ); const IAICanvasMaskButtonPopover = () => { const dispatch = useAppDispatch(); - const { layer, maskColor, isMaskEnabled } = useAppSelector(selector); + const { layer, maskColor, isMaskEnabled, shouldInvertMask } = + useAppSelector(selector); return ( { isChecked={isMaskEnabled} onChange={(e) => dispatch(setIsMaskEnabled(e.target.checked))} /> - + dispatch(setShouldInvertMask(e.target.checked))} + /> dispatch(setMaskColor(newColor))} diff --git a/frontend/src/features/canvas/IAICanvasMaskCompositer.tsx b/frontend/src/features/canvas/IAICanvasMaskCompositer.tsx index 93c91ec619..1f9c46ee19 100644 --- a/frontend/src/features/canvas/IAICanvasMaskCompositer.tsx +++ b/frontend/src/features/canvas/IAICanvasMaskCompositer.tsx @@ -15,14 +15,20 @@ import Konva from 'konva'; export const canvasMaskCompositerSelector = createSelector( currentCanvasSelector, (currentCanvas) => { - const { maskColor, stageCoordinates, stageDimensions, stageScale } = - currentCanvas as InpaintingCanvasState | OutpaintingCanvasState; + const { + maskColor, + stageCoordinates, + stageDimensions, + stageScale, + shouldInvertMask, + } = currentCanvas as InpaintingCanvasState | OutpaintingCanvasState; return { stageCoordinates, stageDimensions, stageScale, maskColorString: rgbaColorToString(maskColor), + shouldInvertMask, }; } ); @@ -114,8 +120,13 @@ const getColoredSVG = (color: string) => { const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { const { ...rest } = props; - const { maskColorString, stageCoordinates, stageDimensions, stageScale } = - useAppSelector(canvasMaskCompositerSelector); + const { + maskColorString, + stageCoordinates, + stageDimensions, + stageScale, + shouldInvertMask, + } = useAppSelector(canvasMaskCompositerSelector); const [fillPatternImage, setFillPatternImage] = useState(null); @@ -162,7 +173,7 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => { fillPatternRepeat={'repeat'} fillPatternScale={{ x: 1 / stageScale, y: 1 / stageScale }} listening={true} - globalCompositeOperation={'source-in'} + globalCompositeOperation={shouldInvertMask ? 'source-out' : 'source-in'} {...rest} /> ); diff --git a/frontend/src/features/canvas/canvasSlice.ts b/frontend/src/features/canvas/canvasSlice.ts index d1535105f0..7ff15ce6e7 100644 --- a/frontend/src/features/canvas/canvasSlice.ts +++ b/frontend/src/features/canvas/canvasSlice.ts @@ -546,25 +546,38 @@ export const canvasSlice = createSlice({ }); }, addLine: (state, action: PayloadAction) => { - const { tool, layer, brushColor, brushSize, eraserSize } = - state[state.currentCanvas]; + const currentCanvas = state[state.currentCanvas]; + + const { tool, layer, brushColor, brushSize, eraserSize } = currentCanvas; if (tool === 'move') return; - state[state.currentCanvas].pastObjects.push( - state[state.currentCanvas].objects - ); + let newTool: CanvasDrawingTool; - state[state.currentCanvas].objects.push({ + if (layer === 'mask' && currentCanvas.shouldInvertMask) { + newTool = tool === 'eraser' ? 'brush' : 'eraser'; + } else { + newTool = tool; + } + + const newStrokeWidth = tool === 'brush' ? brushSize / 2 : eraserSize / 2; + + // set & then spread this to only conditionally add the "color" key + const newColor = + layer === 'base' && tool === 'brush' ? { color: brushColor } : {}; + + currentCanvas.pastObjects.push(currentCanvas.objects); + + currentCanvas.objects.push({ kind: 'line', layer, - tool, - strokeWidth: tool === 'brush' ? brushSize / 2 : eraserSize / 2, + tool: newTool, + strokeWidth: newStrokeWidth, points: action.payload, - ...(layer === 'base' && tool === 'brush' && { color: brushColor }), + ...newColor, }); - state[state.currentCanvas].futureObjects = []; + currentCanvas.futureObjects = []; }, addPointToCurrentLine: (state, action: PayloadAction) => { const lastLine = @@ -579,14 +592,18 @@ export const canvasSlice = createSlice({ const newObjects = state[state.currentCanvas].pastObjects.pop(); if (!newObjects) return; - state[state.currentCanvas].futureObjects.unshift(state[state.currentCanvas].objects); + state[state.currentCanvas].futureObjects.unshift( + state[state.currentCanvas].objects + ); state[state.currentCanvas].objects = newObjects; }, redo: (state) => { if (state[state.currentCanvas].futureObjects.length === 0) return; const newObjects = state[state.currentCanvas].futureObjects.shift(); if (!newObjects) return; - state[state.currentCanvas].pastObjects.push(state[state.currentCanvas].objects); + state[state.currentCanvas].pastObjects.push( + state[state.currentCanvas].objects + ); state[state.currentCanvas].objects = newObjects; }, setShouldShowGrid: (state, action: PayloadAction) => {