Skip to content

Commit b7389da

Browse files
dunkeronipsychedelicious
authored andcommitted
add: Noise filter on Canvas
1 parent 254b89b commit b7389da

File tree

8 files changed

+229
-12
lines changed

8 files changed

+229
-12
lines changed

invokeai/app/invocations/image.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,11 +1093,11 @@ def invoke(self, context: InvocationContext) -> ImageOutput:
10931093

10941094

10951095
@invocation(
1096-
"image_noise",
1096+
"img_noise",
10971097
title="Add Image Noise",
10981098
tags=["image", "noise"],
10991099
category="image",
1100-
version="1.0.2",
1100+
version="1.0.0",
11011101
)
11021102
class ImageNoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
11031103
"""Add noise to an image"""
@@ -1114,7 +1114,7 @@ class ImageNoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
11141114
description="The type of noise to add",
11151115
)
11161116
amount: float = InputField(default=0.1, ge=0, le=1, description="The amount of noise to add")
1117-
color: bool = InputField(default=False, description="Whether to add color noise")
1117+
noise_color: bool = InputField(default=True, description="Whether to add colored noise")
11181118

11191119
def invoke(self, context: InvocationContext) -> ImageOutput:
11201120
image = context.images.get_pil(self.image.image_name, mode="RGBA")
@@ -1126,13 +1126,13 @@ def invoke(self, context: InvocationContext) -> ImageOutput:
11261126
rs = numpy.random.RandomState(numpy.random.MT19937(numpy.random.SeedSequence(self.seed)))
11271127

11281128
if self.noise_type == "gaussian":
1129-
if self.color:
1129+
if self.noise_color:
11301130
noise = rs.normal(0, 1, (image.height, image.width, 3)) * 255
11311131
else:
11321132
noise = rs.normal(0, 1, (image.height, image.width)) * 255
11331133
noise = numpy.stack([noise] * 3, axis=-1)
11341134
elif self.noise_type == "salt_and_pepper":
1135-
if self.color:
1135+
if self.noise_color:
11361136
noise = rs.choice([0, 255], (image.height, image.width, 3), p=[1 - self.amount, self.amount])
11371137
else:
11381138
noise = rs.choice([0, 255], (image.height, image.width), p=[1 - self.amount, self.amount])

invokeai/frontend/web/public/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,15 @@
19401940
"blur_radius": "Radius",
19411941
"gaussian_type": "Gaussian",
19421942
"box_type": "Box"
1943+
},
1944+
"img_noise": {
1945+
"label": "Noise Image",
1946+
"description": "Adds noise to the selected layer.",
1947+
"noise_type": "Noise Type",
1948+
"noise_amount": "Amount",
1949+
"gaussian_type": "Gaussian",
1950+
"salt_and_pepper_type": "Salt and Pepper",
1951+
"noise_color": "Colored Noise"
19431952
}
19441953
},
19451954
"transform": {

invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterBlur.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const FilterBlur = memo(({ onChange, config }: Props) => {
5353
onChange={handleRadiusChange}
5454
min={1}
5555
max={64}
56-
step={1}
56+
step={0.1}
5757
marks
5858
/>
5959
<CompositeNumberInput
@@ -62,7 +62,7 @@ export const FilterBlur = memo(({ onChange, config }: Props) => {
6262
onChange={handleRadiusChange}
6363
min={1}
6464
max={4096}
65-
step={1}
65+
step={0.1}
6666
/>
6767
</FormControl>
6868
</>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
2+
import { Combobox, CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
3+
import type { NoiseFilterConfig, NoiseTypes } from 'features/controlLayers/store/filters';
4+
import { IMAGE_FILTERS, isNoiseTypes } from 'features/controlLayers/store/filters';
5+
import type { ChangeEvent } from 'react';
6+
import { memo, useCallback, useMemo } from 'react';
7+
import { useTranslation } from 'react-i18next';
8+
9+
import type { FilterComponentProps } from './types';
10+
11+
type Props = FilterComponentProps<NoiseFilterConfig>;
12+
const DEFAULTS = IMAGE_FILTERS.img_noise.buildDefaults();
13+
14+
export const FilterNoise = memo(({ onChange, config }: Props) => {
15+
const { t } = useTranslation();
16+
const handleNoiseTypeChange = useCallback<ComboboxOnChange>(
17+
(v) => {
18+
if (!isNoiseTypes(v?.value)) {
19+
return;
20+
}
21+
onChange({ ...config, noise_type: v.value });
22+
},
23+
[config, onChange]
24+
);
25+
26+
const handleAmountChange = useCallback(
27+
(v: number) => {
28+
onChange({ ...config, amount: v });
29+
},
30+
[config, onChange]
31+
);
32+
33+
const handleColorChange = useCallback(
34+
(e: ChangeEvent<HTMLInputElement>) => {
35+
onChange({ ...config, noise_color: e.target.checked });
36+
},
37+
[config, onChange]
38+
);
39+
40+
const options: { label: string; value: NoiseTypes }[] = useMemo(
41+
() => [
42+
{ label: t('controlLayers.filter.img_noise.gaussian_type'), value: 'gaussian' },
43+
{ label: t('controlLayers.filter.img_noise.salt_and_pepper_type'), value: 'salt_and_pepper' },
44+
],
45+
[t]
46+
);
47+
48+
const value = useMemo(() => options.filter((o) => o.value === config.noise_type)[0], [options, config.noise_type]);
49+
50+
return (
51+
<>
52+
<FormControl>
53+
<FormLabel m={0}>{t('controlLayers.filter.img_noise.noise_type')}</FormLabel>
54+
<Combobox value={value} options={options} onChange={handleNoiseTypeChange} isSearchable={false} />
55+
</FormControl>
56+
<FormControl>
57+
<FormLabel m={0}>{t('controlLayers.filter.img_noise.noise_amount')}</FormLabel>
58+
<CompositeSlider
59+
value={config.amount}
60+
defaultValue={DEFAULTS.amount}
61+
onChange={handleAmountChange}
62+
min={0}
63+
max={1}
64+
step={0.01}
65+
marks
66+
/>
67+
<CompositeNumberInput
68+
value={config.amount}
69+
defaultValue={DEFAULTS.amount}
70+
onChange={handleAmountChange}
71+
min={0}
72+
max={1}
73+
step={0.01}
74+
/>
75+
</FormControl>
76+
<FormControl w="max-content">
77+
<FormLabel m={0}>{t('controlLayers.filter.img_noise.noise_color')}</FormLabel>
78+
<Switch defaultChecked={DEFAULTS.noise_color} isChecked={config.noise_color} onChange={handleColorChange} />
79+
</FormControl>
80+
</>
81+
);
82+
});
83+
84+
FilterNoise.displayName = 'Filternoise';

invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { FilterHEDEdgeDetection } from 'features/controlLayers/components/Filter
99
import { FilterLineartEdgeDetection } from 'features/controlLayers/components/Filters/FilterLineartEdgeDetection';
1010
import { FilterMediaPipeFaceDetection } from 'features/controlLayers/components/Filters/FilterMediaPipeFaceDetection';
1111
import { FilterMLSDDetection } from 'features/controlLayers/components/Filters/FilterMLSDDetection';
12+
import { FilterNoise } from 'features/controlLayers/components/Filters/FilterNoise';
1213
import { FilterPiDiNetEdgeDetection } from 'features/controlLayers/components/Filters/FilterPiDiNetEdgeDetection';
1314
import { FilterSpandrel } from 'features/controlLayers/components/Filters/FilterSpandrel';
1415
import type { FilterConfig } from 'features/controlLayers/store/filters';
@@ -64,6 +65,10 @@ export const FilterSettings = memo(({ filterConfig, onChange }: Props) => {
6465
return <FilterPiDiNetEdgeDetection config={filterConfig} onChange={onChange} />;
6566
}
6667

68+
if (filterConfig.type === 'img_noise') {
69+
return <FilterNoise config={filterConfig} onChange={onChange} />;
70+
}
71+
6772
if (filterConfig.type === 'spandrel_filter') {
6873
return <FilterSpandrel config={filterConfig} onChange={onChange} />;
6974
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ const zBlurFilterConfig = z.object({
105105
});
106106
export type BlurFilterConfig = z.infer<typeof zBlurFilterConfig>;
107107

108+
const zNoiseTypes = z.enum(['gaussian', 'salt_and_pepper']);
109+
export type NoiseTypes = z.infer<typeof zNoiseTypes>;
110+
export const isNoiseTypes = (v: unknown): v is NoiseTypes => zNoiseTypes.safeParse(v).success;
111+
const zNoiseFilterConfig = z.object({
112+
type: z.literal('img_noise'),
113+
noise_type: zNoiseTypes,
114+
amount: z.number().gte(0).lte(1),
115+
noise_color: z.boolean(),
116+
});
117+
export type NoiseFilterConfig = z.infer<typeof zNoiseFilterConfig>;
118+
108119
const zFilterConfig = z.discriminatedUnion('type', [
109120
zCannyEdgeDetectionFilterConfig,
110121
zColorMapFilterConfig,
@@ -120,6 +131,7 @@ const zFilterConfig = z.discriminatedUnion('type', [
120131
zDWOpenposeDetectionFilterConfig,
121132
zSpandrelFilterConfig,
122133
zBlurFilterConfig,
134+
zNoiseFilterConfig,
123135
]);
124136
export type FilterConfig = z.infer<typeof zFilterConfig>;
125137

@@ -138,6 +150,7 @@ const zFilterType = z.enum([
138150
'dw_openpose_detection',
139151
'spandrel_filter',
140152
'img_blur',
153+
'img_noise',
141154
]);
142155
export type FilterType = z.infer<typeof zFilterType>;
143156
export const isFilterType = (v: unknown): v is FilterType => zFilterType.safeParse(v).success;
@@ -463,6 +476,38 @@ export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData<key
463476
};
464477
},
465478
},
479+
img_noise: {
480+
type: 'img_noise',
481+
buildDefaults: () => ({
482+
type: 'img_noise',
483+
noise_type: 'gaussian',
484+
amount: 0.3,
485+
noise_color: true,
486+
}),
487+
buildGraph: ({ image_name }, { noise_type, amount, noise_color }) => {
488+
const graph = new Graph(getPrefixedId('img_noise'));
489+
const node = graph.addNode({
490+
id: getPrefixedId('img_noise'),
491+
type: 'img_noise',
492+
image: { image_name },
493+
noise_type: noise_type,
494+
amount: amount,
495+
noise_color: noise_color,
496+
});
497+
const rand = graph.addNode({
498+
id: getPrefixedId('rand_int'),
499+
use_cache: false,
500+
type: 'rand_int',
501+
low: 0,
502+
high: 2147483647,
503+
});
504+
graph.addEdge(rand, 'value', node, 'seed');
505+
return {
506+
graph,
507+
outputNodeId: node.id,
508+
};
509+
},
510+
},
466511
} as const;
467512

468513
/**

invokeai/frontend/web/src/features/controlLayers/store/types.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
LineartEdgeDetectionFilterConfig,
1414
MediaPipeFaceDetectionFilterConfig,
1515
MLSDDetectionFilterConfig,
16+
NoiseFilterConfig,
1617
NormalMapFilterConfig,
1718
PiDiNetEdgeDetectionFilterConfig,
1819
} from 'features/controlLayers/store/filters';
@@ -73,6 +74,7 @@ describe('Control Adapter Types', () => {
7374
type _MLSDDetectionFilterConfig = Required<
7475
Pick<Invocation<'mlsd_detection'>, 'type' | 'score_threshold' | 'distance_threshold'>
7576
>;
77+
type _NoiseFilterConfig = Required<Pick<Invocation<'img_noise'>, 'type' | 'noise_type' | 'amount' | 'noise_color'>>;
7678
type _NormalMapFilterConfig = Required<Pick<Invocation<'normal_map'>, 'type'>>;
7779
type _DWOpenposeDetectionFilterConfig = Required<
7880
Pick<Invocation<'dw_openpose_detection'>, 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
@@ -93,6 +95,7 @@ describe('Control Adapter Types', () => {
9395
assert<Equals<_LineartEdgeDetectionFilterConfig, LineartEdgeDetectionFilterConfig>>();
9496
assert<Equals<_MediaPipeFaceDetectionFilterConfig, MediaPipeFaceDetectionFilterConfig>>();
9597
assert<Equals<_MLSDDetectionFilterConfig, MLSDDetectionFilterConfig>>();
98+
assert<Equals<_NoiseFilterConfig, NoiseFilterConfig>>();
9699
assert<Equals<_NormalMapFilterConfig, NormalMapFilterConfig>>();
97100
assert<Equals<_DWOpenposeDetectionFilterConfig, DWOpenposeDetectionFilterConfig>>();
98101
assert<Equals<_PiDiNetEdgeDetectionFilterConfig, PiDiNetEdgeDetectionFilterConfig>>();

0 commit comments

Comments
 (0)