feat(gui): display source images after selection
This commit is contained in:
parent
2332c44cee
commit
f49fc960c9
|
@ -0,0 +1,54 @@
|
||||||
|
import { doesExist, mustDefault, mustExist } from '@apextoaster/js-utils';
|
||||||
|
import { PhotoCamera } from '@mui/icons-material';
|
||||||
|
import { Button, Stack } from '@mui/material';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const { useState } = React;
|
||||||
|
|
||||||
|
export interface ImageInputProps {
|
||||||
|
filter: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
renderImage?: (image: string | undefined) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageInput(props: ImageInputProps) {
|
||||||
|
const [image, setImage] = useState<string>();
|
||||||
|
|
||||||
|
function renderImage() {
|
||||||
|
if (mustDefault(props.hidden, false)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesExist(props.renderImage)) {
|
||||||
|
return props.renderImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={image} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Stack direction='row' spacing={2}>
|
||||||
|
<Button component='label' startIcon={<PhotoCamera />}>
|
||||||
|
{props.label}
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
accept={props.filter}
|
||||||
|
type='file'
|
||||||
|
onChange={(event) => {
|
||||||
|
const files = mustExist(event.target.files);
|
||||||
|
const file = mustExist(files[0]);
|
||||||
|
|
||||||
|
if (doesExist(image)) {
|
||||||
|
URL.revokeObjectURL(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(URL.createObjectURL(file));
|
||||||
|
props.onChange(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
{renderImage()}
|
||||||
|
</Stack>;
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import { doesExist, mustExist } from '@apextoaster/js-utils';
|
import { mustExist } from '@apextoaster/js-utils';
|
||||||
import { Box, Button, Stack } from '@mui/material';
|
import { Box, Button, Stack } from '@mui/material';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
|
|
||||||
import { ApiClient, BaseImgParams } from '../api/client.js';
|
import { ApiClient, BaseImgParams } from '../api/client.js';
|
||||||
import { Config, CONFIG_DEFAULTS, STALE_TIME } from '../config.js';
|
import { Config, CONFIG_DEFAULTS, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||||
import { SCHEDULER_LABELS } from '../strings.js';
|
import { SCHEDULER_LABELS } from '../strings.js';
|
||||||
|
import { ImageInput } from './ImageInput.js';
|
||||||
import { ImageCard } from './ImageCard.js';
|
import { ImageCard } from './ImageCard.js';
|
||||||
import { ImageControl } from './ImageControl.js';
|
import { ImageControl } from './ImageControl.js';
|
||||||
import { MutationHistory } from './MutationHistory.js';
|
import { MutationHistory } from './MutationHistory.js';
|
||||||
|
@ -36,15 +37,6 @@ export function Img2Img(props: Img2ImgProps) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSource(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (doesExist(event.target.files)) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (doesExist(file)) {
|
|
||||||
setSource(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload = useMutation(uploadSource);
|
const upload = useMutation(uploadSource);
|
||||||
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
||||||
staleTime: STALE_TIME,
|
staleTime: STALE_TIME,
|
||||||
|
@ -74,7 +66,7 @@ export function Img2Img(props: Img2ImgProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input type='file' onChange={changeSource} />
|
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} />
|
||||||
<ImageControl params={params} onChange={(newParams) => {
|
<ImageControl params={params} onChange={(newParams) => {
|
||||||
setParams(newParams);
|
setParams(newParams);
|
||||||
}} />
|
}} />
|
||||||
|
|
|
@ -5,8 +5,9 @@ import * as React from 'react';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
|
|
||||||
import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js';
|
import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js';
|
||||||
import { Config, CONFIG_DEFAULTS, STALE_TIME } from '../config.js';
|
import { Config, CONFIG_DEFAULTS, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||||
import { SCHEDULER_LABELS } from '../strings.js';
|
import { SCHEDULER_LABELS } from '../strings.js';
|
||||||
|
import { ImageInput } from './ImageInput.js';
|
||||||
import { ImageCard } from './ImageCard.js';
|
import { ImageCard } from './ImageCard.js';
|
||||||
import { ImageControl } from './ImageControl.js';
|
import { ImageControl } from './ImageControl.js';
|
||||||
import { MutationHistory } from './MutationHistory.js';
|
import { MutationHistory } from './MutationHistory.js';
|
||||||
|
@ -15,7 +16,6 @@ import { QueryList } from './QueryList.js';
|
||||||
|
|
||||||
const { useEffect, useRef, useState } = React;
|
const { useEffect, useRef, useState } = React;
|
||||||
|
|
||||||
export const DEFAULT_BRUSH = 8;
|
|
||||||
export const FULL_CIRCLE = 2 * Math.PI;
|
export const FULL_CIRCLE = 2 * Math.PI;
|
||||||
export const PIXEL_SIZE = 4;
|
export const PIXEL_SIZE = 4;
|
||||||
export const PIXEL_WEIGHT = 3;
|
export const PIXEL_WEIGHT = 3;
|
||||||
|
@ -69,14 +69,13 @@ export function Inpaint(props: InpaintProps) {
|
||||||
async function uploadSource() {
|
async function uploadSource() {
|
||||||
const canvas = mustExist(canvasRef.current);
|
const canvas = mustExist(canvasRef.current);
|
||||||
return new Promise<ApiResponse>((res, _rej) => {
|
return new Promise<ApiResponse>((res, _rej) => {
|
||||||
canvas.toBlob((value) => {
|
canvas.toBlob((blob) => {
|
||||||
const mask = mustExist(value);
|
|
||||||
res(client.inpaint({
|
res(client.inpaint({
|
||||||
...params,
|
...params,
|
||||||
model,
|
model,
|
||||||
platform,
|
platform,
|
||||||
scheduler,
|
scheduler,
|
||||||
mask,
|
mask: mustExist(blob),
|
||||||
source: mustExist(source),
|
source: mustExist(source),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -93,13 +92,19 @@ export function Inpaint(props: InpaintProps) {
|
||||||
image.src = URL.createObjectURL(file);
|
image.src = URL.createObjectURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSource(event: React.ChangeEvent<HTMLInputElement>) {
|
function changeMask(file: File) {
|
||||||
if (doesExist(event.target.files)) {
|
setMask(file);
|
||||||
const file = event.target.files[0];
|
|
||||||
if (doesExist(file)) {
|
// always draw the mask to the canvas
|
||||||
setSource(file);
|
drawSource(file);
|
||||||
drawSource(file);
|
}
|
||||||
}
|
|
||||||
|
function changeSource(file: File) {
|
||||||
|
setSource(file);
|
||||||
|
|
||||||
|
// draw the source to the canvas if the mask has not been set
|
||||||
|
if (doesExist(mask) === false) {
|
||||||
|
drawSource(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +161,7 @@ export function Inpaint(props: InpaintProps) {
|
||||||
const [brushColor, setBrushColor] = useState(0);
|
const [brushColor, setBrushColor] = useState(0);
|
||||||
const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH);
|
const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH);
|
||||||
|
|
||||||
|
const [mask, setMask] = useState<File>();
|
||||||
const [source, setSource] = useState<File>();
|
const [source, setSource] = useState<File>();
|
||||||
const [params, setParams] = useState<BaseImgParams>({
|
const [params, setParams] = useState<BaseImgParams>({
|
||||||
cfg: CONFIG_DEFAULTS.cfg.default,
|
cfg: CONFIG_DEFAULTS.cfg.default,
|
||||||
|
@ -179,6 +185,50 @@ export function Inpaint(props: InpaintProps) {
|
||||||
clicks.length = 0;
|
clicks.length = 0;
|
||||||
}, [clicks.length]);
|
}, [clicks.length]);
|
||||||
|
|
||||||
|
function renderCanvas() {
|
||||||
|
return <canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
height={CONFIG_DEFAULTS.height.default}
|
||||||
|
width={CONFIG_DEFAULTS.width.default}
|
||||||
|
style={{
|
||||||
|
maxHeight: CONFIG_DEFAULTS.height.default,
|
||||||
|
maxWidth: CONFIG_DEFAULTS.width.default,
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
const canvas = mustExist(canvasRef.current);
|
||||||
|
const bounds = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
setClicks([...clicks, {
|
||||||
|
x: event.clientX - bounds.left,
|
||||||
|
y: event.clientY - bounds.top,
|
||||||
|
}]);
|
||||||
|
}}
|
||||||
|
onMouseDown={() => {
|
||||||
|
setPainting(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setPainting(false);
|
||||||
|
}}
|
||||||
|
onMouseOut={() => {
|
||||||
|
setPainting(false);
|
||||||
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setPainting(false);
|
||||||
|
}}
|
||||||
|
onMouseMove={(event) => {
|
||||||
|
if (painting) {
|
||||||
|
const canvas = mustExist(canvasRef.current);
|
||||||
|
const bounds = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
setClicks([...clicks, {
|
||||||
|
x: event.clientX - bounds.left,
|
||||||
|
y: event.clientY - bounds.top,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Box>
|
return <Box>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Stack direction='row' spacing={2}>
|
<Stack direction='row' spacing={2}>
|
||||||
|
@ -193,48 +243,8 @@ export function Inpaint(props: InpaintProps) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<input type='file' onChange={changeSource} />
|
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={changeSource} />
|
||||||
<canvas
|
<ImageInput filter={IMAGE_FILTER} label='Mask' onChange={changeMask} renderImage={renderCanvas} />
|
||||||
ref={canvasRef}
|
|
||||||
height={CONFIG_DEFAULTS.height.default}
|
|
||||||
width={CONFIG_DEFAULTS.width.default}
|
|
||||||
style={{
|
|
||||||
maxHeight: CONFIG_DEFAULTS.height.default,
|
|
||||||
maxWidth: CONFIG_DEFAULTS.width.default,
|
|
||||||
}}
|
|
||||||
onClick={(event) => {
|
|
||||||
const canvas = mustExist(canvasRef.current);
|
|
||||||
const bounds = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
setClicks([...clicks, {
|
|
||||||
x: event.clientX - bounds.left,
|
|
||||||
y: event.clientY - bounds.top,
|
|
||||||
}]);
|
|
||||||
}}
|
|
||||||
onMouseDown={() => {
|
|
||||||
setPainting(true);
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setPainting(false);
|
|
||||||
}}
|
|
||||||
onMouseOut={() => {
|
|
||||||
setPainting(false);
|
|
||||||
}}
|
|
||||||
onMouseUp={() => {
|
|
||||||
setPainting(false);
|
|
||||||
}}
|
|
||||||
onMouseMove={(event) => {
|
|
||||||
if (painting) {
|
|
||||||
const canvas = mustExist(canvasRef.current);
|
|
||||||
const bounds = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
setClicks([...clicks, {
|
|
||||||
x: event.clientX - bounds.left,
|
|
||||||
y: event.clientY - bounds.top,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack direction='row' spacing={4}>
|
<Stack direction='row' spacing={4}>
|
||||||
<NumericField
|
<NumericField
|
||||||
decimal
|
decimal
|
||||||
|
@ -271,7 +281,7 @@ export function Inpaint(props: InpaintProps) {
|
||||||
<Button
|
<Button
|
||||||
startIcon={<FormatColorFill />}
|
startIcon={<FormatColorFill />}
|
||||||
onClick={() => floodMask(floodAbove)}>
|
onClick={() => floodMask(floodAbove)}>
|
||||||
Gray to white
|
Gray to white
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ImageControl params={params} onChange={(newParams) => {
|
<ImageControl params={params} onChange={(newParams) => {
|
||||||
|
|
|
@ -37,6 +37,8 @@ export type ConfigRanges<T extends object> = {
|
||||||
[K in KeyFilter<T>]: T[K] extends number ? ConfigRange : T[K] extends string ? string : never;
|
[K in KeyFilter<T>]: T[K] extends number ? ConfigRange : T[K] extends string ? string : never;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_BRUSH = 8;
|
||||||
|
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
|
||||||
export const IMAGE_STEP = 8;
|
export const IMAGE_STEP = 8;
|
||||||
export const IMAGE_MAX = 512;
|
export const IMAGE_MAX = 512;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"directml",
|
"directml",
|
||||||
"ftfy",
|
"ftfy",
|
||||||
"huggingface",
|
"huggingface",
|
||||||
|
"Inpaint",
|
||||||
"Multistep",
|
"Multistep",
|
||||||
"numpy",
|
"numpy",
|
||||||
"Onnx",
|
"Onnx",
|
||||||
|
|
Loading…
Reference in New Issue