1
0
Fork 0

feat(gui): display source images after selection

This commit is contained in:
Sean Sube 2023-01-08 22:15:58 -06:00
parent 2332c44cee
commit f49fc960c9
5 changed files with 126 additions and 67 deletions

View File

@ -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>;
}

View File

@ -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);
}} /> }} />

View File

@ -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) => {

View File

@ -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;

View File

@ -18,6 +18,7 @@
"directml", "directml",
"ftfy", "ftfy",
"huggingface", "huggingface",
"Inpaint",
"Multistep", "Multistep",
"numpy", "numpy",
"Onnx", "Onnx",