Skip to content

Commit dd0b398

Browse files
Fix Invert Mask option.
1 parent 4af7aee commit dd0b398

File tree

2 files changed

+323
-33
lines changed

2 files changed

+323
-33
lines changed

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ import {
1515
selectRegionalGuidanceReferenceImage,
1616
} from 'features/controlLayers/store/selectors';
1717
import type {
18+
CanvasBrushLineState,
19+
CanvasBrushLineWithPressureState,
1820
CanvasEntityStateFromType,
1921
CanvasEntityType,
22+
CanvasEraserLineState,
23+
CanvasEraserLineWithPressureState,
2024
CanvasInpaintMaskState,
2125
CanvasMetadata,
26+
CanvasRectState,
2227
ControlLoRAConfig,
2328
EntityMovedByPayload,
2429
FillStyle,
@@ -1005,11 +1010,16 @@ export const canvasSlice = createSlice({
10051010
return;
10061011
}
10071012

1008-
// Create a rectangle covering the current bounding box relative to the entity position
1013+
// For now, we'll use a simple approach: create a full rectangle and add eraser lines
1014+
// This is a temporary solution until we can properly handle the bitmap conversion
1015+
1016+
// Get the bbox dimensions for the mask
10091017
const bboxRect = state.bbox.rect;
1010-
const fillRectObject = {
1018+
1019+
// Create a full rectangle covering the bbox
1020+
const fillRect: CanvasRectState = {
10111021
id: getPrefixedId('rect'),
1012-
type: 'rect' as const,
1022+
type: 'rect',
10131023
rect: {
10141024
x: bboxRect.x - entity.position.x,
10151025
y: bboxRect.y - entity.position.y,
@@ -1019,52 +1029,89 @@ export const canvasSlice = createSlice({
10191029
color: { r: 255, g: 255, b: 255, a: 1 },
10201030
};
10211031

1022-
// To invert a mask, we need to:
1023-
// 1. Start with a full rectangle covering the bbox (this becomes the "base mask")
1024-
// 2. Convert existing brush/rect objects to eraser lines to "punch holes" in the base mask
1025-
const convertedObjects = entity.objects.map((obj) => {
1032+
// Convert existing brush lines to eraser lines to "punch holes" in the full rectangle
1033+
const invertedObjects: (
1034+
| CanvasRectState
1035+
| CanvasEraserLineState
1036+
| CanvasEraserLineWithPressureState
1037+
| CanvasBrushLineState
1038+
| CanvasBrushLineWithPressureState
1039+
)[] = [fillRect];
1040+
1041+
for (const obj of entity.objects) {
10261042
if (obj.type === 'brush_line') {
1027-
// Convert brush lines to eraser lines
1028-
return {
1029-
...obj,
1043+
// Convert brush line to eraser line
1044+
const eraserLine: CanvasEraserLineState = {
10301045
id: getPrefixedId('eraser_line'),
1031-
type: 'eraser_line' as const,
1046+
type: 'eraser_line',
1047+
strokeWidth: obj.strokeWidth,
1048+
points: obj.points,
1049+
clip: obj.clip,
10321050
};
1051+
invertedObjects.push(eraserLine);
10331052
} else if (obj.type === 'brush_line_with_pressure') {
1034-
// Convert brush lines with pressure to eraser lines with pressure
1035-
return {
1036-
...obj,
1053+
// Convert brush line with pressure to eraser line with pressure
1054+
const eraserLine: CanvasEraserLineWithPressureState = {
10371055
id: getPrefixedId('eraser_line'),
1038-
type: 'eraser_line_with_pressure' as const,
1056+
type: 'eraser_line_with_pressure',
1057+
strokeWidth: obj.strokeWidth,
1058+
points: obj.points,
1059+
clip: obj.clip,
10391060
};
1061+
invertedObjects.push(eraserLine);
10401062
} else if (obj.type === 'rect') {
1041-
// Convert rectangles to eraser "rectangles" by making them transparent
1042-
return {
1043-
...obj,
1044-
id: getPrefixedId('rect'),
1045-
color: { ...obj.color, a: 0 }, // Make transparent to act as eraser
1063+
// Convert rectangle to eraser rectangle (we'll use eraser lines to trace the rectangle)
1064+
const { x, y, width, height } = obj.rect;
1065+
const points = [
1066+
x,
1067+
y,
1068+
x + width,
1069+
y,
1070+
x + width,
1071+
y + height,
1072+
x,
1073+
y + height,
1074+
x,
1075+
y, // Close the rectangle
1076+
];
1077+
1078+
const eraserLine: CanvasEraserLineState = {
1079+
id: getPrefixedId('eraser_line'),
1080+
type: 'eraser_line',
1081+
points,
1082+
strokeWidth: Math.max(width, height) / 2, // Use a stroke width that covers the rectangle
1083+
clip: null,
10461084
};
1085+
invertedObjects.push(eraserLine);
10471086
} else if (obj.type === 'eraser_line') {
1048-
// Convert eraser lines to brush lines
1049-
return {
1050-
...obj,
1087+
// Convert eraser line to brush line
1088+
const brushLine: CanvasBrushLineState = {
10511089
id: getPrefixedId('brush_line'),
1052-
type: 'brush_line' as const,
1090+
type: 'brush_line',
1091+
strokeWidth: obj.strokeWidth,
1092+
points: obj.points,
1093+
clip: obj.clip,
1094+
color: { r: 255, g: 255, b: 255, a: 1 },
10531095
};
1096+
invertedObjects.push(brushLine);
10541097
} else if (obj.type === 'eraser_line_with_pressure') {
1055-
// Convert eraser lines with pressure to brush lines with pressure
1056-
return {
1057-
...obj,
1098+
// Convert eraser line with pressure to brush line with pressure
1099+
const brushLine: CanvasBrushLineWithPressureState = {
10581100
id: getPrefixedId('brush_line'),
1059-
type: 'brush_line_with_pressure' as const,
1101+
type: 'brush_line_with_pressure',
1102+
strokeWidth: obj.strokeWidth,
1103+
points: obj.points,
1104+
clip: obj.clip,
1105+
color: { r: 255, g: 255, b: 255, a: 1 },
10601106
};
1107+
invertedObjects.push(brushLine);
10611108
}
1062-
// Keep images and other objects as is
1063-
return obj;
1064-
});
1109+
// Note: Image objects are not handled in this simple approach
1110+
// They would need to be processed through the compositor
1111+
}
10651112

1066-
// Replace all objects with the base rectangle followed by converted objects
1067-
entity.objects = [fillRectObject, ...convertedObjects];
1113+
// Replace the entity's objects with the inverted mask objects
1114+
entity.objects = invertedObjects;
10681115
},
10691116
//#region BBox
10701117
bboxScaledWidthChanged: (state, action: PayloadAction<number>) => {
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { getPrefixedId } from 'features/controlLayers/konva/util';
2+
import type {
3+
CanvasBrushLineState,
4+
CanvasBrushLineWithPressureState,
5+
CanvasEraserLineState,
6+
CanvasEraserLineWithPressureState,
7+
CanvasImageState,
8+
CanvasRectState,
9+
RgbaColor,
10+
} from 'features/controlLayers/store/types';
11+
12+
/**
13+
* Options for converting bitmap to mask objects
14+
*/
15+
export interface BitmapToMaskOptions {
16+
/**
17+
* The threshold for considering a pixel as masked (0-255)
18+
* Pixels with alpha >= threshold are considered masked
19+
*/
20+
threshold?: number;
21+
/**
22+
* The color to use for brush lines
23+
*/
24+
brushColor?: RgbaColor;
25+
/**
26+
* The stroke width for brush lines
27+
*/
28+
strokeWidth?: number;
29+
/**
30+
* Whether to use pressure-sensitive lines
31+
*/
32+
usePressure?: boolean;
33+
/**
34+
* The pressure value to use for pressure-sensitive lines (0-1)
35+
*/
36+
pressure?: number;
37+
}
38+
39+
/**
40+
* Default options for bitmap to mask conversion
41+
*/
42+
const DEFAULT_OPTIONS: Required<BitmapToMaskOptions> = {
43+
threshold: 128,
44+
brushColor: { r: 255, g: 255, b: 255, a: 1 },
45+
strokeWidth: 50,
46+
usePressure: false,
47+
pressure: 1.0,
48+
};
49+
50+
/**
51+
* Converts a bitmap (ImageData) to mask objects (brush lines, eraser lines, rectangles)
52+
*
53+
* @param imageData - The bitmap data to convert
54+
* @param options - Conversion options
55+
* @returns Array of mask objects
56+
*/
57+
export function bitmapToMaskObjects(
58+
imageData: ImageData,
59+
options: BitmapToMaskOptions = {}
60+
): (
61+
| CanvasBrushLineState
62+
| CanvasBrushLineWithPressureState
63+
| CanvasEraserLineState
64+
| CanvasEraserLineWithPressureState
65+
| CanvasRectState
66+
| CanvasImageState
67+
)[] {
68+
const opts = { ...DEFAULT_OPTIONS, ...options };
69+
const { width, height, data } = imageData;
70+
const objects: (
71+
| CanvasBrushLineState
72+
| CanvasBrushLineWithPressureState
73+
| CanvasEraserLineState
74+
| CanvasEraserLineWithPressureState
75+
| CanvasRectState
76+
| CanvasImageState
77+
)[] = [];
78+
79+
// For now, we'll create a simple approach that creates rectangles for masked areas
80+
// This can be enhanced later to create more sophisticated brush/eraser line patterns
81+
82+
// Scan the image data to find masked areas
83+
for (let y = 0; y < height; y += opts.strokeWidth) {
84+
for (let x = 0; x < width; x += opts.strokeWidth) {
85+
// Check if this pixel is masked
86+
const pixelIndex = (y * width + x) * 4;
87+
const alpha = data[pixelIndex + 3] ?? 0;
88+
89+
if (alpha >= opts.threshold) {
90+
// Create a rectangle for this masked area
91+
const rect: CanvasRectState = {
92+
id: getPrefixedId('rect'),
93+
type: 'rect',
94+
rect: {
95+
x,
96+
y,
97+
width: Math.min(opts.strokeWidth, width - x),
98+
height: Math.min(opts.strokeWidth, height - y),
99+
},
100+
color: opts.brushColor,
101+
};
102+
objects.push(rect);
103+
}
104+
}
105+
}
106+
107+
return objects;
108+
}
109+
110+
/**
111+
* Inverts a bitmap by flipping the alpha channel
112+
*
113+
* @param imageData - The bitmap data to invert
114+
* @returns New ImageData with inverted alpha channel
115+
*/
116+
export function invertBitmap(imageData: ImageData): ImageData {
117+
const { width, height, data } = imageData;
118+
const newImageData = new ImageData(width, height);
119+
const newData = newImageData.data;
120+
121+
for (let i = 0; i < data.length; i += 4) {
122+
// Copy RGB values
123+
newData[i] = data[i] ?? 0; // R
124+
newData[i + 1] = data[i + 1] ?? 0; // G
125+
newData[i + 2] = data[i + 2] ?? 0; // B
126+
// Invert alpha
127+
newData[i + 3] = 255 - (data[i + 3] ?? 0); // A
128+
}
129+
130+
return newImageData;
131+
}
132+
133+
/**
134+
* Converts mask objects to a bitmap (ImageData)
135+
* This is a simplified version that creates a basic bitmap representation
136+
*
137+
* @param objects - Array of mask objects
138+
* @param width - Width of the output bitmap
139+
* @param height - Height of the output bitmap
140+
* @returns ImageData representing the mask
141+
*/
142+
export function maskObjectsToBitmap(
143+
objects: (
144+
| CanvasBrushLineState
145+
| CanvasBrushLineWithPressureState
146+
| CanvasEraserLineState
147+
| CanvasEraserLineWithPressureState
148+
| CanvasRectState
149+
| CanvasImageState
150+
)[],
151+
width: number,
152+
height: number
153+
): ImageData {
154+
const canvas = document.createElement('canvas');
155+
canvas.width = width;
156+
canvas.height = height;
157+
const ctx = canvas.getContext('2d');
158+
159+
if (!ctx) {
160+
throw new Error('Failed to get canvas context');
161+
}
162+
163+
// Clear canvas with transparent background
164+
ctx.clearRect(0, 0, width, height);
165+
166+
// Draw each object
167+
for (const obj of objects) {
168+
if (obj.type === 'rect') {
169+
ctx.fillStyle = `rgba(${obj.color.r}, ${obj.color.g}, ${obj.color.b}, ${obj.color.a})`;
170+
ctx.fillRect(obj.rect.x, obj.rect.y, obj.rect.width, obj.rect.height);
171+
} else if (obj.type === 'brush_line' || obj.type === 'brush_line_with_pressure') {
172+
ctx.strokeStyle = `rgba(${obj.color.r}, ${obj.color.g}, ${obj.color.b}, ${obj.color.a})`;
173+
ctx.lineWidth = obj.strokeWidth;
174+
ctx.lineCap = 'round';
175+
ctx.lineJoin = 'round';
176+
177+
// Draw the line
178+
ctx.beginPath();
179+
for (let i = 0; i < obj.points.length; i += 2) {
180+
const x = obj.points[i] ?? 0;
181+
const y = obj.points[i + 1] ?? 0;
182+
if (i === 0) {
183+
ctx.moveTo(x, y);
184+
} else {
185+
ctx.lineTo(x, y);
186+
}
187+
}
188+
ctx.stroke();
189+
} else if (obj.type === 'eraser_line' || obj.type === 'eraser_line_with_pressure') {
190+
// Eraser lines use destination-out composite operation
191+
ctx.globalCompositeOperation = 'destination-out';
192+
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
193+
ctx.lineWidth = obj.strokeWidth;
194+
ctx.lineCap = 'round';
195+
ctx.lineJoin = 'round';
196+
197+
// Draw the line
198+
ctx.beginPath();
199+
for (let i = 0; i < obj.points.length; i += 2) {
200+
const x = obj.points[i] ?? 0;
201+
const y = obj.points[i + 1] ?? 0;
202+
if (i === 0) {
203+
ctx.moveTo(x, y);
204+
} else {
205+
ctx.lineTo(x, y);
206+
}
207+
}
208+
ctx.stroke();
209+
210+
// Reset composite operation
211+
ctx.globalCompositeOperation = 'source-over';
212+
} else if (obj.type === 'image') {
213+
// For image objects, we need to load the image and draw it
214+
// This is a simplified approach - in a real implementation, you'd want to handle image loading properly
215+
const img = new Image();
216+
img.crossOrigin = 'anonymous';
217+
218+
// Create a temporary canvas to draw the image
219+
const tempCanvas = document.createElement('canvas');
220+
const tempCtx = tempCanvas.getContext('2d');
221+
if (tempCtx) {
222+
tempCanvas.width = obj.image.width;
223+
tempCanvas.height = obj.image.height;
224+
225+
// Draw the image to the temp canvas
226+
if ('image_name' in obj.image) {
227+
// This would need proper image loading from the server
228+
// For now, we'll skip image objects in the mask conversion
229+
console.warn('Image objects with image_name are not supported in mask conversion');
230+
} else {
231+
// Data URL image
232+
img.src = obj.image.dataURL;
233+
tempCtx.drawImage(img, 0, 0);
234+
235+
// Draw the temp canvas to the main canvas
236+
ctx.drawImage(tempCanvas, 0, 0);
237+
}
238+
}
239+
}
240+
}
241+
242+
return ctx.getImageData(0, 0, width, height);
243+
}

0 commit comments

Comments
 (0)