compositing in frontend

This commit is contained in:
dunkeroni
2025-05-03 00:51:00 -04:00
committed by psychedelicious
parent 5e20c9a1ca
commit 23627cf18d
5 changed files with 595 additions and 12 deletions

View File

@@ -1218,12 +1218,16 @@ class ApplyMaskToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
title="Add Image Noise",
tags=["image", "noise"],
category="image",
version="1.0.1",
version="1.1.0",
)
class ImageNoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Add noise to an image"""
image: ImageField = InputField(description="The image to add noise to")
mask: Optional[ImageField] = InputField(
default=None,
description="Optional mask determining where to apply noise (black=noise, white=no noise)"
)
seed: int = InputField(
default=0,
ge=0,
@@ -1267,12 +1271,27 @@ class ImageNoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
noise = Image.fromarray(noise.astype(numpy.uint8), mode="RGB").resize(
(image.width, image.height), Image.Resampling.NEAREST
)
# Create a noisy version of the input image
noisy_image = Image.blend(image.convert("RGB"), noise, self.amount).convert("RGBA")
# Apply mask if provided
if self.mask is not None:
mask_image = context.images.get_pil(self.mask.image_name, mode="L")
if mask_image.size != image.size:
mask_image = mask_image.resize(image.size, Image.Resampling.LANCZOS)
result_image = image.copy()
mask_image = ImageOps.invert(mask_image)
result_image.paste(noisy_image, (0, 0), mask=mask_image)
else:
result_image = noisy_image
# Paste back the alpha channel from the original image
result_image.putalpha(alpha)
# Paste back the alpha channel
noisy_image.putalpha(alpha)
image_dto = context.images.save(image=noisy_image)
image_dto = context.images.save(image=result_image)
return ImageOutput.build(image_dto)

View File

@@ -1,5 +1,6 @@
import { withResult, withResultAsync } from 'common/util/result';
import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule';
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
@@ -426,6 +427,147 @@ export class CanvasCompositorModule extends CanvasModuleBase {
return this.mergeByEntityIdentifiers(entityIdentifiers, false);
};
/**
* Creates and uploads a grayscale representation of the noise masks or any other attribute.
* This produces an image with a white background where the mask is represented by dark values
* rather than transparency.
*
* @param adapters The adapters for the canvas entities to composite
* @param rect The region to include in the rasterized image
* @param attribute The attribute to use for grayscale values (defaults to 'noiseLevel')
* @param uploadOptions Options for uploading the image
* @param forceUpload If true, the image is always re-uploaded, returning a new image DTO
* @returns A promise that resolves to the image DTO
*/
getGrayscaleMaskCompositeImageDTO = async (
adapters: CanvasEntityAdapterInpaintMask[],
rect: Rect,
attribute: 'noiseLevel' = 'noiseLevel',
uploadOptions: SetOptional<Omit<UploadImageArg, 'file'>, 'image_category'> = { is_intermediate: true },
forceUpload?: boolean
): Promise<ImageDTO> => {
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');
//const entityIdentifiers = adapters.map((adapter) => adapter.entityIdentifier);
// Use a unique hash that includes the attribute name for caching
const hash = this.getCompositeHash(adapters, { rect, attribute, grayscale: true });
const cachedImageName = forceUpload ? undefined : this.manager.cache.imageNameCache.get(hash);
let imageDTO: ImageDTO | null = null;
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.debug({ rect, imageName: cachedImageName, imageDTO }, 'Using cached grayscale composite image');
return imageDTO;
}
this.log.warn({ rect, imageName: cachedImageName }, 'Cached grayscale image name not found, recompositing');
}
// Create a white background canvas
const canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
const ctx = canvas.getContext('2d');
assert(ctx !== null, 'Canvas 2D context is null');
// Fill with white first (creates white background)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, rect.width, rect.height);
// Apply special compositing mode
ctx.globalCompositeOperation = 'darken';
// Draw each adapter's content
for (const adapter of adapters) {
this.log.debug({ entityIdentifier: adapter.entityIdentifier }, 'Drawing entity to grayscale composite canvas');
// Get the canvas from the adapter
const adapterCanvas = adapter.getCanvas(rect);
// Create a temporary canvas for grayscale conversion
const tempCanvas = document.createElement('canvas');
tempCanvas.width = adapterCanvas.width;
tempCanvas.height = adapterCanvas.height;
const tempCtx = tempCanvas.getContext('2d');
assert(tempCtx !== null, 'Temp canvas 2D context is null');
// Draw the original adapter canvas to the temp canvas
tempCtx.drawImage(adapterCanvas, 0, 0);
// Get the image data for processing
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Convert alpha values to grayscale based on the specified attribute
// Get the attribute value with proper type checking
const attributeValue = typeof adapter.state[attribute] === 'number' ? (adapter.state[attribute] as number) : 1.0; // Default to full strength if attribute is not a number
// Process all pixels in the image data
for (let i = 3; i < data.length; i += 4) {
// Make sure we're accessing valid array indices
if (i + 3 < data.length) {
const alpha = data[i + 3]! / 255;
// Calculate grayscale value: white (255) for no mask, darker for stronger mask
// Scale according to the attribute value (higher attribute = darker pixels)
const grayValue = Math.max(0, Math.min(255, 255 - Math.round(255 * alpha * attributeValue)));
data[i] = grayValue; // R
data[i + 1] = grayValue; // G
data[i + 2] = grayValue; // B
data[i + 3] = 255; // A (fully opaque)
}
}
// Put the processed image data back to the temp canvas
tempCtx.putImageData(imageData, 0, 0);
// Draw the temp canvas to the main canvas
ctx.drawImage(tempCanvas, 0, 0);
}
// Convert to blob and upload
this.$isProcessing.set(true);
const blobResult = await withResultAsync(() => canvasToBlob(canvas));
this.$isProcessing.set(false);
if (blobResult.isErr()) {
this.log.error(
{ error: serializeError(blobResult.error) },
'Failed to convert grayscale composite canvas to blob'
);
throw blobResult.error;
}
const blob = blobResult.value;
if (this.manager._isDebugging) {
previewBlob(blob, 'Grayscale Composite');
}
this.$isUploading.set(true);
const uploadResult = await withResultAsync(() =>
uploadImage({
file: new File([blob], 'canvas-grayscale-composite.png', { type: 'image/png' }),
image_category: 'general',
...uploadOptions,
})
);
this.$isUploading.set(false);
if (uploadResult.isErr()) {
throw uploadResult.error;
}
imageDTO = uploadResult.value;
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
/**
* Calculates the transparency of the composite of the give adapters.
* @param adapters The adapters to composite

View File

@@ -1,10 +1,11 @@
import type { RootState } from 'app/store/store';
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { Dimensions } from 'features/controlLayers/store/types';
import type { Dimensions, Rect } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { isMainModelWithoutUnet } from 'features/nodes/util/graph/graphBuilderUtils';
import type {
@@ -51,20 +52,39 @@ export const addInpaint = async ({
const canvasSettings = selectCanvasSettingsSlice(state);
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
// Make sure bbox.rect is defined, use an empty rect if it's not
const rect: Rect = canvas.bbox?.rect ?? getEmptyRect();
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
is_intermediate: true,
silent: true,
});
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, rect, {
is_intermediate: true,
silent: true,
});
// Get inpaint mask adapters that have noise settings
const noiseMaskAdapters = inpaintMaskAdapters.filter((adapter) => adapter.state.noiseLevel !== null);
// Create a composite noise mask if we have any adapters with noise settings
let noiseMaskImage = null;
if (noiseMaskAdapters.length > 0) {
// Use the grayscale mask composite method with proper typing
noiseMaskImage = await manager.compositor.getGrayscaleMaskCompositeImageDTO(
noiseMaskAdapters as CanvasEntityAdapterInpaintMask[],
rect,
'noiseLevel',
{
is_intermediate: true,
silent: true,
}
);
}
const needsScaleBeforeProcessing = !isEqual(scaledSize, originalSize);
if (needsScaleBeforeProcessing) {
@@ -82,6 +102,32 @@ export const addInpaint = async ({
image: { image_name: initialImage.image_name },
...scaledSize,
});
// If we have a noise mask, apply it to the input image before i2l conversion
if (noiseMaskImage) {
// Resize the noise mask to match the scaled size
const resizeNoiseMaskToScaledSize = g.addNode({
id: getPrefixedId('resize_noise_mask_to_scaled_size'),
type: 'img_resize',
image: { image_name: noiseMaskImage.image_name },
...scaledSize,
});
// Add noise to the scaled image using the mask
const noiseNode = g.addNode({
type: 'img_noise',
id: getPrefixedId('add_inpaint_noise'),
noise_type: 'gaussian',
amount: 1.0, // the mask controls the actual intensity
noise_color: true,
seed: Math.floor(Math.random() * 2147483647), // should this seed match the denoise latents seed?
});
g.addEdge(resizeImageToScaledSize, 'image', noiseNode, 'image');
g.addEdge(resizeNoiseMaskToScaledSize, 'image', noiseNode, 'mask');
g.addEdge(noiseNode, 'image', i2l, 'image');
}
const alphaToMask = g.addNode({
id: getPrefixedId('alpha_to_mask'),
type: 'tomask',
@@ -120,8 +166,6 @@ export const addInpaint = async ({
// Resize initial image and mask to scaled size, feed into to gradient mask
g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image');
g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image');
g.addEdge(i2l, 'latents', denoise, 'latents');
g.addEdge(vaeSource, 'vae', i2l, 'vae');
g.addEdge(vaeSource, 'vae', createGradientMask, 'vae');
if (!isMainModelWithoutUnet(modelLoader)) {
@@ -169,6 +213,23 @@ export const addInpaint = async ({
...(i2lNodeType === 'i2l' ? { fp32 } : {}),
});
// If we have a noise mask, apply it to the input image before i2l conversion
if (noiseMaskImage) {
// Add noise to the scaled image using the mask
const noiseNode = g.addNode({
type: 'img_noise',
id: getPrefixedId('add_inpaint_noise'),
image: initialImage.image_name ? { image_name: initialImage.image_name } : undefined,
noise_type: 'gaussian',
amount: 1.0, // the mask controls the actual intensity
noise_color: true,
seed: Math.floor(Math.random() * 2147483647), // should this seed match the denoise latents seed?
mask: { image_name: noiseMaskImage.image_name },
});
g.addEdge(noiseNode, 'image', i2l, 'image');
}
const alphaToMask = g.addNode({
id: getPrefixedId('alpha_to_mask'),
type: 'tomask',

View File

@@ -0,0 +1,356 @@
# Adding Image Noise Slider to InpaintMask Layer
This document outlines the steps required to add an "Image Noise" slider feature to the InpaintMask layer type in the Invoke AI frontend.
## Overview
The feature allows users to add and adjust an image noise level to an InpaintMask layer, similar to how Regional Guidance layers have the ability to add prompts. The implementation includes:
1. Extending the InpaintMask data model
2. Creating UI components for adding and adjusting noise
3. Adding Redux actions to handle state changes
4. Adding a delete button to remove the noise setting
5. Implementing conditional rendering of the add button based on current state
## Implementation Steps
### 1. Extend the InpaintMask Data Model
Update the `CanvasInpaintMaskState` type in `types.ts` to include a `noiseLevel` property:
```typescript
const zCanvasInpaintMaskState = zCanvasEntityBase.extend({
type: z.literal('inpaint_mask'),
position: zCoordinate,
fill: zFill,
opacity: zOpacity,
objects: z.array(zCanvasObjectState),
noiseLevel: z.number().gte(0).lte(1).nullable().default(null),
});
export type CanvasInpaintMaskState = z.infer<typeof zCanvasInpaintMaskState>;
```
### 2. Update the Initial State Function
Modify the `getInpaintMaskState` function in `util.ts` to include the new `noiseLevel` property:
```typescript
export const getInpaintMaskState = (
id: string,
overrides?: Partial<CanvasInpaintMaskState>
): CanvasInpaintMaskState => {
const entityState: CanvasInpaintMaskState = {
id,
name: null,
type: 'inpaint_mask',
isEnabled: true,
isLocked: false,
objects: [],
opacity: 1,
position: { x: 0, y: 0 },
fill: {
style: 'diagonal',
color: getInpaintMaskFillColor(),
},
noiseLevel: null,
};
merge(entityState, overrides);
return entityState;
};
```
### 3. Add Redux Actions
Add new actions to the `canvasSlice.ts` file to handle adding, changing, and deleting noise:
```typescript
inpaintMaskNoiseAdded: (state, action: PayloadAction<EntityIdentifierPayload<void, 'inpaint_mask'>>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (entity && entity.type === 'inpaint_mask') {
entity.noiseLevel = 0.5; // Default noise level
}
},
inpaintMaskNoiseChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ noiseLevel: number }, 'inpaint_mask'>>) => {
const { entityIdentifier, noiseLevel } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (entity && entity.type === 'inpaint_mask') {
entity.noiseLevel = noiseLevel;
}
},
inpaintMaskNoiseDeleted: (state, action: PayloadAction<EntityIdentifierPayload<void, 'inpaint_mask'>>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (entity && entity.type === 'inpaint_mask') {
entity.noiseLevel = null;
}
},
```
Make sure to export these actions from the canvasSlice:
```typescript
export const {
// ... other exports
inpaintMaskNoiseAdded,
inpaintMaskNoiseChanged,
inpaintMaskNoiseDeleted,
} = canvasSlice.actions;
```
### 4. Create a Hook for Adding Noise
Add a new hook in `addLayerHooks.ts` to handle adding noise to an InpaintMask:
```typescript
export const useAddInpaintMaskNoise = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => {
const dispatch = useAppDispatch();
const func = useCallback(() => {
dispatch(inpaintMaskNoiseAdded({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return func;
};
```
### 5. Create UI Components
#### 5.1. Create a Button Component for Adding Noise
Create a new file `InpaintMaskAddNoiseButton.tsx` for the button that adds noise:
```typescript
import { Button, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useAddInpaintMaskNoise } from 'features/controlLayers/hooks/addLayerHooks';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const InpaintMaskAddNoiseButton = () => {
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const { t } = useTranslation();
const addInpaintMaskNoise = useAddInpaintMaskNoise(entityIdentifier);
return (
<Flex w="full" p={2} justifyContent="center">
<Button
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addInpaintMaskNoise}
>
{t('controlLayers.imageNoise')}
</Button>
</Flex>
);
};
```
#### 5.2. Create a Delete Button Component
Create a new file `InpaintMaskDeleteNoiseButton.tsx` for the X button that deletes noise:
```typescript
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
type Props = Omit<IconButtonProps, 'aria-label'> & {
onDelete: () => void;
};
export const InpaintMaskDeleteNoiseButton = memo(({ onDelete, ...rest }: Props) => {
const { t } = useTranslation();
return (
<IconButton
tooltip={t('common.delete')}
variant="link"
aria-label={t('common.delete')}
icon={<PiXBold />}
onClick={onDelete}
flexGrow={0}
size="sm"
p={0}
colorScheme="error"
{...rest}
/>
);
});
InpaintMaskDeleteNoiseButton.displayName = 'InpaintMaskDeleteNoiseButton';
```
#### 5.3. Create a Slider Component with Delete Button
Create a new file `InpaintMaskNoiseSlider.tsx` for adjusting the noise level and including the delete button:
```typescript
import { Flex, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { InpaintMaskDeleteNoiseButton } from './InpaintMaskDeleteNoiseButton';
export const InpaintMaskNoiseSlider = memo(() => {
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectNoiseLevel = useMemo(
() =>
createSelector(
selectCanvasSlice,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel
),
[entityIdentifier]
);
const noiseLevel = useAppSelector(selectNoiseLevel);
const handleNoiseChange = useCallback(
(value: number) => {
dispatch(inpaintMaskNoiseChanged({ entityIdentifier, noiseLevel: value }));
},
[dispatch, entityIdentifier]
);
const onDeleteNoise = useCallback(() => {
dispatch(inpaintMaskNoiseDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
if (noiseLevel === null) {
return null;
}
return (
<Flex direction="column" gap={1} w="full" px={2} pb={2}>
<Flex justifyContent="space-between" w="full" alignItems="center">
<Text fontSize="sm">{t('controlLayers.imageNoise')}</Text>
<Flex alignItems="center" gap={1}>
<Text fontSize="sm">{Math.round(noiseLevel * 100)}%</Text>
<InpaintMaskDeleteNoiseButton onDelete={onDeleteNoise} />
</Flex>
</Flex>
<Slider
aria-label={t('controlLayers.imageNoise')}
value={noiseLevel}
min={0}
max={1}
step={0.01}
onChange={handleNoiseChange}
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
);
});
InpaintMaskNoiseSlider.displayName = 'InpaintMaskNoiseSlider';
```
### 6. Create a Settings Component for Conditional Rendering
Create a new file `InpaintMaskSettings.tsx` to handle conditional rendering of components based on state:
```typescript
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { InpaintMaskAddNoiseButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton';
import { InpaintMaskNoiseSlider } from 'features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings');
return {
hasNoiseLevel: entity.noiseLevel !== null,
};
});
export const InpaintMaskSettings = memo(() => {
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const selectFlags = useMemo(() => buildSelectFlags(entityIdentifier), [entityIdentifier]);
const flags = useAppSelector(selectFlags);
return (
<CanvasEntitySettingsWrapper>
{!flags.hasNoiseLevel && <InpaintMaskAddNoiseButton />}
{flags.hasNoiseLevel && <InpaintMaskNoiseSlider />}
</CanvasEntitySettingsWrapper>
);
});
InpaintMaskSettings.displayName = 'InpaintMaskSettings';
```
### 7. Update the InpaintMask Component
Modify the `InpaintMask.tsx` file to use the new settings component:
```typescript
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings';
import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate';
import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
type Props = {
id: string;
};
export const InpaintMask = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier<'inpaint_mask'>>(() => ({ id, type: 'inpaint_mask' }), [id]);
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<InpaintMaskAdapterGate>
<CanvasEntityStateGate entityIdentifier={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader>
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<InpaintMaskSettings />
</CanvasEntityContainer>
</CanvasEntityStateGate>
</InpaintMaskAdapterGate>
</EntityIdentifierContext.Provider>
);
});
InpaintMask.displayName = 'InpaintMask';
```
## Additional Considerations
1. Add translations for the "Image Noise" text in the translation files
2. Implement the backend functionality to process the noise level during image generation
3. Add tests for the new functionality
4. Update documentation to reflect the new feature
## Conclusion
This implementation adds an "Image Noise" slider to the InpaintMask layer, following a similar pattern to how Regional Guidance layers handle prompts. The feature allows users to add, adjust, and delete noise levels for their inpainting masks, enhancing the functionality of the application. The implementation includes conditional rendering of UI components based on the current state, ensuring that the add button is only shown when no noise level is set, and the slider with delete button is shown when a noise level exists.

View File

@@ -10443,6 +10443,11 @@ export type components = {
* @default null
*/
image?: components["schemas"]["ImageField"] | null;
/**
* @description Optional mask determining where to apply noise (black=noise, white=no noise)
* @default null
*/
mask?: components["schemas"]["ImageField"] | null;
/**
* Seed
* @description Seed for random number generation