rename to iopaint
15
iopaint/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
|
||||
|
||||
def entry_point():
|
||||
# To make os.environ["XDG_CACHE_HOME"] = args.model_cache_dir works for diffusers
|
||||
# https://github.com/huggingface/diffusers/blob/be99201a567c1ccd841dc16fb24e88f7f239c187/src/diffusers/utils/constants.py#L18
|
||||
from iopaint.cli import typer_app
|
||||
|
||||
typer_app()
|
||||
392
iopaint/api.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import socketio
|
||||
import torch
|
||||
import uvicorn
|
||||
from PIL import Image
|
||||
from fastapi import APIRouter, FastAPI, Request, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, FileResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from socketio import AsyncServer
|
||||
|
||||
from iopaint.file_manager import FileManager
|
||||
from iopaint.helper import (
|
||||
load_img,
|
||||
decode_base64_to_image,
|
||||
pil_to_bytes,
|
||||
numpy_to_bytes,
|
||||
concat_alpha_channel,
|
||||
gen_frontend_mask,
|
||||
adjust_mask,
|
||||
)
|
||||
from iopaint.model.utils import torch_gc
|
||||
from iopaint.model_info import ModelInfo
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.plugins import build_plugins
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import (
|
||||
GenInfoResponse,
|
||||
ApiConfig,
|
||||
ServerConfigResponse,
|
||||
SwitchModelRequest,
|
||||
InpaintRequest,
|
||||
RunPluginRequest,
|
||||
SDSampler,
|
||||
PluginInfo,
|
||||
AdjustMaskRequest,
|
||||
)
|
||||
|
||||
CURRENT_DIR = Path(__file__).parent.absolute().resolve()
|
||||
WEB_APP_DIR = CURRENT_DIR / "web_app"
|
||||
|
||||
|
||||
def api_middleware(app: FastAPI):
|
||||
rich_available = False
|
||||
try:
|
||||
if os.environ.get("WEBUI_RICH_EXCEPTIONS", None) is not None:
|
||||
import anyio # importing just so it can be placed on silent list
|
||||
import starlette # importing just so it can be placed on silent list
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
rich_available = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def handle_exception(request: Request, e: Exception):
|
||||
err = {
|
||||
"error": type(e).__name__,
|
||||
"detail": vars(e).get("detail", ""),
|
||||
"body": vars(e).get("body", ""),
|
||||
"errors": str(e),
|
||||
}
|
||||
if not isinstance(
|
||||
e, HTTPException
|
||||
): # do not print backtrace on known httpexceptions
|
||||
message = f"API error: {request.method}: {request.url} {err}"
|
||||
if rich_available:
|
||||
print(message)
|
||||
console.print_exception(
|
||||
show_locals=True,
|
||||
max_frames=2,
|
||||
extra_lines=1,
|
||||
suppress=[anyio, starlette],
|
||||
word_wrap=False,
|
||||
width=min([console.width, 200]),
|
||||
)
|
||||
else:
|
||||
traceback.print_exc()
|
||||
return JSONResponse(
|
||||
status_code=vars(e).get("status_code", 500), content=jsonable_encoder(err)
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def exception_handling(request: Request, call_next):
|
||||
try:
|
||||
return await call_next(request)
|
||||
except Exception as e:
|
||||
return handle_exception(request, e)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def fastapi_exception_handler(request: Request, e: Exception):
|
||||
return handle_exception(request, e)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, e: HTTPException):
|
||||
return handle_exception(request, e)
|
||||
|
||||
cors_options = {
|
||||
"allow_methods": ["*"],
|
||||
"allow_headers": ["*"],
|
||||
"allow_origins": ["*"],
|
||||
"allow_credentials": True,
|
||||
}
|
||||
app.add_middleware(CORSMiddleware, **cors_options)
|
||||
|
||||
|
||||
global_sio: AsyncServer = None
|
||||
|
||||
|
||||
def diffuser_callback(pipe, step: int, timestep: int, callback_kwargs: Dict = {}):
|
||||
# self: DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict
|
||||
# logger.info(f"diffusion callback: step={step}, timestep={timestep}")
|
||||
|
||||
# We use asyncio loos for task processing. Perhaps in the future, we can add a processing queue similar to InvokeAI,
|
||||
# but for now let's just start a separate event loop. It shouldn't make a difference for single person use
|
||||
asyncio.run(global_sio.emit("diffusion_progress", {"step": step}))
|
||||
return {}
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, app: FastAPI, config: ApiConfig):
|
||||
self.app = app
|
||||
self.config = config
|
||||
self.router = APIRouter()
|
||||
self.queue_lock = threading.Lock()
|
||||
api_middleware(self.app)
|
||||
|
||||
self.file_manager = self._build_file_manager()
|
||||
self.plugins = self._build_plugins()
|
||||
self.model_manager = self._build_model_manager()
|
||||
|
||||
# fmt: off
|
||||
self.add_api_route("/api/v1/gen-info", self.api_geninfo, methods=["POST"], response_model=GenInfoResponse)
|
||||
self.add_api_route("/api/v1/server-config", self.api_server_config, methods=["GET"], response_model=ServerConfigResponse)
|
||||
self.add_api_route("/api/v1/models", self.api_models, methods=["GET"], response_model=List[ModelInfo])
|
||||
self.add_api_route("/api/v1/model", self.api_current_model, methods=["GET"], response_model=ModelInfo)
|
||||
self.add_api_route("/api/v1/model", self.api_switch_model, methods=["POST"], response_model=ModelInfo)
|
||||
self.add_api_route("/api/v1/inputimage", self.api_input_image, methods=["GET"])
|
||||
self.add_api_route("/api/v1/inpaint", self.api_inpaint, methods=["POST"])
|
||||
self.add_api_route("/api/v1/run_plugin_gen_mask", self.api_run_plugin_gen_mask, methods=["POST"])
|
||||
self.add_api_route("/api/v1/run_plugin_gen_image", self.api_run_plugin_gen_image, methods=["POST"])
|
||||
self.add_api_route("/api/v1/samplers", self.api_samplers, methods=["GET"])
|
||||
self.add_api_route("/api/v1/adjust_mask", self.api_adjust_mask, methods=["POST"])
|
||||
self.app.mount("/", StaticFiles(directory=WEB_APP_DIR, html=True), name="assets")
|
||||
# fmt: on
|
||||
|
||||
global global_sio
|
||||
self.sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
|
||||
self.combined_asgi_app = socketio.ASGIApp(self.sio, self.app)
|
||||
self.app.mount("/ws", self.combined_asgi_app)
|
||||
global_sio = self.sio
|
||||
|
||||
def add_api_route(self, path: str, endpoint, **kwargs):
|
||||
return self.app.add_api_route(path, endpoint, **kwargs)
|
||||
|
||||
def api_models(self) -> List[ModelInfo]:
|
||||
return self.model_manager.scan_models()
|
||||
|
||||
def api_current_model(self) -> ModelInfo:
|
||||
return self.model_manager.current_model
|
||||
|
||||
def api_switch_model(self, req: SwitchModelRequest) -> ModelInfo:
|
||||
if req.name == self.model_manager.name:
|
||||
return self.model_manager.current_model
|
||||
self.model_manager.switch(req.name)
|
||||
return self.model_manager.current_model
|
||||
|
||||
def api_server_config(self) -> ServerConfigResponse:
|
||||
return ServerConfigResponse(
|
||||
plugins=[
|
||||
PluginInfo(
|
||||
name=it.name,
|
||||
support_gen_image=it.support_gen_image,
|
||||
support_gen_mask=it.support_gen_mask,
|
||||
)
|
||||
for it in self.plugins.values()
|
||||
],
|
||||
enableFileManager=self.file_manager is not None,
|
||||
enableAutoSaving=self.config.output_dir is not None,
|
||||
enableControlnet=self.model_manager.enable_controlnet,
|
||||
controlnetMethod=self.model_manager.controlnet_method,
|
||||
disableModelSwitch=self.config.disable_model_switch,
|
||||
isDesktop=self.config.gui,
|
||||
samplers=self.api_samplers(),
|
||||
)
|
||||
|
||||
def api_input_image(self) -> FileResponse:
|
||||
if self.config.input and self.config.input.is_file():
|
||||
return FileResponse(self.config.input)
|
||||
raise HTTPException(status_code=404, detail="Input image not found")
|
||||
|
||||
def api_geninfo(self, file: UploadFile) -> GenInfoResponse:
|
||||
_, _, info = load_img(file.file.read(), return_info=True)
|
||||
parts = info.get("parameters", "").split("Negative prompt: ")
|
||||
prompt = parts[0].strip()
|
||||
negative_prompt = ""
|
||||
if len(parts) > 1:
|
||||
negative_prompt = parts[1].split("\n")[0].strip()
|
||||
return GenInfoResponse(prompt=prompt, negative_prompt=negative_prompt)
|
||||
|
||||
def api_inpaint(self, req: InpaintRequest):
|
||||
image, alpha_channel, infos = decode_base64_to_image(req.image)
|
||||
mask, _, _ = decode_base64_to_image(req.mask, gray=True)
|
||||
|
||||
mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)[1]
|
||||
if image.shape[:2] != mask.shape[:2]:
|
||||
raise HTTPException(
|
||||
400,
|
||||
detail=f"Image size({image.shape[:2]}) and mask size({mask.shape[:2]}) not match.",
|
||||
)
|
||||
|
||||
if req.paint_by_example_example_image:
|
||||
paint_by_example_image, _, _ = decode_base64_to_image(
|
||||
req.paint_by_example_example_image
|
||||
)
|
||||
|
||||
start = time.time()
|
||||
rgb_np_img = self.model_manager(image, mask, req)
|
||||
logger.info(f"process time: {(time.time() - start) * 1000:.2f}ms")
|
||||
torch_gc()
|
||||
|
||||
rgb_np_img = cv2.cvtColor(rgb_np_img.astype(np.uint8), cv2.COLOR_BGR2RGB)
|
||||
rgb_res = concat_alpha_channel(rgb_np_img, alpha_channel)
|
||||
|
||||
ext = "png"
|
||||
res_img_bytes = pil_to_bytes(
|
||||
Image.fromarray(rgb_res),
|
||||
ext=ext,
|
||||
quality=self.config.quality,
|
||||
infos=infos,
|
||||
)
|
||||
|
||||
asyncio.run(self.sio.emit("diffusion_finish"))
|
||||
|
||||
return Response(
|
||||
content=res_img_bytes,
|
||||
media_type=f"image/{ext}",
|
||||
headers={"X-Seed": str(req.sd_seed)},
|
||||
)
|
||||
|
||||
def api_run_plugin_gen_image(self, req: RunPluginRequest):
|
||||
ext = "png"
|
||||
if req.name not in self.plugins:
|
||||
raise HTTPException(status_code=422, detail="Plugin not found")
|
||||
if not self.plugins[req.name].support_gen_image:
|
||||
raise HTTPException(
|
||||
status_code=422, detail="Plugin does not support output image"
|
||||
)
|
||||
rgb_np_img, alpha_channel, infos = decode_base64_to_image(req.image)
|
||||
bgr_or_rgba_np_img = self.plugins[req.name].gen_image(rgb_np_img, req)
|
||||
torch_gc()
|
||||
|
||||
if bgr_or_rgba_np_img.shape[2] == 4:
|
||||
rgba_np_img = bgr_or_rgba_np_img
|
||||
else:
|
||||
rgba_np_img = cv2.cvtColor(bgr_or_rgba_np_img, cv2.COLOR_BGR2RGB)
|
||||
rgba_np_img = concat_alpha_channel(rgba_np_img, alpha_channel)
|
||||
|
||||
return Response(
|
||||
content=pil_to_bytes(
|
||||
Image.fromarray(rgba_np_img),
|
||||
ext=ext,
|
||||
quality=self.config.quality,
|
||||
infos=infos,
|
||||
),
|
||||
media_type=f"image/{ext}",
|
||||
)
|
||||
|
||||
def api_run_plugin_gen_mask(self, req: RunPluginRequest):
|
||||
if req.name not in self.plugins:
|
||||
raise HTTPException(status_code=422, detail="Plugin not found")
|
||||
if not self.plugins[req.name].support_gen_mask:
|
||||
raise HTTPException(
|
||||
status_code=422, detail="Plugin does not support output image"
|
||||
)
|
||||
rgb_np_img, alpha_channel, infos = decode_base64_to_image(req.image)
|
||||
bgr_or_gray_mask = self.plugins[req.name].gen_mask(rgb_np_img, req)
|
||||
torch_gc()
|
||||
res_mask = gen_frontend_mask(bgr_or_gray_mask)
|
||||
return Response(
|
||||
content=numpy_to_bytes(res_mask, "png"),
|
||||
media_type="image/png",
|
||||
)
|
||||
|
||||
def api_samplers(self) -> List[str]:
|
||||
return [member.value for member in SDSampler.__members__.values()]
|
||||
|
||||
def api_adjust_mask(self, req: AdjustMaskRequest):
|
||||
mask, _, _ = decode_base64_to_image(req.mask, gray=True)
|
||||
cv2.imwrite("tmp_adjust_mask_input.png", mask)
|
||||
mask = adjust_mask(mask, req.kernel_size, req.operate)
|
||||
cv2.imwrite("tmp_adjust_mask.png", mask)
|
||||
return Response(content=numpy_to_bytes(mask, "png"), media_type="image/png")
|
||||
|
||||
def launch(self):
|
||||
self.app.include_router(self.router)
|
||||
uvicorn.run(
|
||||
self.combined_asgi_app,
|
||||
host=self.config.host,
|
||||
port=self.config.port,
|
||||
timeout_keep_alive=999999999,
|
||||
)
|
||||
|
||||
def _build_file_manager(self) -> Optional[FileManager]:
|
||||
if self.config.input and self.config.input.is_dir():
|
||||
logger.info(
|
||||
f"Input is directory, initialize file manager {self.config.input}"
|
||||
)
|
||||
|
||||
return FileManager(
|
||||
app=self.app,
|
||||
input_dir=self.config.input,
|
||||
output_dir=self.config.output_dir,
|
||||
)
|
||||
return None
|
||||
|
||||
def _build_plugins(self) -> Dict[str, BasePlugin]:
|
||||
return build_plugins(
|
||||
self.config.enable_interactive_seg,
|
||||
self.config.interactive_seg_model,
|
||||
self.config.interactive_seg_device,
|
||||
self.config.enable_remove_bg,
|
||||
self.config.enable_anime_seg,
|
||||
self.config.enable_realesrgan,
|
||||
self.config.realesrgan_device,
|
||||
self.config.realesrgan_model,
|
||||
self.config.enable_gfpgan,
|
||||
self.config.gfpgan_device,
|
||||
self.config.enable_restoreformer,
|
||||
self.config.restoreformer_device,
|
||||
self.config.no_half,
|
||||
)
|
||||
|
||||
def _build_model_manager(self):
|
||||
return ModelManager(
|
||||
name=self.config.model,
|
||||
device=torch.device(self.config.device),
|
||||
no_half=self.config.no_half,
|
||||
disable_nsfw=self.config.disable_nsfw_checker,
|
||||
sd_cpu_textencoder=self.config.cpu_textencoder,
|
||||
cpu_offload=self.config.cpu_offload,
|
||||
callback=diffuser_callback,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from iopaint.schema import InteractiveSegModel, RealESRGANModel
|
||||
|
||||
app = FastAPI()
|
||||
api = Api(
|
||||
app,
|
||||
ApiConfig(
|
||||
host="127.0.0.1",
|
||||
port=8080,
|
||||
model="lama",
|
||||
no_half=False,
|
||||
cpu_offload=False,
|
||||
disable_nsfw_checker=False,
|
||||
cpu_textencoder=False,
|
||||
device="cpu",
|
||||
gui=False,
|
||||
disable_model_switch=False,
|
||||
input="/Users/cwq/code/github/MI-GAN/examples/places2_512_object/images",
|
||||
output_dir="/Users/cwq/code/github/lama-cleaner/tmp",
|
||||
quality=100,
|
||||
enable_interactive_seg=False,
|
||||
interactive_seg_model=InteractiveSegModel.vit_b,
|
||||
interactive_seg_device="cpu",
|
||||
enable_remove_bg=False,
|
||||
enable_anime_seg=False,
|
||||
enable_realesrgan=False,
|
||||
realesrgan_device="cpu",
|
||||
realesrgan_model=RealESRGANModel.realesr_general_x4v3,
|
||||
enable_gfpgan=False,
|
||||
gfpgan_device="cpu",
|
||||
enable_restoreformer=False,
|
||||
restoreformer_device="cpu",
|
||||
),
|
||||
)
|
||||
api.launch()
|
||||
120
iopaint/batch_processing.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import json
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from PIL import Image
|
||||
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.progress import (
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TimeElapsedColumn,
|
||||
MofNCompleteColumn,
|
||||
TextColumn,
|
||||
BarColumn,
|
||||
TaskProgressColumn,
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
|
||||
from iopaint.helper import pil_to_bytes
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
def glob_images(path: Path) -> Dict[str, Path]:
|
||||
# png/jpg/jpeg
|
||||
if path.is_file():
|
||||
return {path.stem: path}
|
||||
elif path.is_dir():
|
||||
res = {}
|
||||
for it in path.glob("*.*"):
|
||||
if it.suffix.lower() in [".png", ".jpg", ".jpeg"]:
|
||||
res[it.stem] = it
|
||||
return res
|
||||
|
||||
|
||||
def batch_inpaint(
|
||||
model: str,
|
||||
device,
|
||||
image: Path,
|
||||
mask: Path,
|
||||
output: Path,
|
||||
config: Optional[Path] = None,
|
||||
concat: bool = False,
|
||||
):
|
||||
if image.is_dir() and output.is_file():
|
||||
logger.error(
|
||||
f"invalid --output: when image is a directory, output should be a directory"
|
||||
)
|
||||
exit(-1)
|
||||
|
||||
image_paths = glob_images(image)
|
||||
mask_paths = glob_images(mask)
|
||||
if len(image_paths) == 0:
|
||||
logger.error(f"invalid --image: empty image folder")
|
||||
exit(-1)
|
||||
if len(mask_paths) == 0:
|
||||
logger.error(f"invalid --mask: empty mask folder")
|
||||
exit(-1)
|
||||
|
||||
if config is None:
|
||||
inpaint_request = InpaintRequest()
|
||||
logger.info(f"Using default config: {inpaint_request}")
|
||||
else:
|
||||
with open(config, "r", encoding="utf-8") as f:
|
||||
inpaint_request = InpaintRequest(**json.load(f))
|
||||
|
||||
model_manager = ModelManager(name=model, device=device)
|
||||
first_mask = list(mask_paths.values())[0]
|
||||
|
||||
console = Console()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
MofNCompleteColumn(),
|
||||
TimeElapsedColumn(),
|
||||
console=console,
|
||||
transient=False,
|
||||
) as progress:
|
||||
task = progress.add_task("Batch processing...", total=len(image_paths))
|
||||
for stem, image_p in image_paths.items():
|
||||
if stem not in mask_paths and mask.is_dir():
|
||||
progress.log(f"mask for {image_p} not found")
|
||||
progress.update(task, advance=1)
|
||||
continue
|
||||
mask_p = mask_paths.get(stem, first_mask)
|
||||
|
||||
infos = Image.open(image_p).info
|
||||
|
||||
img = cv2.imread(str(image_p))
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
|
||||
mask_img = cv2.imread(str(mask_p), cv2.IMREAD_GRAYSCALE)
|
||||
if mask_img.shape[:2] != img.shape[:2]:
|
||||
progress.log(
|
||||
f"resize mask {mask_p.name} to image {image_p.name} size: {img.shape[:2]}"
|
||||
)
|
||||
mask_img = cv2.resize(
|
||||
mask_img,
|
||||
(img.shape[1], img.shape[0]),
|
||||
interpolation=cv2.INTER_NEAREST,
|
||||
)
|
||||
mask_img[mask_img >= 127] = 255
|
||||
mask_img[mask_img < 127] = 0
|
||||
|
||||
# bgr
|
||||
inpaint_result = model_manager(img, mask_img, inpaint_request)
|
||||
inpaint_result = cv2.cvtColor(inpaint_result, cv2.COLOR_BGR2RGB)
|
||||
if concat:
|
||||
mask_img = cv2.cvtColor(mask_img, cv2.COLOR_GRAY2RGB)
|
||||
inpaint_result = cv2.hconcat([img, mask_img, inpaint_result])
|
||||
|
||||
img_bytes = pil_to_bytes(Image.fromarray(inpaint_result), "png", 100, infos)
|
||||
save_p = output / f"{stem}.png"
|
||||
with open(save_p, "wb") as fw:
|
||||
fw.write(img_bytes)
|
||||
|
||||
progress.update(task, advance=1)
|
||||
109
iopaint/benchmark.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import nvidia_smi
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import InpaintRequest, HDStrategy, SDSampler
|
||||
|
||||
try:
|
||||
torch._C._jit_override_can_fuse_on_cpu(False)
|
||||
torch._C._jit_override_can_fuse_on_gpu(False)
|
||||
torch._C._jit_set_texpr_fuser_enabled(False)
|
||||
torch._C._jit_set_nvfuser_enabled(False)
|
||||
except:
|
||||
pass
|
||||
|
||||
NUM_THREADS = str(4)
|
||||
|
||||
os.environ["OMP_NUM_THREADS"] = NUM_THREADS
|
||||
os.environ["OPENBLAS_NUM_THREADS"] = NUM_THREADS
|
||||
os.environ["MKL_NUM_THREADS"] = NUM_THREADS
|
||||
os.environ["VECLIB_MAXIMUM_THREADS"] = NUM_THREADS
|
||||
os.environ["NUMEXPR_NUM_THREADS"] = NUM_THREADS
|
||||
if os.environ.get("CACHE_DIR"):
|
||||
os.environ["TORCH_HOME"] = os.environ["CACHE_DIR"]
|
||||
|
||||
|
||||
def run_model(model, size):
|
||||
# RGB
|
||||
image = np.random.randint(0, 256, (size[0], size[1], 3)).astype(np.uint8)
|
||||
mask = np.random.randint(0, 255, size).astype(np.uint8)
|
||||
|
||||
config = InpaintRequest(
|
||||
ldm_steps=2,
|
||||
hd_strategy=HDStrategy.ORIGINAL,
|
||||
hd_strategy_crop_margin=128,
|
||||
hd_strategy_crop_trigger_size=128,
|
||||
hd_strategy_resize_limit=128,
|
||||
prompt="a fox is sitting on a bench",
|
||||
sd_steps=5,
|
||||
sd_sampler=SDSampler.ddim,
|
||||
)
|
||||
model(image, mask, config)
|
||||
|
||||
|
||||
def benchmark(model, times: int, empty_cache: bool):
|
||||
sizes = [(512, 512)]
|
||||
|
||||
nvidia_smi.nvmlInit()
|
||||
device_id = 0
|
||||
handle = nvidia_smi.nvmlDeviceGetHandleByIndex(device_id)
|
||||
|
||||
def format(metrics):
|
||||
return f"{np.mean(metrics):.2f} ± {np.std(metrics):.2f}"
|
||||
|
||||
process = psutil.Process(os.getpid())
|
||||
# 每个 size 给出显存和内存占用的指标
|
||||
for size in sizes:
|
||||
torch.cuda.empty_cache()
|
||||
time_metrics = []
|
||||
cpu_metrics = []
|
||||
memory_metrics = []
|
||||
gpu_memory_metrics = []
|
||||
for _ in range(times):
|
||||
start = time.time()
|
||||
run_model(model, size)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# cpu_metrics.append(process.cpu_percent())
|
||||
time_metrics.append((time.time() - start) * 1000)
|
||||
memory_metrics.append(process.memory_info().rss / 1024 / 1024)
|
||||
gpu_memory_metrics.append(
|
||||
nvidia_smi.nvmlDeviceGetMemoryInfo(handle).used / 1024 / 1024
|
||||
)
|
||||
|
||||
print(f"size: {size}".center(80, "-"))
|
||||
# print(f"cpu: {format(cpu_metrics)}")
|
||||
print(f"latency: {format(time_metrics)}ms")
|
||||
print(f"memory: {format(memory_metrics)} MB")
|
||||
print(f"gpu memory: {format(gpu_memory_metrics)} MB")
|
||||
|
||||
nvidia_smi.nvmlShutdown()
|
||||
|
||||
|
||||
def get_args_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--name")
|
||||
parser.add_argument("--device", default="cuda", type=str)
|
||||
parser.add_argument("--times", default=10, type=int)
|
||||
parser.add_argument("--empty-cache", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = get_args_parser()
|
||||
device = torch.device(args.device)
|
||||
model = ModelManager(
|
||||
name=args.name,
|
||||
device=device,
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=True,
|
||||
)
|
||||
benchmark(model, args.times, args.empty_cache)
|
||||
173
iopaint/cli.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
import typer
|
||||
from fastapi import FastAPI
|
||||
from loguru import logger
|
||||
from typer import Option
|
||||
|
||||
from iopaint.const import *
|
||||
from iopaint.download import cli_download_model, scan_models
|
||||
from iopaint.runtime import setup_model_dir, dump_environment_info, check_device
|
||||
|
||||
typer_app = typer.Typer(pretty_exceptions_show_locals=False, add_completion=False)
|
||||
|
||||
|
||||
@typer_app.command(help="Install all plugins dependencies")
|
||||
def install_plugins_packages():
|
||||
from iopaint.installer import install_plugins_package
|
||||
|
||||
install_plugins_package()
|
||||
|
||||
|
||||
@typer_app.command(help="Download SD/SDXL normal/inpainting model from HuggingFace")
|
||||
def download(
|
||||
model: str = Option(
|
||||
..., help="Model id on HuggingFace e.g: runwayml/stable-diffusion-inpainting"
|
||||
),
|
||||
model_dir: Path = Option(DEFAULT_MODEL_DIR, help=MODEL_DIR_HELP, file_okay=False),
|
||||
):
|
||||
cli_download_model(model, model_dir)
|
||||
|
||||
|
||||
@typer_app.command(name="list", help="List downloaded models")
|
||||
def list_model(
|
||||
model_dir: Path = Option(DEFAULT_MODEL_DIR, help=MODEL_DIR_HELP, file_okay=False),
|
||||
):
|
||||
setup_model_dir(model_dir)
|
||||
scanned_models = scan_models()
|
||||
for it in scanned_models:
|
||||
print(it.name)
|
||||
|
||||
|
||||
@typer_app.command(help="Batch processing images")
|
||||
def run(
|
||||
model: str = Option("lama"),
|
||||
device: Device = Option(Device.cpu),
|
||||
image: Path = Option(..., help="Image folders or file path"),
|
||||
mask: Path = Option(
|
||||
...,
|
||||
help="Mask folders or file path. "
|
||||
"If it is a directory, the mask images in the directory should have the same name as the original image."
|
||||
"If it is a file, all images will use this mask."
|
||||
"Mask will automatically resize to the same size as the original image.",
|
||||
),
|
||||
output: Path = Option(..., help="Output directory or file path"),
|
||||
config: Path = Option(
|
||||
None, help="Config file path. You can use dump command to create a base config."
|
||||
),
|
||||
concat: bool = Option(
|
||||
False, help="Concat original image, mask and output images into one image"
|
||||
),
|
||||
model_dir: Path = Option(DEFAULT_MODEL_DIR, help=MODEL_DIR_HELP, file_okay=False),
|
||||
):
|
||||
setup_model_dir(model_dir)
|
||||
scanned_models = scan_models()
|
||||
if model not in [it.name for it in scanned_models]:
|
||||
logger.info(f"{model} not found in {model_dir}, try to downloading")
|
||||
cli_download_model(model, model_dir)
|
||||
|
||||
from iopaint.batch_processing import batch_inpaint
|
||||
|
||||
batch_inpaint(model, device, image, mask, output, config, concat)
|
||||
|
||||
|
||||
@typer_app.command(help="Start IOPaint server")
|
||||
def start(
|
||||
host: str = Option("127.0.0.1"),
|
||||
port: int = Option(8080),
|
||||
model: str = Option(
|
||||
DEFAULT_MODEL,
|
||||
help=f"Available erase models: [{', '.join(AVAILABLE_MODELS)}]. "
|
||||
f"You can use download command to download other SD/SDXL normal/inpainting models on huggingface",
|
||||
),
|
||||
model_dir: Path = Option(
|
||||
DEFAULT_MODEL_DIR, help=MODEL_DIR_HELP, dir_okay=True, file_okay=False
|
||||
),
|
||||
no_half: bool = Option(False, help=NO_HALF_HELP),
|
||||
cpu_offload: bool = Option(False, help=CPU_OFFLOAD_HELP),
|
||||
disable_nsfw_checker: bool = Option(False, help=DISABLE_NSFW_HELP),
|
||||
cpu_textencoder: bool = Option(False, help=CPU_TEXTENCODER_HELP),
|
||||
local_files_only: bool = Option(False, help=LOCAL_FILES_ONLY_HELP),
|
||||
device: Device = Option(Device.cpu),
|
||||
gui: bool = Option(False, help=GUI_HELP),
|
||||
disable_model_switch: bool = Option(False),
|
||||
input: Path = Option(None, help=INPUT_HELP),
|
||||
output_dir: Path = Option(
|
||||
None, help=OUTPUT_DIR_HELP, dir_okay=True, file_okay=False
|
||||
),
|
||||
quality: int = Option(95, help=QUALITY_HELP),
|
||||
enable_interactive_seg: bool = Option(False, help=INTERACTIVE_SEG_HELP),
|
||||
interactive_seg_model: InteractiveSegModel = Option(
|
||||
InteractiveSegModel.vit_b, help=INTERACTIVE_SEG_MODEL_HELP
|
||||
),
|
||||
interactive_seg_device: Device = Option(Device.cpu),
|
||||
enable_remove_bg: bool = Option(False, help=REMOVE_BG_HELP),
|
||||
enable_anime_seg: bool = Option(False, help=ANIMESEG_HELP),
|
||||
enable_realesrgan: bool = Option(False),
|
||||
realesrgan_device: Device = Option(Device.cpu),
|
||||
realesrgan_model: RealESRGANModel = Option(RealESRGANModel.realesr_general_x4v3),
|
||||
enable_gfpgan: bool = Option(False),
|
||||
gfpgan_device: Device = Option(Device.cpu),
|
||||
enable_restoreformer: bool = Option(False),
|
||||
restoreformer_device: Device = Option(Device.cpu),
|
||||
):
|
||||
dump_environment_info()
|
||||
device = check_device(device)
|
||||
if input and not input.exists():
|
||||
logger.error(f"invalid --input: {input} not exists")
|
||||
exit()
|
||||
if output_dir:
|
||||
output_dir = output_dir.expanduser().absolute()
|
||||
logger.info(f"Image will be saved to {output_dir}")
|
||||
if not output_dir.exists():
|
||||
logger.info(f"Create output directory {output_dir}")
|
||||
output_dir.mkdir(parents=True)
|
||||
|
||||
model_dir = model_dir.expanduser().absolute()
|
||||
setup_model_dir(model_dir)
|
||||
|
||||
if local_files_only:
|
||||
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||
|
||||
scanned_models = scan_models()
|
||||
if model not in [it.name for it in scanned_models]:
|
||||
logger.info(f"{model} not found in {model_dir}, try to downloading")
|
||||
cli_download_model(model, model_dir)
|
||||
|
||||
from iopaint.api import Api
|
||||
from iopaint.schema import ApiConfig
|
||||
|
||||
app = FastAPI()
|
||||
api = Api(
|
||||
app,
|
||||
ApiConfig(
|
||||
host=host,
|
||||
port=port,
|
||||
model=model,
|
||||
no_half=no_half,
|
||||
cpu_offload=cpu_offload,
|
||||
disable_nsfw_checker=disable_nsfw_checker,
|
||||
cpu_textencoder=cpu_textencoder,
|
||||
device=device,
|
||||
gui=gui,
|
||||
disable_model_switch=disable_model_switch,
|
||||
input=input,
|
||||
output_dir=output_dir,
|
||||
quality=quality,
|
||||
enable_interactive_seg=enable_interactive_seg,
|
||||
interactive_seg_model=interactive_seg_model,
|
||||
interactive_seg_device=interactive_seg_device,
|
||||
enable_remove_bg=enable_remove_bg,
|
||||
enable_anime_seg=enable_anime_seg,
|
||||
enable_realesrgan=enable_realesrgan,
|
||||
realesrgan_device=realesrgan_device,
|
||||
realesrgan_model=realesrgan_model,
|
||||
enable_gfpgan=enable_gfpgan,
|
||||
gfpgan_device=gfpgan_device,
|
||||
enable_restoreformer=enable_restoreformer,
|
||||
restoreformer_device=restoreformer_device,
|
||||
),
|
||||
)
|
||||
api.launch()
|
||||
175
iopaint/const.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import json
|
||||
import os
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
DIFFUSERS_SD_CLASS_NAME = "StableDiffusionPipeline"
|
||||
DIFFUSERS_SD_INPAINT_CLASS_NAME = "StableDiffusionInpaintPipeline"
|
||||
DIFFUSERS_SDXL_CLASS_NAME = "StableDiffusionXLPipeline"
|
||||
DIFFUSERS_SDXL_INPAINT_CLASS_NAME = "StableDiffusionXLInpaintPipeline"
|
||||
|
||||
MPS_UNSUPPORT_MODELS = [
|
||||
"lama",
|
||||
"ldm",
|
||||
"zits",
|
||||
"mat",
|
||||
"fcf",
|
||||
"cv2",
|
||||
"manga",
|
||||
]
|
||||
|
||||
DEFAULT_MODEL = "lama"
|
||||
AVAILABLE_MODELS = ["lama", "ldm", "zits", "mat", "fcf", "manga", "cv2", "migan"]
|
||||
|
||||
|
||||
AVAILABLE_DEVICES = ["cuda", "cpu", "mps"]
|
||||
DEFAULT_DEVICE = "cuda"
|
||||
|
||||
NO_HALF_HELP = """
|
||||
Using full precision(fp32) model.
|
||||
If your diffusion model generate result is always black or green, use this argument.
|
||||
"""
|
||||
|
||||
CPU_OFFLOAD_HELP = """
|
||||
Offloads diffusion model's weight to CPU RAM, significantly reducing vRAM usage.
|
||||
"""
|
||||
|
||||
DISABLE_NSFW_HELP = """
|
||||
Disable NSFW checker for diffusion model.
|
||||
"""
|
||||
|
||||
CPU_TEXTENCODER_HELP = """
|
||||
Run diffusion models text encoder on CPU to reduce vRAM usage.
|
||||
"""
|
||||
|
||||
SD_CONTROLNET_CHOICES = [
|
||||
"lllyasviel/control_v11p_sd15_canny",
|
||||
# "lllyasviel/control_v11p_sd15_seg",
|
||||
"lllyasviel/control_v11p_sd15_openpose",
|
||||
"lllyasviel/control_v11p_sd15_inpaint",
|
||||
"lllyasviel/control_v11f1p_sd15_depth",
|
||||
]
|
||||
|
||||
SD2_CONTROLNET_CHOICES = [
|
||||
"thibaud/controlnet-sd21-canny-diffusers",
|
||||
"thibaud/controlnet-sd21-depth-diffusers",
|
||||
"thibaud/controlnet-sd21-openpose-diffusers",
|
||||
]
|
||||
|
||||
SDXL_CONTROLNET_CHOICES = [
|
||||
"thibaud/controlnet-openpose-sdxl-1.0",
|
||||
"destitech/controlnet-inpaint-dreamer-sdxl",
|
||||
"diffusers/controlnet-canny-sdxl-1.0",
|
||||
"diffusers/controlnet-canny-sdxl-1.0-mid",
|
||||
"diffusers/controlnet-canny-sdxl-1.0-small",
|
||||
"diffusers/controlnet-depth-sdxl-1.0",
|
||||
"diffusers/controlnet-depth-sdxl-1.0-mid",
|
||||
"diffusers/controlnet-depth-sdxl-1.0-small",
|
||||
]
|
||||
|
||||
LOCAL_FILES_ONLY_HELP = """
|
||||
When loading diffusion models, using local files only, not connect to HuggingFace server.
|
||||
"""
|
||||
|
||||
DEFAULT_MODEL_DIR = os.getenv(
|
||||
"XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")
|
||||
)
|
||||
MODEL_DIR_HELP = f"""
|
||||
Model download directory (by setting XDG_CACHE_HOME environment variable), by default model download to {DEFAULT_MODEL_DIR}
|
||||
"""
|
||||
|
||||
OUTPUT_DIR_HELP = """
|
||||
Result images will be saved to output directory automatically.
|
||||
"""
|
||||
|
||||
INPUT_HELP = """
|
||||
If input is image, it will be loaded by default.
|
||||
If input is directory, you can browse and select image in file manager.
|
||||
"""
|
||||
|
||||
GUI_HELP = """
|
||||
Launch Lama Cleaner as desktop app
|
||||
"""
|
||||
|
||||
QUALITY_HELP = """
|
||||
Quality of image encoding, 0-100. Default is 95, higher quality will generate larger file size.
|
||||
"""
|
||||
|
||||
|
||||
class Choices(str, Enum):
|
||||
@classmethod
|
||||
def values(cls):
|
||||
return [member.value for member in cls]
|
||||
|
||||
|
||||
class RealESRGANModel(Choices):
|
||||
realesr_general_x4v3 = "realesr-general-x4v3"
|
||||
RealESRGAN_x4plus = "RealESRGAN_x4plus"
|
||||
RealESRGAN_x4plus_anime_6B = "RealESRGAN_x4plus_anime_6B"
|
||||
|
||||
|
||||
class Device(Choices):
|
||||
cpu = "cpu"
|
||||
cuda = "cuda"
|
||||
mps = "mps"
|
||||
|
||||
|
||||
class InteractiveSegModel(Choices):
|
||||
vit_b = "vit_b"
|
||||
vit_l = "vit_l"
|
||||
vit_h = "vit_h"
|
||||
mobile_sam = "mobile_sam"
|
||||
|
||||
|
||||
INTERACTIVE_SEG_HELP = "Enable interactive segmentation using Segment Anything."
|
||||
INTERACTIVE_SEG_MODEL_HELP = "Model size: vit_b < vit_l < vit_h. Bigger model size means better segmentation but slower speed."
|
||||
REMOVE_BG_HELP = "Enable remove background. Always run on CPU"
|
||||
ANIMESEG_HELP = "Enable anime segmentation. Always run on CPU"
|
||||
REALESRGAN_HELP = "Enable realesrgan super resolution"
|
||||
GFPGAN_HELP = (
|
||||
"Enable GFPGAN face restore. To enhance background, use with --enable-realesrgan"
|
||||
)
|
||||
RESTOREFORMER_HELP = "Enable RestoreFormer face restore. To enhance background, use with --enable-realesrgan"
|
||||
GIF_HELP = "Enable GIF plugin. Make GIF to compare original and cleaned image"
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8080
|
||||
model: str = DEFAULT_MODEL
|
||||
sd_local_model_path: str = None
|
||||
device: str = DEFAULT_DEVICE
|
||||
gui: bool = False
|
||||
no_gui_auto_close: bool = False
|
||||
no_half: bool = False
|
||||
cpu_offload: bool = False
|
||||
disable_nsfw: bool = False
|
||||
sd_cpu_textencoder: bool = False
|
||||
local_files_only: bool = False
|
||||
model_dir: str = DEFAULT_MODEL_DIR
|
||||
input: str = None
|
||||
output_dir: str = None
|
||||
# plugins
|
||||
enable_interactive_seg: bool = False
|
||||
interactive_seg_model: str = "vit_l"
|
||||
interactive_seg_device: str = "cpu"
|
||||
enable_remove_bg: bool = False
|
||||
enable_anime_seg: bool = False
|
||||
enable_realesrgan: bool = False
|
||||
realesrgan_device: str = "cpu"
|
||||
realesrgan_model: str = RealESRGANModel.realesr_general_x4v3.value
|
||||
realesrgan_no_half: bool = False
|
||||
enable_gfpgan: bool = False
|
||||
gfpgan_device: str = "cpu"
|
||||
enable_restoreformer: bool = False
|
||||
restoreformer_device: str = "cpu"
|
||||
enable_gif: bool = False
|
||||
|
||||
|
||||
def load_config(installer_config: str):
|
||||
if os.path.exists(installer_config):
|
||||
with open(installer_config, "r", encoding="utf-8") as f:
|
||||
return Config(**json.load(f))
|
||||
else:
|
||||
return Config()
|
||||
147
iopaint/download.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from huggingface_hub.constants import HF_HUB_CACHE
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
|
||||
from iopaint.const import (
|
||||
DEFAULT_MODEL_DIR,
|
||||
DIFFUSERS_SD_CLASS_NAME,
|
||||
DIFFUSERS_SD_INPAINT_CLASS_NAME,
|
||||
DIFFUSERS_SDXL_CLASS_NAME,
|
||||
DIFFUSERS_SDXL_INPAINT_CLASS_NAME,
|
||||
)
|
||||
from iopaint.model.utils import handle_from_pretrained_exceptions
|
||||
from iopaint.model_info import ModelInfo, ModelType
|
||||
from iopaint.runtime import setup_model_dir
|
||||
|
||||
|
||||
def cli_download_model(model: str, model_dir: Path):
|
||||
setup_model_dir(model_dir)
|
||||
from iopaint.model import models
|
||||
|
||||
if model in models and models[model].is_erase_model:
|
||||
logger.info(f"Downloading {model}...")
|
||||
models[model].download()
|
||||
logger.info(f"Done.")
|
||||
else:
|
||||
logger.info(f"Downloading model from Huggingface: {model}")
|
||||
from diffusers import DiffusionPipeline
|
||||
|
||||
downloaded_path = handle_from_pretrained_exceptions(
|
||||
DiffusionPipeline.download,
|
||||
pretrained_model_name=model,
|
||||
variant="fp16",
|
||||
resume_download=True,
|
||||
)
|
||||
logger.info(f"Done. Downloaded to {downloaded_path}")
|
||||
|
||||
|
||||
def folder_name_to_show_name(name: str) -> str:
|
||||
return name.replace("models--", "").replace("--", "/")
|
||||
|
||||
|
||||
def scan_single_file_diffusion_models(cache_dir) -> List[ModelInfo]:
|
||||
cache_dir = Path(cache_dir)
|
||||
stable_diffusion_dir = cache_dir / "stable_diffusion"
|
||||
stable_diffusion_xl_dir = cache_dir / "stable_diffusion_xl"
|
||||
# logger.info(f"Scanning single file sd/sdxl models in {cache_dir}")
|
||||
res = []
|
||||
for it in stable_diffusion_dir.glob(f"*.*"):
|
||||
if it.suffix not in [".safetensors", ".ckpt"]:
|
||||
continue
|
||||
if "inpaint" in str(it).lower():
|
||||
model_type = ModelType.DIFFUSERS_SD_INPAINT
|
||||
else:
|
||||
model_type = ModelType.DIFFUSERS_SD
|
||||
res.append(
|
||||
ModelInfo(
|
||||
name=it.name,
|
||||
path=str(it.absolute()),
|
||||
model_type=model_type,
|
||||
is_single_file_diffusers=True,
|
||||
)
|
||||
)
|
||||
|
||||
for it in stable_diffusion_xl_dir.glob(f"*.*"):
|
||||
if it.suffix not in [".safetensors", ".ckpt"]:
|
||||
continue
|
||||
if "inpaint" in str(it).lower():
|
||||
model_type = ModelType.DIFFUSERS_SDXL_INPAINT
|
||||
else:
|
||||
model_type = ModelType.DIFFUSERS_SDXL
|
||||
res.append(
|
||||
ModelInfo(
|
||||
name=it.name,
|
||||
path=str(it.absolute()),
|
||||
model_type=model_type,
|
||||
is_single_file_diffusers=True,
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def scan_inpaint_models(model_dir: Path) -> List[ModelInfo]:
|
||||
res = []
|
||||
from iopaint.model import models
|
||||
|
||||
# logger.info(f"Scanning inpaint models in {model_dir}")
|
||||
|
||||
for name, m in models.items():
|
||||
if m.is_erase_model and m.is_downloaded():
|
||||
res.append(
|
||||
ModelInfo(
|
||||
name=name,
|
||||
path=name,
|
||||
model_type=ModelType.INPAINT,
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def scan_models() -> List[ModelInfo]:
|
||||
model_dir = os.getenv("XDG_CACHE_HOME", DEFAULT_MODEL_DIR)
|
||||
available_models = []
|
||||
available_models.extend(scan_inpaint_models(model_dir))
|
||||
available_models.extend(scan_single_file_diffusion_models(model_dir))
|
||||
cache_dir = Path(HF_HUB_CACHE)
|
||||
# logger.info(f"Scanning diffusers models in {cache_dir}")
|
||||
diffusers_model_names = []
|
||||
for it in cache_dir.glob("**/*/model_index.json"):
|
||||
with open(it, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
_class_name = data["_class_name"]
|
||||
name = folder_name_to_show_name(it.parent.parent.parent.name)
|
||||
if name in diffusers_model_names:
|
||||
continue
|
||||
if "PowerPaint" in name:
|
||||
model_type = ModelType.DIFFUSERS_OTHER
|
||||
elif _class_name == DIFFUSERS_SD_CLASS_NAME:
|
||||
model_type = ModelType.DIFFUSERS_SD
|
||||
elif _class_name == DIFFUSERS_SD_INPAINT_CLASS_NAME:
|
||||
model_type = ModelType.DIFFUSERS_SD_INPAINT
|
||||
elif _class_name == DIFFUSERS_SDXL_CLASS_NAME:
|
||||
model_type = ModelType.DIFFUSERS_SDXL
|
||||
elif _class_name == DIFFUSERS_SDXL_INPAINT_CLASS_NAME:
|
||||
model_type = ModelType.DIFFUSERS_SDXL_INPAINT
|
||||
elif _class_name in [
|
||||
"StableDiffusionInstructPix2PixPipeline",
|
||||
"PaintByExamplePipeline",
|
||||
"KandinskyV22InpaintPipeline",
|
||||
]:
|
||||
model_type = ModelType.DIFFUSERS_OTHER
|
||||
else:
|
||||
continue
|
||||
|
||||
diffusers_model_names.append(name)
|
||||
available_models.append(
|
||||
ModelInfo(
|
||||
name=name,
|
||||
path=name,
|
||||
model_type=model_type,
|
||||
)
|
||||
)
|
||||
|
||||
return available_models
|
||||
1
iopaint/file_manager/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .file_manager import FileManager
|
||||
222
iopaint/file_manager/file_manager.py
Normal file
@@ -0,0 +1,222 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PIL import Image, ImageOps, PngImagePlugin
|
||||
from fastapi import FastAPI, UploadFile, HTTPException
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from ..schema import MediasResponse, MediaTab
|
||||
|
||||
LARGE_ENOUGH_NUMBER = 100
|
||||
PngImagePlugin.MAX_TEXT_CHUNK = LARGE_ENOUGH_NUMBER * (1024**2)
|
||||
from .storage_backends import FilesystemStorageBackend
|
||||
from .utils import aspect_to_string, generate_filename, glob_img
|
||||
|
||||
|
||||
class FileManager:
|
||||
def __init__(self, app: FastAPI, input_dir: Path, output_dir: Path):
|
||||
self.app = app
|
||||
self.input_dir: Path = input_dir
|
||||
self.output_dir: Path = output_dir
|
||||
|
||||
self.image_dir_filenames = []
|
||||
self.output_dir_filenames = []
|
||||
if not self.thumbnail_directory.exists():
|
||||
self.thumbnail_directory.mkdir(parents=True)
|
||||
|
||||
# fmt: off
|
||||
self.app.add_api_route("/api/v1/save_image", self.api_save_image, methods=["POST"])
|
||||
self.app.add_api_route("/api/v1/medias", self.api_medias, methods=["GET"], response_model=List[MediasResponse])
|
||||
self.app.add_api_route("/api/v1/media_file", self.api_media_file, methods=["GET"])
|
||||
self.app.add_api_route("/api/v1/media_thumbnail_file", self.api_media_thumbnail_file, methods=["GET"])
|
||||
# fmt: on
|
||||
|
||||
def api_save_image(self, file: UploadFile):
|
||||
filename = file.filename
|
||||
origin_image_bytes = file.file.read()
|
||||
with open(self.output_dir / filename, "wb") as fw:
|
||||
fw.write(origin_image_bytes)
|
||||
|
||||
def api_medias(self, tab: MediaTab) -> List[MediasResponse]:
|
||||
img_dir = self._get_dir(tab)
|
||||
return self._media_names(img_dir)
|
||||
|
||||
def api_media_file(self, tab: MediaTab, filename: str) -> FileResponse:
|
||||
file_path = self._get_file(tab, filename)
|
||||
return FileResponse(file_path, media_type="image/png")
|
||||
|
||||
# tab=${tab}?filename=${filename.name}?width=${width}&height=${height}
|
||||
def api_media_thumbnail_file(
|
||||
self, tab: MediaTab, filename: str, width: int, height: int
|
||||
) -> FileResponse:
|
||||
img_dir = self._get_dir(tab)
|
||||
thumb_filename, (width, height) = self.get_thumbnail(
|
||||
img_dir, filename, width=width, height=height
|
||||
)
|
||||
thumbnail_filepath = self.thumbnail_directory / thumb_filename
|
||||
return FileResponse(
|
||||
thumbnail_filepath,
|
||||
headers={
|
||||
"X-Width": str(width),
|
||||
"X-Height": str(height),
|
||||
},
|
||||
media_type="image/jpeg",
|
||||
)
|
||||
|
||||
def _get_dir(self, tab: MediaTab) -> Path:
|
||||
if tab == "input":
|
||||
return self.input_dir
|
||||
elif tab == "output":
|
||||
return self.output_dir
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail=f"tab not found: {tab}")
|
||||
|
||||
def _get_file(self, tab: MediaTab, filename: str) -> Path:
|
||||
file_path = self._get_dir(tab) / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=422, detail=f"file not found: {file_path}")
|
||||
return file_path
|
||||
|
||||
@property
|
||||
def thumbnail_directory(self) -> Path:
|
||||
return self.output_dir / "thumbnails"
|
||||
|
||||
@staticmethod
|
||||
def _media_names(directory: Path) -> List[MediasResponse]:
|
||||
names = sorted([it.name for it in glob_img(directory)])
|
||||
res = []
|
||||
for name in names:
|
||||
path = os.path.join(directory, name)
|
||||
img = Image.open(path)
|
||||
res.append(
|
||||
MediasResponse(
|
||||
name=name,
|
||||
height=img.height,
|
||||
width=img.width,
|
||||
ctime=os.path.getctime(path),
|
||||
mtime=os.path.getmtime(path),
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
def get_thumbnail(
|
||||
self, directory: Path, original_filename: str, width, height, **options
|
||||
):
|
||||
directory = Path(directory)
|
||||
storage = FilesystemStorageBackend(self.app)
|
||||
crop = options.get("crop", "fit")
|
||||
background = options.get("background")
|
||||
quality = options.get("quality", 90)
|
||||
|
||||
original_path, original_filename = os.path.split(original_filename)
|
||||
original_filepath = os.path.join(directory, original_path, original_filename)
|
||||
image = Image.open(BytesIO(storage.read(original_filepath)))
|
||||
|
||||
# keep ratio resize
|
||||
if not width and not height:
|
||||
width = 256
|
||||
|
||||
if width != 0:
|
||||
height = int(image.height * width / image.width)
|
||||
else:
|
||||
width = int(image.width * height / image.height)
|
||||
|
||||
thumbnail_size = (width, height)
|
||||
|
||||
thumbnail_filename = generate_filename(
|
||||
directory,
|
||||
original_filename,
|
||||
aspect_to_string(thumbnail_size),
|
||||
crop,
|
||||
background,
|
||||
quality,
|
||||
)
|
||||
|
||||
thumbnail_filepath = os.path.join(
|
||||
self.thumbnail_directory, original_path, thumbnail_filename
|
||||
)
|
||||
|
||||
if storage.exists(thumbnail_filepath):
|
||||
return thumbnail_filepath, (width, height)
|
||||
|
||||
try:
|
||||
image.load()
|
||||
except (IOError, OSError):
|
||||
self.app.logger.warning("Thumbnail not load image: %s", original_filepath)
|
||||
return thumbnail_filepath, (width, height)
|
||||
|
||||
# get original image format
|
||||
options["format"] = options.get("format", image.format)
|
||||
|
||||
image = self._create_thumbnail(
|
||||
image, thumbnail_size, crop, background=background
|
||||
)
|
||||
|
||||
raw_data = self.get_raw_data(image, **options)
|
||||
storage.save(thumbnail_filepath, raw_data)
|
||||
|
||||
return thumbnail_filepath, (width, height)
|
||||
|
||||
def get_raw_data(self, image, **options):
|
||||
data = {
|
||||
"format": self._get_format(image, **options),
|
||||
"quality": options.get("quality", 90),
|
||||
}
|
||||
|
||||
_file = BytesIO()
|
||||
image.save(_file, **data)
|
||||
return _file.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def colormode(image, colormode="RGB"):
|
||||
if colormode == "RGB" or colormode == "RGBA":
|
||||
if image.mode == "RGBA":
|
||||
return image
|
||||
if image.mode == "LA":
|
||||
return image.convert("RGBA")
|
||||
return image.convert(colormode)
|
||||
|
||||
if colormode == "GRAY":
|
||||
return image.convert("L")
|
||||
|
||||
return image.convert(colormode)
|
||||
|
||||
@staticmethod
|
||||
def background(original_image, color=0xFF):
|
||||
size = (max(original_image.size),) * 2
|
||||
image = Image.new("L", size, color)
|
||||
image.paste(
|
||||
original_image,
|
||||
tuple(map(lambda x: (x[0] - x[1]) / 2, zip(size, original_image.size))),
|
||||
)
|
||||
|
||||
return image
|
||||
|
||||
def _get_format(self, image, **options):
|
||||
if options.get("format"):
|
||||
return options.get("format")
|
||||
if image.format:
|
||||
return image.format
|
||||
|
||||
return "JPEG"
|
||||
|
||||
def _create_thumbnail(self, image, size, crop="fit", background=None):
|
||||
try:
|
||||
resample = Image.Resampling.LANCZOS
|
||||
except AttributeError: # pylint: disable=raise-missing-from
|
||||
resample = Image.ANTIALIAS
|
||||
|
||||
if crop == "fit":
|
||||
image = ImageOps.fit(image, size, resample)
|
||||
else:
|
||||
image = image.copy()
|
||||
image.thumbnail(size, resample=resample)
|
||||
|
||||
if background is not None:
|
||||
image = self.background(image)
|
||||
|
||||
image = self.colormode(image)
|
||||
|
||||
return image
|
||||
46
iopaint/file_manager/storage_backends.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Copy from https://github.com/silentsokolov/flask-thumbnails/blob/master/flask_thumbnails/storage_backends.py
|
||||
import errno
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseStorageBackend(ABC):
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
|
||||
@abstractmethod
|
||||
def read(self, filepath, mode="rb", **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def exists(self, filepath):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def save(self, filepath, data):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FilesystemStorageBackend(BaseStorageBackend):
|
||||
def read(self, filepath, mode="rb", **kwargs):
|
||||
with open(filepath, mode) as f: # pylint: disable=unspecified-encoding
|
||||
return f.read()
|
||||
|
||||
def exists(self, filepath):
|
||||
return os.path.exists(filepath)
|
||||
|
||||
def save(self, filepath, data):
|
||||
directory = os.path.dirname(filepath)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
raise IOError("{} is not a directory".format(directory))
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(data)
|
||||
65
iopaint/file_manager/utils.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copy from: https://github.com/silentsokolov/flask-thumbnails/blob/master/flask_thumbnails/utils.py
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
def generate_filename(directory: Path, original_filename, *options) -> str:
|
||||
text = str(directory.absolute()) + original_filename
|
||||
for v in options:
|
||||
text += "%s" % v
|
||||
md5_hash = hashlib.md5()
|
||||
md5_hash.update(text.encode("utf-8"))
|
||||
return md5_hash.hexdigest() + ".jpg"
|
||||
|
||||
|
||||
def parse_size(size):
|
||||
if isinstance(size, int):
|
||||
# If the size parameter is a single number, assume square aspect.
|
||||
return [size, size]
|
||||
|
||||
if isinstance(size, (tuple, list)):
|
||||
if len(size) == 1:
|
||||
# If single value tuple/list is provided, exand it to two elements
|
||||
return size + type(size)(size)
|
||||
return size
|
||||
|
||||
try:
|
||||
thumbnail_size = [int(x) for x in size.lower().split("x", 1)]
|
||||
except ValueError:
|
||||
raise ValueError( # pylint: disable=raise-missing-from
|
||||
"Bad thumbnail size format. Valid format is INTxINT."
|
||||
)
|
||||
|
||||
if len(thumbnail_size) == 1:
|
||||
# If the size parameter only contains a single integer, assume square aspect.
|
||||
thumbnail_size.append(thumbnail_size[0])
|
||||
|
||||
return thumbnail_size
|
||||
|
||||
|
||||
def aspect_to_string(size):
|
||||
if isinstance(size, str):
|
||||
return size
|
||||
|
||||
return "x".join(map(str, size))
|
||||
|
||||
|
||||
IMG_SUFFIX = {".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG"}
|
||||
|
||||
|
||||
def glob_img(p: Union[Path, str], recursive: bool = False):
|
||||
p = Path(p)
|
||||
if p.is_file() and p.suffix in IMG_SUFFIX:
|
||||
yield p
|
||||
else:
|
||||
if recursive:
|
||||
files = Path(p).glob("**/*.*")
|
||||
else:
|
||||
files = Path(p).glob("*.*")
|
||||
|
||||
for it in files:
|
||||
if it.suffix not in IMG_SUFFIX:
|
||||
continue
|
||||
yield it
|
||||
399
iopaint/helper.py
Normal file
@@ -0,0 +1,399 @@
|
||||
import base64
|
||||
import imghdr
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from urllib.parse import urlparse
|
||||
import cv2
|
||||
from PIL import Image, ImageOps, PngImagePlugin
|
||||
import numpy as np
|
||||
import torch
|
||||
from iopaint.const import MPS_UNSUPPORT_MODELS
|
||||
from loguru import logger
|
||||
from torch.hub import download_url_to_file, get_dir
|
||||
import hashlib
|
||||
|
||||
|
||||
def md5sum(filename):
|
||||
md5 = hashlib.md5()
|
||||
with open(filename, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(128 * md5.block_size), b""):
|
||||
md5.update(chunk)
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def switch_mps_device(model_name, device):
|
||||
if model_name in MPS_UNSUPPORT_MODELS and str(device) == "mps":
|
||||
logger.info(f"{model_name} not support mps, switch to cpu")
|
||||
return torch.device("cpu")
|
||||
return device
|
||||
|
||||
|
||||
def get_cache_path_by_url(url):
|
||||
parts = urlparse(url)
|
||||
hub_dir = get_dir()
|
||||
model_dir = os.path.join(hub_dir, "checkpoints")
|
||||
if not os.path.isdir(model_dir):
|
||||
os.makedirs(model_dir)
|
||||
filename = os.path.basename(parts.path)
|
||||
cached_file = os.path.join(model_dir, filename)
|
||||
return cached_file
|
||||
|
||||
|
||||
def download_model(url, model_md5: str = None):
|
||||
cached_file = get_cache_path_by_url(url)
|
||||
if not os.path.exists(cached_file):
|
||||
sys.stderr.write('Downloading: "{}" to {}\n'.format(url, cached_file))
|
||||
hash_prefix = None
|
||||
download_url_to_file(url, cached_file, hash_prefix, progress=True)
|
||||
if model_md5:
|
||||
_md5 = md5sum(cached_file)
|
||||
if model_md5 == _md5:
|
||||
logger.info(f"Download model success, md5: {_md5}")
|
||||
else:
|
||||
try:
|
||||
os.remove(cached_file)
|
||||
logger.error(
|
||||
f"Model md5: {_md5}, expected md5: {model_md5}, wrong model deleted. Please restart iopaint."
|
||||
f"If you still have errors, please try download model manually first https://lama-cleaner-docs.vercel.app/install/download_model_manually.\n"
|
||||
)
|
||||
except:
|
||||
logger.error(
|
||||
f"Model md5: {_md5}, expected md5: {model_md5}, please delete {cached_file} and restart iopaint."
|
||||
)
|
||||
exit(-1)
|
||||
|
||||
return cached_file
|
||||
|
||||
|
||||
def ceil_modulo(x, mod):
|
||||
if x % mod == 0:
|
||||
return x
|
||||
return (x // mod + 1) * mod
|
||||
|
||||
|
||||
def handle_error(model_path, model_md5, e):
|
||||
_md5 = md5sum(model_path)
|
||||
if _md5 != model_md5:
|
||||
try:
|
||||
os.remove(model_path)
|
||||
logger.error(
|
||||
f"Model md5: {_md5}, expected md5: {model_md5}, wrong model deleted. Please restart iopaint."
|
||||
f"If you still have errors, please try download model manually first https://lama-cleaner-docs.vercel.app/install/download_model_manually.\n"
|
||||
)
|
||||
except:
|
||||
logger.error(
|
||||
f"Model md5: {_md5}, expected md5: {model_md5}, please delete {model_path} and restart iopaint."
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to load model {model_path},"
|
||||
f"please submit an issue at https://github.com/Sanster/lama-cleaner/issues and include a screenshot of the error:\n{e}"
|
||||
)
|
||||
exit(-1)
|
||||
|
||||
|
||||
def load_jit_model(url_or_path, device, model_md5: str):
|
||||
if os.path.exists(url_or_path):
|
||||
model_path = url_or_path
|
||||
else:
|
||||
model_path = download_model(url_or_path, model_md5)
|
||||
|
||||
logger.info(f"Loading model from: {model_path}")
|
||||
try:
|
||||
model = torch.jit.load(model_path, map_location="cpu").to(device)
|
||||
except Exception as e:
|
||||
handle_error(model_path, model_md5, e)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def load_model(model: torch.nn.Module, url_or_path, device, model_md5):
|
||||
if os.path.exists(url_or_path):
|
||||
model_path = url_or_path
|
||||
else:
|
||||
model_path = download_model(url_or_path, model_md5)
|
||||
|
||||
try:
|
||||
logger.info(f"Loading model from: {model_path}")
|
||||
state_dict = torch.load(model_path, map_location="cpu")
|
||||
model.load_state_dict(state_dict, strict=True)
|
||||
model.to(device)
|
||||
except Exception as e:
|
||||
handle_error(model_path, model_md5, e)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def numpy_to_bytes(image_numpy: np.ndarray, ext: str) -> bytes:
|
||||
data = cv2.imencode(
|
||||
f".{ext}",
|
||||
image_numpy,
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), 100, int(cv2.IMWRITE_PNG_COMPRESSION), 0],
|
||||
)[1]
|
||||
image_bytes = data.tobytes()
|
||||
return image_bytes
|
||||
|
||||
|
||||
def pil_to_bytes(pil_img, ext: str, quality: int = 95, infos={}) -> bytes:
|
||||
with io.BytesIO() as output:
|
||||
kwargs = {k: v for k, v in infos.items() if v is not None}
|
||||
if ext == "jpg":
|
||||
ext = "jpeg"
|
||||
if "png" == ext.lower() and "parameters" in kwargs:
|
||||
pnginfo_data = PngImagePlugin.PngInfo()
|
||||
pnginfo_data.add_text("parameters", kwargs["parameters"])
|
||||
kwargs["pnginfo"] = pnginfo_data
|
||||
|
||||
pil_img.save(output, format=ext, quality=quality, **kwargs)
|
||||
image_bytes = output.getvalue()
|
||||
return image_bytes
|
||||
|
||||
|
||||
def load_img(img_bytes, gray: bool = False, return_info: bool = False):
|
||||
alpha_channel = None
|
||||
image = Image.open(io.BytesIO(img_bytes))
|
||||
|
||||
if return_info:
|
||||
infos = image.info
|
||||
|
||||
try:
|
||||
image = ImageOps.exif_transpose(image)
|
||||
except:
|
||||
pass
|
||||
|
||||
if gray:
|
||||
image = image.convert("L")
|
||||
np_img = np.array(image)
|
||||
else:
|
||||
if image.mode == "RGBA":
|
||||
np_img = np.array(image)
|
||||
alpha_channel = np_img[:, :, -1]
|
||||
np_img = cv2.cvtColor(np_img, cv2.COLOR_RGBA2RGB)
|
||||
else:
|
||||
image = image.convert("RGB")
|
||||
np_img = np.array(image)
|
||||
|
||||
if return_info:
|
||||
return np_img, alpha_channel, infos
|
||||
return np_img, alpha_channel
|
||||
|
||||
|
||||
def norm_img(np_img):
|
||||
if len(np_img.shape) == 2:
|
||||
np_img = np_img[:, :, np.newaxis]
|
||||
np_img = np.transpose(np_img, (2, 0, 1))
|
||||
np_img = np_img.astype("float32") / 255
|
||||
return np_img
|
||||
|
||||
|
||||
def resize_max_size(
|
||||
np_img, size_limit: int, interpolation=cv2.INTER_CUBIC
|
||||
) -> np.ndarray:
|
||||
# Resize image's longer size to size_limit if longer size larger than size_limit
|
||||
h, w = np_img.shape[:2]
|
||||
if max(h, w) > size_limit:
|
||||
ratio = size_limit / max(h, w)
|
||||
new_w = int(w * ratio + 0.5)
|
||||
new_h = int(h * ratio + 0.5)
|
||||
return cv2.resize(np_img, dsize=(new_w, new_h), interpolation=interpolation)
|
||||
else:
|
||||
return np_img
|
||||
|
||||
|
||||
def pad_img_to_modulo(
|
||||
img: np.ndarray, mod: int, square: bool = False, min_size: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
|
||||
Args:
|
||||
img: [H, W, C]
|
||||
mod:
|
||||
square: 是否为正方形
|
||||
min_size:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if len(img.shape) == 2:
|
||||
img = img[:, :, np.newaxis]
|
||||
height, width = img.shape[:2]
|
||||
out_height = ceil_modulo(height, mod)
|
||||
out_width = ceil_modulo(width, mod)
|
||||
|
||||
if min_size is not None:
|
||||
assert min_size % mod == 0
|
||||
out_width = max(min_size, out_width)
|
||||
out_height = max(min_size, out_height)
|
||||
|
||||
if square:
|
||||
max_size = max(out_height, out_width)
|
||||
out_height = max_size
|
||||
out_width = max_size
|
||||
|
||||
return np.pad(
|
||||
img,
|
||||
((0, out_height - height), (0, out_width - width), (0, 0)),
|
||||
mode="symmetric",
|
||||
)
|
||||
|
||||
|
||||
def boxes_from_mask(mask: np.ndarray) -> List[np.ndarray]:
|
||||
"""
|
||||
Args:
|
||||
mask: (h, w, 1) 0~255
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
height, width = mask.shape[:2]
|
||||
_, thresh = cv2.threshold(mask, 127, 255, 0)
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
boxes = []
|
||||
for cnt in contours:
|
||||
x, y, w, h = cv2.boundingRect(cnt)
|
||||
box = np.array([x, y, x + w, y + h]).astype(int)
|
||||
|
||||
box[::2] = np.clip(box[::2], 0, width)
|
||||
box[1::2] = np.clip(box[1::2], 0, height)
|
||||
boxes.append(box)
|
||||
|
||||
return boxes
|
||||
|
||||
|
||||
def only_keep_largest_contour(mask: np.ndarray) -> List[np.ndarray]:
|
||||
"""
|
||||
Args:
|
||||
mask: (h, w) 0~255
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
_, thresh = cv2.threshold(mask, 127, 255, 0)
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
max_area = 0
|
||||
max_index = -1
|
||||
for i, cnt in enumerate(contours):
|
||||
area = cv2.contourArea(cnt)
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
max_index = i
|
||||
|
||||
if max_index != -1:
|
||||
new_mask = np.zeros_like(mask)
|
||||
return cv2.drawContours(new_mask, contours, max_index, 255, -1)
|
||||
else:
|
||||
return mask
|
||||
|
||||
|
||||
def is_mac():
|
||||
return sys.platform == "darwin"
|
||||
|
||||
|
||||
def get_image_ext(img_bytes):
|
||||
w = imghdr.what("", img_bytes)
|
||||
if w is None:
|
||||
w = "jpeg"
|
||||
return w
|
||||
|
||||
|
||||
def decode_base64_to_image(
|
||||
encoding: str, gray=False
|
||||
) -> Tuple[np.array, Optional[np.array], Dict]:
|
||||
if encoding.startswith("data:image/"):
|
||||
encoding = encoding.split(";")[1].split(",")[1]
|
||||
image = Image.open(io.BytesIO(base64.b64decode(encoding)))
|
||||
|
||||
alpha_channel = None
|
||||
try:
|
||||
image = ImageOps.exif_transpose(image)
|
||||
except:
|
||||
pass
|
||||
# exif_transpose will remove exif rotate info,we must call image.info after exif_transpose
|
||||
infos = image.info
|
||||
|
||||
if gray:
|
||||
image = image.convert("L")
|
||||
np_img = np.array(image)
|
||||
else:
|
||||
if image.mode == "RGBA":
|
||||
np_img = np.array(image)
|
||||
alpha_channel = np_img[:, :, -1]
|
||||
np_img = cv2.cvtColor(np_img, cv2.COLOR_RGBA2RGB)
|
||||
else:
|
||||
image = image.convert("RGB")
|
||||
np_img = np.array(image)
|
||||
|
||||
return np_img, alpha_channel, infos
|
||||
|
||||
|
||||
def encode_pil_to_base64(image: Image, quality: int, infos: Dict) -> bytes:
|
||||
img_bytes = pil_to_bytes(
|
||||
image,
|
||||
"png",
|
||||
quality=quality,
|
||||
infos=infos,
|
||||
)
|
||||
return base64.b64encode(img_bytes)
|
||||
|
||||
|
||||
def concat_alpha_channel(rgb_np_img, alpha_channel) -> np.ndarray:
|
||||
if alpha_channel is not None:
|
||||
if alpha_channel.shape[:2] != rgb_np_img.shape[:2]:
|
||||
alpha_channel = cv2.resize(
|
||||
alpha_channel, dsize=(rgb_np_img.shape[1], rgb_np_img.shape[0])
|
||||
)
|
||||
rgb_np_img = np.concatenate(
|
||||
(rgb_np_img, alpha_channel[:, :, np.newaxis]), axis=-1
|
||||
)
|
||||
return rgb_np_img
|
||||
|
||||
|
||||
def adjust_mask(mask: np.ndarray, kernel_size: int, operate):
|
||||
# kernel_size = kernel_size*2+1
|
||||
mask[mask >= 127] = 255
|
||||
mask[mask < 127] = 0
|
||||
# fronted brush color "ffcc00bb"
|
||||
kernel = cv2.getStructuringElement(
|
||||
cv2.MORPH_ELLIPSE, (2 * kernel_size + 1, 2 * kernel_size + 1)
|
||||
)
|
||||
if operate == "expand":
|
||||
mask = cv2.dilate(
|
||||
mask,
|
||||
kernel,
|
||||
iterations=1,
|
||||
)
|
||||
else:
|
||||
mask = cv2.erode(
|
||||
mask,
|
||||
kernel,
|
||||
iterations=1,
|
||||
)
|
||||
res_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
|
||||
res_mask[mask > 128] = [255, 203, 0, int(255 * 0.73)]
|
||||
res_mask = cv2.cvtColor(res_mask, cv2.COLOR_BGRA2RGBA)
|
||||
return res_mask
|
||||
|
||||
|
||||
def gen_frontend_mask(bgr_or_gray_mask):
|
||||
if len(bgr_or_gray_mask.shape) == 3 and bgr_or_gray_mask.shape[2] != 1:
|
||||
bgr_or_gray_mask = cv2.cvtColor(bgr_or_gray_mask, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# fronted brush color "ffcc00bb"
|
||||
# TODO: how to set kernel size?
|
||||
kernel_size = 9
|
||||
bgr_or_gray_mask = cv2.dilate(
|
||||
bgr_or_gray_mask,
|
||||
np.ones((kernel_size, kernel_size), np.uint8),
|
||||
iterations=1,
|
||||
)
|
||||
res_mask = np.zeros(
|
||||
(bgr_or_gray_mask.shape[0], bgr_or_gray_mask.shape[1], 4), dtype=np.uint8
|
||||
)
|
||||
res_mask[bgr_or_gray_mask > 128] = [255, 203, 0, int(255 * 0.73)]
|
||||
res_mask = cv2.cvtColor(res_mask, cv2.COLOR_BGRA2RGBA)
|
||||
return res_mask
|
||||
12
iopaint/installer.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def install(package):
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
||||
|
||||
|
||||
def install_plugins_package():
|
||||
install("rembg")
|
||||
install("realesrgan")
|
||||
install("gfpgan")
|
||||
35
iopaint/model/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from .controlnet import ControlNet
|
||||
from .fcf import FcF
|
||||
from .instruct_pix2pix import InstructPix2Pix
|
||||
from .kandinsky import Kandinsky22
|
||||
from .lama import LaMa
|
||||
from .ldm import LDM
|
||||
from .manga import Manga
|
||||
from .mat import MAT
|
||||
from .mi_gan import MIGAN
|
||||
from .opencv2 import OpenCV2
|
||||
from .paint_by_example import PaintByExample
|
||||
from .power_paint.power_paint import PowerPaint
|
||||
from .sd import SD15, SD2, Anything4, RealisticVision14, SD
|
||||
from .sdxl import SDXL
|
||||
from .zits import ZITS
|
||||
|
||||
models = {
|
||||
LaMa.name: LaMa,
|
||||
LDM.name: LDM,
|
||||
ZITS.name: ZITS,
|
||||
MAT.name: MAT,
|
||||
FcF.name: FcF,
|
||||
OpenCV2.name: OpenCV2,
|
||||
Manga.name: Manga,
|
||||
MIGAN.name: MIGAN,
|
||||
SD15.name: SD15,
|
||||
Anything4.name: Anything4,
|
||||
RealisticVision14.name: RealisticVision14,
|
||||
SD2.name: SD2,
|
||||
PaintByExample.name: PaintByExample,
|
||||
InstructPix2Pix.name: InstructPix2Pix,
|
||||
Kandinsky22.name: Kandinsky22,
|
||||
SDXL.name: SDXL,
|
||||
PowerPaint.name: PowerPaint,
|
||||
}
|
||||
422
iopaint/model/base.py
Normal file
@@ -0,0 +1,422 @@
|
||||
import abc
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import (
|
||||
boxes_from_mask,
|
||||
resize_max_size,
|
||||
pad_img_to_modulo,
|
||||
switch_mps_device,
|
||||
)
|
||||
from iopaint.model.helper.g_diffuser_bot import expand_image
|
||||
from iopaint.model.utils import get_scheduler
|
||||
from iopaint.schema import InpaintRequest, HDStrategy, SDSampler
|
||||
|
||||
|
||||
class InpaintModel:
|
||||
name = "base"
|
||||
min_size: Optional[int] = None
|
||||
pad_mod = 8
|
||||
pad_to_square = False
|
||||
is_erase_model = False
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
"""
|
||||
|
||||
Args:
|
||||
device:
|
||||
"""
|
||||
device = switch_mps_device(self.name, device)
|
||||
self.device = device
|
||||
self.init_model(device, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def init_model(self, device, **kwargs):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def is_downloaded() -> bool:
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input images and output images have same size
|
||||
images: [H, W, C] RGB
|
||||
masks: [H, W, 1] 255 为 masks 区域
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
...
|
||||
|
||||
def _pad_forward(self, image, mask, config: InpaintRequest):
|
||||
origin_height, origin_width = image.shape[:2]
|
||||
pad_image = pad_img_to_modulo(
|
||||
image, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size
|
||||
)
|
||||
pad_mask = pad_img_to_modulo(
|
||||
mask, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size
|
||||
)
|
||||
|
||||
# logger.info(f"final forward pad size: {pad_image.shape}")
|
||||
|
||||
image, mask = self.forward_pre_process(image, mask, config)
|
||||
|
||||
result = self.forward(pad_image, pad_mask, config)
|
||||
result = result[0:origin_height, 0:origin_width, :]
|
||||
|
||||
result, image, mask = self.forward_post_process(result, image, mask, config)
|
||||
|
||||
if config.sd_keep_unmasked_area:
|
||||
mask = mask[:, :, np.newaxis]
|
||||
result = result * (mask / 255) + image[:, :, ::-1] * (1 - (mask / 255))
|
||||
return result
|
||||
|
||||
def forward_pre_process(self, image, mask, config):
|
||||
return image, mask
|
||||
|
||||
def forward_post_process(self, result, image, mask, config):
|
||||
return result, image, mask
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
images: [H, W, C] RGB, not normalized
|
||||
masks: [H, W]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
inpaint_result = None
|
||||
# logger.info(f"hd_strategy: {config.hd_strategy}")
|
||||
if config.hd_strategy == HDStrategy.CROP:
|
||||
if max(image.shape) > config.hd_strategy_crop_trigger_size:
|
||||
logger.info(f"Run crop strategy")
|
||||
boxes = boxes_from_mask(mask)
|
||||
crop_result = []
|
||||
for box in boxes:
|
||||
crop_image, crop_box = self._run_box(image, mask, box, config)
|
||||
crop_result.append((crop_image, crop_box))
|
||||
|
||||
inpaint_result = image[:, :, ::-1]
|
||||
for crop_image, crop_box in crop_result:
|
||||
x1, y1, x2, y2 = crop_box
|
||||
inpaint_result[y1:y2, x1:x2, :] = crop_image
|
||||
|
||||
elif config.hd_strategy == HDStrategy.RESIZE:
|
||||
if max(image.shape) > config.hd_strategy_resize_limit:
|
||||
origin_size = image.shape[:2]
|
||||
downsize_image = resize_max_size(
|
||||
image, size_limit=config.hd_strategy_resize_limit
|
||||
)
|
||||
downsize_mask = resize_max_size(
|
||||
mask, size_limit=config.hd_strategy_resize_limit
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Run resize strategy, origin size: {image.shape} forward size: {downsize_image.shape}"
|
||||
)
|
||||
inpaint_result = self._pad_forward(
|
||||
downsize_image, downsize_mask, config
|
||||
)
|
||||
|
||||
# only paste masked area result
|
||||
inpaint_result = cv2.resize(
|
||||
inpaint_result,
|
||||
(origin_size[1], origin_size[0]),
|
||||
interpolation=cv2.INTER_CUBIC,
|
||||
)
|
||||
original_pixel_indices = mask < 127
|
||||
inpaint_result[original_pixel_indices] = image[:, :, ::-1][
|
||||
original_pixel_indices
|
||||
]
|
||||
|
||||
if inpaint_result is None:
|
||||
inpaint_result = self._pad_forward(image, mask, config)
|
||||
|
||||
return inpaint_result
|
||||
|
||||
def _crop_box(self, image, mask, box, config: InpaintRequest):
|
||||
"""
|
||||
|
||||
Args:
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1]
|
||||
box: [left,top,right,bottom]
|
||||
|
||||
Returns:
|
||||
BGR IMAGE, (l, r, r, b)
|
||||
"""
|
||||
box_h = box[3] - box[1]
|
||||
box_w = box[2] - box[0]
|
||||
cx = (box[0] + box[2]) // 2
|
||||
cy = (box[1] + box[3]) // 2
|
||||
img_h, img_w = image.shape[:2]
|
||||
|
||||
w = box_w + config.hd_strategy_crop_margin * 2
|
||||
h = box_h + config.hd_strategy_crop_margin * 2
|
||||
|
||||
_l = cx - w // 2
|
||||
_r = cx + w // 2
|
||||
_t = cy - h // 2
|
||||
_b = cy + h // 2
|
||||
|
||||
l = max(_l, 0)
|
||||
r = min(_r, img_w)
|
||||
t = max(_t, 0)
|
||||
b = min(_b, img_h)
|
||||
|
||||
# try to get more context when crop around image edge
|
||||
if _l < 0:
|
||||
r += abs(_l)
|
||||
if _r > img_w:
|
||||
l -= _r - img_w
|
||||
if _t < 0:
|
||||
b += abs(_t)
|
||||
if _b > img_h:
|
||||
t -= _b - img_h
|
||||
|
||||
l = max(l, 0)
|
||||
r = min(r, img_w)
|
||||
t = max(t, 0)
|
||||
b = min(b, img_h)
|
||||
|
||||
crop_img = image[t:b, l:r, :]
|
||||
crop_mask = mask[t:b, l:r]
|
||||
|
||||
# logger.info(f"box size: ({box_h},{box_w}) crop size: {crop_img.shape}")
|
||||
|
||||
return crop_img, crop_mask, [l, t, r, b]
|
||||
|
||||
def _calculate_cdf(self, histogram):
|
||||
cdf = histogram.cumsum()
|
||||
normalized_cdf = cdf / float(cdf.max())
|
||||
return normalized_cdf
|
||||
|
||||
def _calculate_lookup(self, source_cdf, reference_cdf):
|
||||
lookup_table = np.zeros(256)
|
||||
lookup_val = 0
|
||||
for source_index, source_val in enumerate(source_cdf):
|
||||
for reference_index, reference_val in enumerate(reference_cdf):
|
||||
if reference_val >= source_val:
|
||||
lookup_val = reference_index
|
||||
break
|
||||
lookup_table[source_index] = lookup_val
|
||||
return lookup_table
|
||||
|
||||
def _match_histograms(self, source, reference, mask):
|
||||
transformed_channels = []
|
||||
for channel in range(source.shape[-1]):
|
||||
source_channel = source[:, :, channel]
|
||||
reference_channel = reference[:, :, channel]
|
||||
|
||||
# only calculate histograms for non-masked parts
|
||||
source_histogram, _ = np.histogram(source_channel[mask == 0], 256, [0, 256])
|
||||
reference_histogram, _ = np.histogram(
|
||||
reference_channel[mask == 0], 256, [0, 256]
|
||||
)
|
||||
|
||||
source_cdf = self._calculate_cdf(source_histogram)
|
||||
reference_cdf = self._calculate_cdf(reference_histogram)
|
||||
|
||||
lookup = self._calculate_lookup(source_cdf, reference_cdf)
|
||||
|
||||
transformed_channels.append(cv2.LUT(source_channel, lookup))
|
||||
|
||||
result = cv2.merge(transformed_channels)
|
||||
result = cv2.convertScaleAbs(result)
|
||||
|
||||
return result
|
||||
|
||||
def _apply_cropper(self, image, mask, config: InpaintRequest):
|
||||
img_h, img_w = image.shape[:2]
|
||||
l, t, w, h = (
|
||||
config.croper_x,
|
||||
config.croper_y,
|
||||
config.croper_width,
|
||||
config.croper_height,
|
||||
)
|
||||
r = l + w
|
||||
b = t + h
|
||||
|
||||
l = max(l, 0)
|
||||
r = min(r, img_w)
|
||||
t = max(t, 0)
|
||||
b = min(b, img_h)
|
||||
|
||||
crop_img = image[t:b, l:r, :]
|
||||
crop_mask = mask[t:b, l:r]
|
||||
return crop_img, crop_mask, (l, t, r, b)
|
||||
|
||||
def _run_box(self, image, mask, box, config: InpaintRequest):
|
||||
"""
|
||||
|
||||
Args:
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1]
|
||||
box: [left,top,right,bottom]
|
||||
|
||||
Returns:
|
||||
BGR IMAGE
|
||||
"""
|
||||
crop_img, crop_mask, [l, t, r, b] = self._crop_box(image, mask, box, config)
|
||||
|
||||
return self._pad_forward(crop_img, crop_mask, config), [l, t, r, b]
|
||||
|
||||
|
||||
class DiffusionInpaintModel(InpaintModel):
|
||||
def __init__(self, device, **kwargs):
|
||||
self.model_info = kwargs["model_info"]
|
||||
self.model_id_or_path = self.model_info.path
|
||||
super().__init__(device, **kwargs)
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
images: [H, W, C] RGB, not normalized
|
||||
masks: [H, W]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
# boxes = boxes_from_mask(mask)
|
||||
if config.use_croper:
|
||||
crop_img, crop_mask, (l, t, r, b) = self._apply_cropper(image, mask, config)
|
||||
crop_image = self._scaled_pad_forward(crop_img, crop_mask, config)
|
||||
inpaint_result = image[:, :, ::-1]
|
||||
inpaint_result[t:b, l:r, :] = crop_image
|
||||
elif config.use_extender:
|
||||
inpaint_result = self._do_outpainting(image, config)
|
||||
else:
|
||||
inpaint_result = self._scaled_pad_forward(image, mask, config)
|
||||
|
||||
return inpaint_result
|
||||
|
||||
def _do_outpainting(self, image, config: InpaintRequest):
|
||||
# cropper 和 image 在同一个坐标系下,croper_x/y 可能为负数
|
||||
# 从 image 中 crop 出 outpainting 区域
|
||||
image_h, image_w = image.shape[:2]
|
||||
cropper_l = config.extender_x
|
||||
cropper_t = config.extender_y
|
||||
cropper_r = config.extender_x + config.extender_width
|
||||
cropper_b = config.extender_y + config.extender_height
|
||||
image_l = 0
|
||||
image_t = 0
|
||||
image_r = image_w
|
||||
image_b = image_h
|
||||
|
||||
# 类似求 IOU
|
||||
l = max(cropper_l, image_l)
|
||||
t = max(cropper_t, image_t)
|
||||
r = min(cropper_r, image_r)
|
||||
b = min(cropper_b, image_b)
|
||||
|
||||
assert (
|
||||
0 <= l < r and 0 <= t < b
|
||||
), f"cropper and image not overlap, {l},{t},{r},{b}"
|
||||
|
||||
cropped_image = image[t:b, l:r, :]
|
||||
padding_l = max(0, image_l - cropper_l)
|
||||
padding_t = max(0, image_t - cropper_t)
|
||||
padding_r = max(0, cropper_r - image_r)
|
||||
padding_b = max(0, cropper_b - image_b)
|
||||
|
||||
zero_padding_count = [padding_l, padding_t, padding_r, padding_b].count(0)
|
||||
|
||||
if zero_padding_count not in [0, 3]:
|
||||
logger.warning(
|
||||
f"padding count({zero_padding_count}) not 0 or 3, may result in bad edge outpainting"
|
||||
)
|
||||
|
||||
expanded_image, mask_image = expand_image(
|
||||
cropped_image,
|
||||
left=padding_l,
|
||||
top=padding_t,
|
||||
right=padding_r,
|
||||
bottom=padding_b,
|
||||
softness=config.sd_outpainting_softness,
|
||||
space=config.sd_outpainting_space,
|
||||
)
|
||||
|
||||
# 最终扩大了的 image, BGR
|
||||
expanded_cropped_result_image = self._scaled_pad_forward(
|
||||
expanded_image, mask_image, config
|
||||
)
|
||||
|
||||
# RGB -> BGR
|
||||
outpainting_image = cv2.copyMakeBorder(
|
||||
image,
|
||||
left=padding_l,
|
||||
top=padding_t,
|
||||
right=padding_r,
|
||||
bottom=padding_b,
|
||||
borderType=cv2.BORDER_CONSTANT,
|
||||
value=0,
|
||||
)[:, :, ::-1]
|
||||
|
||||
# 把 cropped_result_image 贴到 outpainting_image 上,这一步不需要 blend
|
||||
paste_t = 0 if config.extender_y < 0 else config.extender_y
|
||||
paste_l = 0 if config.extender_x < 0 else config.extender_x
|
||||
|
||||
outpainting_image[
|
||||
paste_t : paste_t + expanded_cropped_result_image.shape[0],
|
||||
paste_l : paste_l + expanded_cropped_result_image.shape[1],
|
||||
:,
|
||||
] = expanded_cropped_result_image
|
||||
return outpainting_image
|
||||
|
||||
def _scaled_pad_forward(self, image, mask, config: InpaintRequest):
|
||||
longer_side_length = int(config.sd_scale * max(image.shape[:2]))
|
||||
origin_size = image.shape[:2]
|
||||
downsize_image = resize_max_size(image, size_limit=longer_side_length)
|
||||
downsize_mask = resize_max_size(mask, size_limit=longer_side_length)
|
||||
if config.sd_scale != 1:
|
||||
logger.info(
|
||||
f"Resize image to do sd inpainting: {image.shape} -> {downsize_image.shape}"
|
||||
)
|
||||
inpaint_result = self._pad_forward(downsize_image, downsize_mask, config)
|
||||
# only paste masked area result
|
||||
inpaint_result = cv2.resize(
|
||||
inpaint_result,
|
||||
(origin_size[1], origin_size[0]),
|
||||
interpolation=cv2.INTER_CUBIC,
|
||||
)
|
||||
|
||||
# blend result, copy from g_diffuser_bot
|
||||
# mask_rgb = 1.0 - np_img_grey_to_rgb(mask / 255.0)
|
||||
# inpaint_result = np.clip(
|
||||
# inpaint_result * (1.0 - mask_rgb) + image * mask_rgb, 0.0, 255.0
|
||||
# )
|
||||
# original_pixel_indices = mask < 127
|
||||
# inpaint_result[original_pixel_indices] = image[:, :, ::-1][
|
||||
# original_pixel_indices
|
||||
# ]
|
||||
return inpaint_result
|
||||
|
||||
def set_scheduler(self, config: InpaintRequest):
|
||||
scheduler_config = self.model.scheduler.config
|
||||
sd_sampler = config.sd_sampler
|
||||
if config.sd_lcm_lora:
|
||||
sd_sampler = SDSampler.lcm
|
||||
logger.info(f"LCM Lora enabled, use {sd_sampler} sampler")
|
||||
scheduler = get_scheduler(sd_sampler, scheduler_config)
|
||||
self.model.scheduler = scheduler
|
||||
|
||||
def forward_pre_process(self, image, mask, config):
|
||||
if config.sd_mask_blur != 0:
|
||||
k = 2 * config.sd_mask_blur + 1
|
||||
mask = cv2.GaussianBlur(mask, (k, k), 0)[:, :, np.newaxis]
|
||||
|
||||
return image, mask
|
||||
|
||||
def forward_post_process(self, result, image, mask, config):
|
||||
if config.sd_match_histograms:
|
||||
result = self._match_histograms(result, image[:, :, ::-1], mask)
|
||||
|
||||
if config.sd_mask_blur != 0:
|
||||
k = 2 * config.sd_mask_blur + 1
|
||||
mask = cv2.GaussianBlur(mask, (k, k), 0)
|
||||
return result, image, mask
|
||||
166
iopaint/model/controlnet.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from diffusers import ControlNetModel, DiffusionPipeline
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.model.helper.controlnet_preprocess import (
|
||||
make_canny_control_image,
|
||||
make_openpose_control_image,
|
||||
make_depth_control_image,
|
||||
make_inpaint_control_image,
|
||||
)
|
||||
from iopaint.model.helper.cpu_text_encoder import CPUTextEncoderWrapper
|
||||
from iopaint.model.utils import get_scheduler, handle_from_pretrained_exceptions
|
||||
from iopaint.schema import InpaintRequest, ModelType
|
||||
|
||||
|
||||
class ControlNet(DiffusionInpaintModel):
|
||||
name = "controlnet"
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
|
||||
@property
|
||||
def lcm_lora_id(self):
|
||||
if self.model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
]:
|
||||
return "latent-consistency/lcm-lora-sdv1-5"
|
||||
if self.model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
]:
|
||||
return "latent-consistency/lcm-lora-sdxl"
|
||||
raise NotImplementedError(f"Unsupported controlnet lcm model {self.model_info}")
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
model_info = kwargs["model_info"]
|
||||
controlnet_method = kwargs["controlnet_method"]
|
||||
|
||||
self.model_info = model_info
|
||||
self.controlnet_method = controlnet_method
|
||||
|
||||
model_kwargs = {}
|
||||
if kwargs["disable_nsfw"] or kwargs.get("cpu_offload", False):
|
||||
logger.info("Disable Stable Diffusion Model NSFW checker")
|
||||
model_kwargs.update(
|
||||
dict(
|
||||
safety_checker=None,
|
||||
feature_extractor=None,
|
||||
requires_safety_checker=False,
|
||||
)
|
||||
)
|
||||
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
self.torch_dtype = torch_dtype
|
||||
|
||||
if model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
]:
|
||||
from diffusers import (
|
||||
StableDiffusionControlNetInpaintPipeline as PipeClass,
|
||||
)
|
||||
elif model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
]:
|
||||
from diffusers import (
|
||||
StableDiffusionXLControlNetInpaintPipeline as PipeClass,
|
||||
)
|
||||
|
||||
controlnet = ControlNetModel.from_pretrained(
|
||||
pretrained_model_name_or_path=controlnet_method,
|
||||
resume_download=True,
|
||||
)
|
||||
if model_info.is_single_file_diffusers:
|
||||
if self.model_info.model_type == ModelType.DIFFUSERS_SD:
|
||||
model_kwargs["num_in_channels"] = 4
|
||||
else:
|
||||
model_kwargs["num_in_channels"] = 9
|
||||
|
||||
self.model = PipeClass.from_single_file(
|
||||
model_info.path, controlnet=controlnet, **model_kwargs
|
||||
).to(torch_dtype)
|
||||
else:
|
||||
self.model = handle_from_pretrained_exceptions(
|
||||
PipeClass.from_pretrained,
|
||||
pretrained_model_name_or_path=model_info.path,
|
||||
controlnet=controlnet,
|
||||
variant="fp16",
|
||||
dtype=torch_dtype,
|
||||
**model_kwargs,
|
||||
)
|
||||
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
logger.info("Enable sequential cpu offload")
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
if kwargs["sd_cpu_textencoder"]:
|
||||
logger.info("Run Stable Diffusion TextEncoder on CPU")
|
||||
self.model.text_encoder = CPUTextEncoderWrapper(
|
||||
self.model.text_encoder, torch_dtype
|
||||
)
|
||||
|
||||
self.callback = kwargs.pop("callback", None)
|
||||
|
||||
def switch_controlnet_method(self, new_method: str):
|
||||
self.controlnet_method = new_method
|
||||
controlnet = ControlNetModel.from_pretrained(
|
||||
new_method, torch_dtype=self.torch_dtype, resume_download=True
|
||||
).to(self.model.device)
|
||||
self.model.controlnet = controlnet
|
||||
|
||||
def _get_control_image(self, image, mask):
|
||||
if "canny" in self.controlnet_method:
|
||||
control_image = make_canny_control_image(image)
|
||||
elif "openpose" in self.controlnet_method:
|
||||
control_image = make_openpose_control_image(image)
|
||||
elif "depth" in self.controlnet_method:
|
||||
control_image = make_depth_control_image(image)
|
||||
elif "inpaint" in self.controlnet_method:
|
||||
control_image = make_inpaint_control_image(image, mask)
|
||||
else:
|
||||
raise NotImplementedError(f"{self.controlnet_method} not implemented")
|
||||
return control_image
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
scheduler_config = self.model.scheduler.config
|
||||
scheduler = get_scheduler(config.sd_sampler, scheduler_config)
|
||||
self.model.scheduler = scheduler
|
||||
|
||||
img_h, img_w = image.shape[:2]
|
||||
control_image = self._get_control_image(image, mask)
|
||||
mask_image = PIL.Image.fromarray(mask[:, :, -1], mode="L")
|
||||
image = PIL.Image.fromarray(image)
|
||||
|
||||
output = self.model(
|
||||
image=image,
|
||||
mask_image=mask_image,
|
||||
control_image=control_image,
|
||||
prompt=config.prompt,
|
||||
negative_prompt=config.negative_prompt,
|
||||
num_inference_steps=config.sd_steps,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
callback_on_step_end=self.callback,
|
||||
height=img_h,
|
||||
width=img_w,
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
controlnet_conditioning_scale=config.controlnet_conditioning_scale,
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
193
iopaint/model/ddim_sampler.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from iopaint.model.utils import make_ddim_timesteps, make_ddim_sampling_parameters, noise_like
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class DDIMSampler(object):
|
||||
def __init__(self, model, schedule="linear"):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.ddpm_num_timesteps = model.num_timesteps
|
||||
self.schedule = schedule
|
||||
|
||||
def register_buffer(self, name, attr):
|
||||
setattr(self, name, attr)
|
||||
|
||||
def make_schedule(
|
||||
self, ddim_num_steps, ddim_discretize="uniform", ddim_eta=0.0, verbose=True
|
||||
):
|
||||
self.ddim_timesteps = make_ddim_timesteps(
|
||||
ddim_discr_method=ddim_discretize,
|
||||
num_ddim_timesteps=ddim_num_steps,
|
||||
# array([1])
|
||||
num_ddpm_timesteps=self.ddpm_num_timesteps,
|
||||
verbose=verbose,
|
||||
)
|
||||
alphas_cumprod = self.model.alphas_cumprod # torch.Size([1000])
|
||||
assert (
|
||||
alphas_cumprod.shape[0] == self.ddpm_num_timesteps
|
||||
), "alphas have to be defined for each timestep"
|
||||
to_torch = lambda x: x.clone().detach().to(torch.float32).to(self.model.device)
|
||||
|
||||
self.register_buffer("betas", to_torch(self.model.betas))
|
||||
self.register_buffer("alphas_cumprod", to_torch(alphas_cumprod))
|
||||
self.register_buffer(
|
||||
"alphas_cumprod_prev", to_torch(self.model.alphas_cumprod_prev)
|
||||
)
|
||||
|
||||
# calculations for diffusion q(x_t | x_{t-1}) and others
|
||||
self.register_buffer(
|
||||
"sqrt_alphas_cumprod", to_torch(np.sqrt(alphas_cumprod.cpu()))
|
||||
)
|
||||
self.register_buffer(
|
||||
"sqrt_one_minus_alphas_cumprod",
|
||||
to_torch(np.sqrt(1.0 - alphas_cumprod.cpu())),
|
||||
)
|
||||
self.register_buffer(
|
||||
"log_one_minus_alphas_cumprod", to_torch(np.log(1.0 - alphas_cumprod.cpu()))
|
||||
)
|
||||
self.register_buffer(
|
||||
"sqrt_recip_alphas_cumprod", to_torch(np.sqrt(1.0 / alphas_cumprod.cpu()))
|
||||
)
|
||||
self.register_buffer(
|
||||
"sqrt_recipm1_alphas_cumprod",
|
||||
to_torch(np.sqrt(1.0 / alphas_cumprod.cpu() - 1)),
|
||||
)
|
||||
|
||||
# ddim sampling parameters
|
||||
ddim_sigmas, ddim_alphas, ddim_alphas_prev = make_ddim_sampling_parameters(
|
||||
alphacums=alphas_cumprod.cpu(),
|
||||
ddim_timesteps=self.ddim_timesteps,
|
||||
eta=ddim_eta,
|
||||
verbose=verbose,
|
||||
)
|
||||
self.register_buffer("ddim_sigmas", ddim_sigmas)
|
||||
self.register_buffer("ddim_alphas", ddim_alphas)
|
||||
self.register_buffer("ddim_alphas_prev", ddim_alphas_prev)
|
||||
self.register_buffer("ddim_sqrt_one_minus_alphas", np.sqrt(1.0 - ddim_alphas))
|
||||
sigmas_for_original_sampling_steps = ddim_eta * torch.sqrt(
|
||||
(1 - self.alphas_cumprod_prev)
|
||||
/ (1 - self.alphas_cumprod)
|
||||
* (1 - self.alphas_cumprod / self.alphas_cumprod_prev)
|
||||
)
|
||||
self.register_buffer(
|
||||
"ddim_sigmas_for_original_num_steps", sigmas_for_original_sampling_steps
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def sample(self, steps, conditioning, batch_size, shape):
|
||||
self.make_schedule(ddim_num_steps=steps, ddim_eta=0, verbose=False)
|
||||
# sampling
|
||||
C, H, W = shape
|
||||
size = (batch_size, C, H, W)
|
||||
|
||||
# samples: 1,3,128,128
|
||||
return self.ddim_sampling(
|
||||
conditioning,
|
||||
size,
|
||||
quantize_denoised=False,
|
||||
ddim_use_original_steps=False,
|
||||
noise_dropout=0,
|
||||
temperature=1.0,
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def ddim_sampling(
|
||||
self,
|
||||
cond,
|
||||
shape,
|
||||
ddim_use_original_steps=False,
|
||||
quantize_denoised=False,
|
||||
temperature=1.0,
|
||||
noise_dropout=0.0,
|
||||
):
|
||||
device = self.model.betas.device
|
||||
b = shape[0]
|
||||
img = torch.randn(shape, device=device, dtype=cond.dtype)
|
||||
timesteps = (
|
||||
self.ddpm_num_timesteps if ddim_use_original_steps else self.ddim_timesteps
|
||||
)
|
||||
|
||||
time_range = (
|
||||
reversed(range(0, timesteps))
|
||||
if ddim_use_original_steps
|
||||
else np.flip(timesteps)
|
||||
)
|
||||
total_steps = timesteps if ddim_use_original_steps else timesteps.shape[0]
|
||||
logger.info(f"Running DDIM Sampling with {total_steps} timesteps")
|
||||
|
||||
iterator = tqdm(time_range, desc="DDIM Sampler", total=total_steps)
|
||||
|
||||
for i, step in enumerate(iterator):
|
||||
index = total_steps - i - 1
|
||||
ts = torch.full((b,), step, device=device, dtype=torch.long)
|
||||
|
||||
outs = self.p_sample_ddim(
|
||||
img,
|
||||
cond,
|
||||
ts,
|
||||
index=index,
|
||||
use_original_steps=ddim_use_original_steps,
|
||||
quantize_denoised=quantize_denoised,
|
||||
temperature=temperature,
|
||||
noise_dropout=noise_dropout,
|
||||
)
|
||||
img, _ = outs
|
||||
|
||||
return img
|
||||
|
||||
@torch.no_grad()
|
||||
def p_sample_ddim(
|
||||
self,
|
||||
x,
|
||||
c,
|
||||
t,
|
||||
index,
|
||||
repeat_noise=False,
|
||||
use_original_steps=False,
|
||||
quantize_denoised=False,
|
||||
temperature=1.0,
|
||||
noise_dropout=0.0,
|
||||
):
|
||||
b, *_, device = *x.shape, x.device
|
||||
e_t = self.model.apply_model(x, t, c)
|
||||
|
||||
alphas = self.model.alphas_cumprod if use_original_steps else self.ddim_alphas
|
||||
alphas_prev = (
|
||||
self.model.alphas_cumprod_prev
|
||||
if use_original_steps
|
||||
else self.ddim_alphas_prev
|
||||
)
|
||||
sqrt_one_minus_alphas = (
|
||||
self.model.sqrt_one_minus_alphas_cumprod
|
||||
if use_original_steps
|
||||
else self.ddim_sqrt_one_minus_alphas
|
||||
)
|
||||
sigmas = (
|
||||
self.model.ddim_sigmas_for_original_num_steps
|
||||
if use_original_steps
|
||||
else self.ddim_sigmas
|
||||
)
|
||||
# select parameters corresponding to the currently considered timestep
|
||||
a_t = torch.full((b, 1, 1, 1), alphas[index], device=device)
|
||||
a_prev = torch.full((b, 1, 1, 1), alphas_prev[index], device=device)
|
||||
sigma_t = torch.full((b, 1, 1, 1), sigmas[index], device=device)
|
||||
sqrt_one_minus_at = torch.full(
|
||||
(b, 1, 1, 1), sqrt_one_minus_alphas[index], device=device
|
||||
)
|
||||
|
||||
# current prediction for x_0
|
||||
pred_x0 = (x - sqrt_one_minus_at * e_t) / a_t.sqrt()
|
||||
if quantize_denoised: # 没用
|
||||
pred_x0, _, *_ = self.model.first_stage_model.quantize(pred_x0)
|
||||
# direction pointing to x_t
|
||||
dir_xt = (1.0 - a_prev - sigma_t ** 2).sqrt() * e_t
|
||||
noise = sigma_t * noise_like(x.shape, device, repeat_noise) * temperature
|
||||
if noise_dropout > 0.0: # 没用
|
||||
noise = torch.nn.functional.dropout(noise, p=noise_dropout)
|
||||
x_prev = a_prev.sqrt() * pred_x0 + dir_xt + noise
|
||||
return x_prev, pred_x0
|
||||
1737
iopaint/model/fcf.py
Normal file
46
iopaint/model/helper/controlnet_preprocess.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import torch
|
||||
import PIL
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
|
||||
def make_canny_control_image(image: np.ndarray) -> Image:
|
||||
canny_image = cv2.Canny(image, 100, 200)
|
||||
canny_image = canny_image[:, :, None]
|
||||
canny_image = np.concatenate([canny_image, canny_image, canny_image], axis=2)
|
||||
canny_image = PIL.Image.fromarray(canny_image)
|
||||
control_image = canny_image
|
||||
return control_image
|
||||
|
||||
|
||||
def make_openpose_control_image(image: np.ndarray) -> Image:
|
||||
from controlnet_aux import OpenposeDetector
|
||||
|
||||
processor = OpenposeDetector.from_pretrained("lllyasviel/ControlNet")
|
||||
control_image = processor(image, hand_and_face=True)
|
||||
return control_image
|
||||
|
||||
|
||||
def make_depth_control_image(image: np.ndarray) -> Image:
|
||||
from transformers import pipeline
|
||||
|
||||
depth_estimator = pipeline("depth-estimation")
|
||||
depth_image = depth_estimator(PIL.Image.fromarray(image))["depth"]
|
||||
depth_image = np.array(depth_image)
|
||||
depth_image = depth_image[:, :, None]
|
||||
depth_image = np.concatenate([depth_image, depth_image, depth_image], axis=2)
|
||||
control_image = PIL.Image.fromarray(depth_image)
|
||||
return control_image
|
||||
|
||||
|
||||
def make_inpaint_control_image(image: np.ndarray, mask: np.ndarray) -> torch.Tensor:
|
||||
"""
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
"""
|
||||
image = image.astype(np.float32) / 255.0
|
||||
image[mask[:, :, -1] > 128] = -1.0 # set as masked pixel
|
||||
image = np.expand_dims(image, 0).transpose(0, 3, 1, 2)
|
||||
image = torch.from_numpy(image)
|
||||
return image
|
||||
25
iopaint/model/helper/cpu_text_encoder.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import torch
|
||||
from iopaint.model.utils import torch_gc
|
||||
|
||||
|
||||
class CPUTextEncoderWrapper(torch.nn.Module):
|
||||
def __init__(self, text_encoder, torch_dtype):
|
||||
super().__init__()
|
||||
self.config = text_encoder.config
|
||||
self.text_encoder = text_encoder.to(torch.device("cpu"), non_blocking=True)
|
||||
self.text_encoder = self.text_encoder.to(torch.float32, non_blocking=True)
|
||||
self.torch_dtype = torch_dtype
|
||||
del text_encoder
|
||||
torch_gc()
|
||||
|
||||
def __call__(self, x, **kwargs):
|
||||
input_device = x.device
|
||||
return [
|
||||
self.text_encoder(x.to(self.text_encoder.device), **kwargs)[0]
|
||||
.to(input_device)
|
||||
.to(self.torch_dtype)
|
||||
]
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self.torch_dtype
|
||||
167
iopaint/model/helper/g_diffuser_bot.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# code copy from: https://github.com/parlance-zz/g-diffuser-bot
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def np_img_grey_to_rgb(data):
|
||||
if data.ndim == 3:
|
||||
return data
|
||||
return np.expand_dims(data, 2) * np.ones((1, 1, 3))
|
||||
|
||||
|
||||
def convolve(data1, data2): # fast convolution with fft
|
||||
if data1.ndim != data2.ndim: # promote to rgb if mismatch
|
||||
if data1.ndim < 3:
|
||||
data1 = np_img_grey_to_rgb(data1)
|
||||
if data2.ndim < 3:
|
||||
data2 = np_img_grey_to_rgb(data2)
|
||||
return ifft2(fft2(data1) * fft2(data2))
|
||||
|
||||
|
||||
def fft2(data):
|
||||
if data.ndim > 2: # multiple channels
|
||||
out_fft = np.zeros(
|
||||
(data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128
|
||||
)
|
||||
for c in range(data.shape[2]):
|
||||
c_data = data[:, :, c]
|
||||
out_fft[:, :, c] = np.fft.fft2(np.fft.fftshift(c_data), norm="ortho")
|
||||
out_fft[:, :, c] = np.fft.ifftshift(out_fft[:, :, c])
|
||||
else: # single channel
|
||||
out_fft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128)
|
||||
out_fft[:, :] = np.fft.fft2(np.fft.fftshift(data), norm="ortho")
|
||||
out_fft[:, :] = np.fft.ifftshift(out_fft[:, :])
|
||||
|
||||
return out_fft
|
||||
|
||||
|
||||
def ifft2(data):
|
||||
if data.ndim > 2: # multiple channels
|
||||
out_ifft = np.zeros(
|
||||
(data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128
|
||||
)
|
||||
for c in range(data.shape[2]):
|
||||
c_data = data[:, :, c]
|
||||
out_ifft[:, :, c] = np.fft.ifft2(np.fft.fftshift(c_data), norm="ortho")
|
||||
out_ifft[:, :, c] = np.fft.ifftshift(out_ifft[:, :, c])
|
||||
else: # single channel
|
||||
out_ifft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128)
|
||||
out_ifft[:, :] = np.fft.ifft2(np.fft.fftshift(data), norm="ortho")
|
||||
out_ifft[:, :] = np.fft.ifftshift(out_ifft[:, :])
|
||||
|
||||
return out_ifft
|
||||
|
||||
|
||||
def get_gradient_kernel(width, height, std=3.14, mode="linear"):
|
||||
window_scale_x = float(
|
||||
width / min(width, height)
|
||||
) # for non-square aspect ratios we still want a circular kernel
|
||||
window_scale_y = float(height / min(width, height))
|
||||
if mode == "gaussian":
|
||||
x = (np.arange(width) / width * 2.0 - 1.0) * window_scale_x
|
||||
kx = np.exp(-x * x * std)
|
||||
if window_scale_x != window_scale_y:
|
||||
y = (np.arange(height) / height * 2.0 - 1.0) * window_scale_y
|
||||
ky = np.exp(-y * y * std)
|
||||
else:
|
||||
y = x
|
||||
ky = kx
|
||||
return np.outer(kx, ky)
|
||||
elif mode == "linear":
|
||||
x = (np.arange(width) / width * 2.0 - 1.0) * window_scale_x
|
||||
if window_scale_x != window_scale_y:
|
||||
y = (np.arange(height) / height * 2.0 - 1.0) * window_scale_y
|
||||
else:
|
||||
y = x
|
||||
return np.clip(1.0 - np.sqrt(np.add.outer(x * x, y * y)) * std / 3.14, 0.0, 1.0)
|
||||
else:
|
||||
raise Exception("Error: Unknown mode in get_gradient_kernel: {0}".format(mode))
|
||||
|
||||
|
||||
def image_blur(data, std=3.14, mode="linear"):
|
||||
width = data.shape[0]
|
||||
height = data.shape[1]
|
||||
kernel = get_gradient_kernel(width, height, std, mode=mode)
|
||||
return np.real(convolve(data, kernel / np.sqrt(np.sum(kernel * kernel))))
|
||||
|
||||
|
||||
def soften_mask(mask_img, softness, space):
|
||||
if softness == 0:
|
||||
return mask_img
|
||||
softness = min(softness, 1.0)
|
||||
space = np.clip(space, 0.0, 1.0)
|
||||
original_max_opacity = np.max(mask_img)
|
||||
out_mask = mask_img <= 0.0
|
||||
blurred_mask = image_blur(mask_img, 3.5 / softness, mode="linear")
|
||||
blurred_mask = np.maximum(blurred_mask - np.max(blurred_mask[out_mask]), 0.0)
|
||||
mask_img *= blurred_mask # preserve partial opacity in original input mask
|
||||
mask_img /= np.max(mask_img) # renormalize
|
||||
mask_img = np.clip(mask_img - space, 0.0, 1.0) # make space
|
||||
mask_img /= np.max(mask_img) # and renormalize again
|
||||
mask_img *= original_max_opacity # restore original max opacity
|
||||
return mask_img
|
||||
|
||||
|
||||
def expand_image(
|
||||
cv2_img, top: int, right: int, bottom: int, left: int, softness: float, space: float
|
||||
):
|
||||
assert cv2_img.shape[2] == 3
|
||||
origin_h, origin_w = cv2_img.shape[:2]
|
||||
new_width = cv2_img.shape[1] + left + right
|
||||
new_height = cv2_img.shape[0] + top + bottom
|
||||
|
||||
# TODO: which is better?
|
||||
# new_img = np.random.randint(0, 255, (new_height, new_width, 3), np.uint8)
|
||||
new_img = cv2.copyMakeBorder(
|
||||
cv2_img, top, bottom, left, right, cv2.BORDER_REPLICATE
|
||||
)
|
||||
mask_img = np.zeros((new_height, new_width), np.uint8)
|
||||
mask_img[top : top + cv2_img.shape[0], left : left + cv2_img.shape[1]] = 255
|
||||
|
||||
if softness > 0.0:
|
||||
mask_img = soften_mask(mask_img / 255.0, softness / 100.0, space / 100.0)
|
||||
mask_img = (np.clip(mask_img, 0.0, 1.0) * 255.0).astype(np.uint8)
|
||||
|
||||
mask_image = 255.0 - mask_img # extract mask from alpha channel and invert
|
||||
rgb_init_image = (
|
||||
0.0 + new_img[:, :, 0:3]
|
||||
) # strip mask from init_img leaving only rgb channels
|
||||
|
||||
hard_mask = np.zeros_like(cv2_img[:, :, 0])
|
||||
if top != 0:
|
||||
hard_mask[0 : origin_h // 2, :] = 255
|
||||
if bottom != 0:
|
||||
hard_mask[origin_h // 2 :, :] = 255
|
||||
if left != 0:
|
||||
hard_mask[:, 0 : origin_w // 2] = 255
|
||||
if right != 0:
|
||||
hard_mask[:, origin_w // 2 :] = 255
|
||||
|
||||
hard_mask = cv2.copyMakeBorder(
|
||||
hard_mask, top, bottom, left, right, cv2.BORDER_DEFAULT, value=255
|
||||
)
|
||||
mask_image = np.where(hard_mask > 0, mask_image, 0)
|
||||
return rgb_init_image.astype(np.uint8), mask_image.astype(np.uint8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pathlib import Path
|
||||
|
||||
current_dir = Path(__file__).parent.absolute().resolve()
|
||||
image_path = current_dir.parent / "tests" / "bunny.jpeg"
|
||||
init_image = cv2.imread(str(image_path))
|
||||
init_image, mask_image = expand_image(
|
||||
init_image,
|
||||
top=100,
|
||||
right=100,
|
||||
bottom=100,
|
||||
left=100,
|
||||
softness=20,
|
||||
space=20,
|
||||
)
|
||||
print(mask_image.dtype, mask_image.min(), mask_image.max())
|
||||
print(init_image.dtype, init_image.min(), init_image.max())
|
||||
mask_image = mask_image.astype(np.uint8)
|
||||
init_image = init_image.astype(np.uint8)
|
||||
cv2.imwrite("expanded_image.png", init_image)
|
||||
cv2.imwrite("expanded_mask.png", mask_image)
|
||||
63
iopaint/model/instruct_pix2pix.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
class InstructPix2Pix(DiffusionInpaintModel):
|
||||
name = "timbrooks/instruct-pix2pix"
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from diffusers import StableDiffusionInstructPix2PixPipeline
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
|
||||
model_kwargs = {}
|
||||
if kwargs["disable_nsfw"] or kwargs.get("cpu_offload", False):
|
||||
logger.info("Disable Stable Diffusion Model NSFW checker")
|
||||
model_kwargs.update(
|
||||
dict(
|
||||
safety_checker=None,
|
||||
feature_extractor=None,
|
||||
requires_safety_checker=False,
|
||||
)
|
||||
)
|
||||
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
self.model = StableDiffusionInstructPix2PixPipeline.from_pretrained(
|
||||
self.name, variant="fp16", torch_dtype=torch_dtype, **model_kwargs
|
||||
)
|
||||
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
logger.info("Enable sequential cpu offload")
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
edit = pipe(prompt, image=image, num_inference_steps=20, image_guidance_scale=1.5, guidance_scale=7).images[0]
|
||||
"""
|
||||
output = self.model(
|
||||
image=PIL.Image.fromarray(image),
|
||||
prompt=config.prompt,
|
||||
negative_prompt=config.negative_prompt,
|
||||
num_inference_steps=config.sd_steps,
|
||||
image_guidance_scale=config.p2p_image_guidance_scale,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
65
iopaint/model/kandinsky.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.model.utils import get_scheduler
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
class Kandinsky(DiffusionInpaintModel):
|
||||
pad_mod = 64
|
||||
min_size = 512
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from diffusers import AutoPipelineForInpainting
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
|
||||
model_kwargs = {
|
||||
"torch_dtype": torch_dtype,
|
||||
}
|
||||
|
||||
self.model = AutoPipelineForInpainting.from_pretrained(
|
||||
self.name, **model_kwargs
|
||||
).to(device)
|
||||
|
||||
self.callback = kwargs.pop("callback", None)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
self.set_scheduler(config)
|
||||
|
||||
generator = torch.manual_seed(config.sd_seed)
|
||||
mask = mask.astype(np.float32) / 255
|
||||
img_h, img_w = image.shape[:2]
|
||||
|
||||
# kandinsky 没有 strength
|
||||
output = self.model(
|
||||
prompt=config.prompt,
|
||||
negative_prompt=config.negative_prompt,
|
||||
image=PIL.Image.fromarray(image),
|
||||
mask_image=mask[:, :, 0],
|
||||
height=img_h,
|
||||
width=img_w,
|
||||
num_inference_steps=config.sd_steps,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
callback_on_step_end=self.callback,
|
||||
generator=generator,
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
|
||||
|
||||
class Kandinsky22(Kandinsky):
|
||||
name = "kandinsky-community/kandinsky-2-2-decoder-inpaint"
|
||||
57
iopaint/model/lama.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from iopaint.helper import (
|
||||
norm_img,
|
||||
get_cache_path_by_url,
|
||||
load_jit_model,
|
||||
download_model,
|
||||
)
|
||||
from iopaint.model.base import InpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
LAMA_MODEL_URL = os.environ.get(
|
||||
"LAMA_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt",
|
||||
)
|
||||
LAMA_MODEL_MD5 = os.environ.get("LAMA_MODEL_MD5", "e3aa4aaa15225a33ec84f9f4bc47e500")
|
||||
|
||||
|
||||
class LaMa(InpaintModel):
|
||||
name = "lama"
|
||||
pad_mod = 8
|
||||
is_erase_model = True
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
download_model(LAMA_MODEL_URL, LAMA_MODEL_MD5)
|
||||
|
||||
def init_model(self, device, **kwargs):
|
||||
self.model = load_jit_model(LAMA_MODEL_URL, device, LAMA_MODEL_MD5).eval()
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
return os.path.exists(get_cache_path_by_url(LAMA_MODEL_URL))
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
image = norm_img(image)
|
||||
mask = norm_img(mask)
|
||||
|
||||
mask = (mask > 0) * 1
|
||||
image = torch.from_numpy(image).unsqueeze(0).to(self.device)
|
||||
mask = torch.from_numpy(mask).unsqueeze(0).to(self.device)
|
||||
|
||||
inpainted_image = self.model(image, mask)
|
||||
|
||||
cur_res = inpainted_image[0].permute(1, 2, 0).detach().cpu().numpy()
|
||||
cur_res = np.clip(cur_res * 255, 0, 255).astype("uint8")
|
||||
cur_res = cv2.cvtColor(cur_res, cv2.COLOR_RGB2BGR)
|
||||
return cur_res
|
||||
336
iopaint/model/ldm.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import InpaintModel
|
||||
from iopaint.model.ddim_sampler import DDIMSampler
|
||||
from iopaint.model.plms_sampler import PLMSSampler
|
||||
from iopaint.schema import InpaintRequest, LDMSampler
|
||||
|
||||
torch.manual_seed(42)
|
||||
import torch.nn as nn
|
||||
from iopaint.helper import (
|
||||
download_model,
|
||||
norm_img,
|
||||
get_cache_path_by_url,
|
||||
load_jit_model,
|
||||
)
|
||||
from iopaint.model.utils import (
|
||||
make_beta_schedule,
|
||||
timestep_embedding,
|
||||
)
|
||||
|
||||
LDM_ENCODE_MODEL_URL = os.environ.get(
|
||||
"LDM_ENCODE_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_ldm/cond_stage_model_encode.pt",
|
||||
)
|
||||
LDM_ENCODE_MODEL_MD5 = os.environ.get(
|
||||
"LDM_ENCODE_MODEL_MD5", "23239fc9081956a3e70de56472b3f296"
|
||||
)
|
||||
|
||||
LDM_DECODE_MODEL_URL = os.environ.get(
|
||||
"LDM_DECODE_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_ldm/cond_stage_model_decode.pt",
|
||||
)
|
||||
LDM_DECODE_MODEL_MD5 = os.environ.get(
|
||||
"LDM_DECODE_MODEL_MD5", "fe419cd15a750d37a4733589d0d3585c"
|
||||
)
|
||||
|
||||
LDM_DIFFUSION_MODEL_URL = os.environ.get(
|
||||
"LDM_DIFFUSION_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_ldm/diffusion.pt",
|
||||
)
|
||||
|
||||
LDM_DIFFUSION_MODEL_MD5 = os.environ.get(
|
||||
"LDM_DIFFUSION_MODEL_MD5", "b0afda12bf790c03aba2a7431f11d22d"
|
||||
)
|
||||
|
||||
|
||||
class DDPM(nn.Module):
|
||||
# classic DDPM with Gaussian diffusion, in image space
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
timesteps=1000,
|
||||
beta_schedule="linear",
|
||||
linear_start=0.0015,
|
||||
linear_end=0.0205,
|
||||
cosine_s=0.008,
|
||||
original_elbo_weight=0.0,
|
||||
v_posterior=0.0, # weight for choosing posterior variance as sigma = (1-v) * beta_tilde + v * beta
|
||||
l_simple_weight=1.0,
|
||||
parameterization="eps", # all assuming fixed variance schedules
|
||||
use_positional_encodings=False,
|
||||
):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.parameterization = parameterization
|
||||
self.use_positional_encodings = use_positional_encodings
|
||||
|
||||
self.v_posterior = v_posterior
|
||||
self.original_elbo_weight = original_elbo_weight
|
||||
self.l_simple_weight = l_simple_weight
|
||||
|
||||
self.register_schedule(
|
||||
beta_schedule=beta_schedule,
|
||||
timesteps=timesteps,
|
||||
linear_start=linear_start,
|
||||
linear_end=linear_end,
|
||||
cosine_s=cosine_s,
|
||||
)
|
||||
|
||||
def register_schedule(
|
||||
self,
|
||||
given_betas=None,
|
||||
beta_schedule="linear",
|
||||
timesteps=1000,
|
||||
linear_start=1e-4,
|
||||
linear_end=2e-2,
|
||||
cosine_s=8e-3,
|
||||
):
|
||||
betas = make_beta_schedule(
|
||||
self.device,
|
||||
beta_schedule,
|
||||
timesteps,
|
||||
linear_start=linear_start,
|
||||
linear_end=linear_end,
|
||||
cosine_s=cosine_s,
|
||||
)
|
||||
alphas = 1.0 - betas
|
||||
alphas_cumprod = np.cumprod(alphas, axis=0)
|
||||
alphas_cumprod_prev = np.append(1.0, alphas_cumprod[:-1])
|
||||
|
||||
(timesteps,) = betas.shape
|
||||
self.num_timesteps = int(timesteps)
|
||||
self.linear_start = linear_start
|
||||
self.linear_end = linear_end
|
||||
assert (
|
||||
alphas_cumprod.shape[0] == self.num_timesteps
|
||||
), "alphas have to be defined for each timestep"
|
||||
|
||||
to_torch = lambda x: torch.tensor(x, dtype=torch.float32).to(self.device)
|
||||
|
||||
self.register_buffer("betas", to_torch(betas))
|
||||
self.register_buffer("alphas_cumprod", to_torch(alphas_cumprod))
|
||||
self.register_buffer("alphas_cumprod_prev", to_torch(alphas_cumprod_prev))
|
||||
|
||||
# calculations for diffusion q(x_t | x_{t-1}) and others
|
||||
self.register_buffer("sqrt_alphas_cumprod", to_torch(np.sqrt(alphas_cumprod)))
|
||||
self.register_buffer(
|
||||
"sqrt_one_minus_alphas_cumprod", to_torch(np.sqrt(1.0 - alphas_cumprod))
|
||||
)
|
||||
self.register_buffer(
|
||||
"log_one_minus_alphas_cumprod", to_torch(np.log(1.0 - alphas_cumprod))
|
||||
)
|
||||
self.register_buffer(
|
||||
"sqrt_recip_alphas_cumprod", to_torch(np.sqrt(1.0 / alphas_cumprod))
|
||||
)
|
||||
self.register_buffer(
|
||||
"sqrt_recipm1_alphas_cumprod", to_torch(np.sqrt(1.0 / alphas_cumprod - 1))
|
||||
)
|
||||
|
||||
# calculations for posterior q(x_{t-1} | x_t, x_0)
|
||||
posterior_variance = (1 - self.v_posterior) * betas * (
|
||||
1.0 - alphas_cumprod_prev
|
||||
) / (1.0 - alphas_cumprod) + self.v_posterior * betas
|
||||
# above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t)
|
||||
self.register_buffer("posterior_variance", to_torch(posterior_variance))
|
||||
# below: log calculation clipped because the posterior variance is 0 at the beginning of the diffusion chain
|
||||
self.register_buffer(
|
||||
"posterior_log_variance_clipped",
|
||||
to_torch(np.log(np.maximum(posterior_variance, 1e-20))),
|
||||
)
|
||||
self.register_buffer(
|
||||
"posterior_mean_coef1",
|
||||
to_torch(betas * np.sqrt(alphas_cumprod_prev) / (1.0 - alphas_cumprod)),
|
||||
)
|
||||
self.register_buffer(
|
||||
"posterior_mean_coef2",
|
||||
to_torch(
|
||||
(1.0 - alphas_cumprod_prev) * np.sqrt(alphas) / (1.0 - alphas_cumprod)
|
||||
),
|
||||
)
|
||||
|
||||
if self.parameterization == "eps":
|
||||
lvlb_weights = self.betas**2 / (
|
||||
2
|
||||
* self.posterior_variance
|
||||
* to_torch(alphas)
|
||||
* (1 - self.alphas_cumprod)
|
||||
)
|
||||
elif self.parameterization == "x0":
|
||||
lvlb_weights = (
|
||||
0.5
|
||||
* np.sqrt(torch.Tensor(alphas_cumprod))
|
||||
/ (2.0 * 1 - torch.Tensor(alphas_cumprod))
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("mu not supported")
|
||||
# TODO how to choose this term
|
||||
lvlb_weights[0] = lvlb_weights[1]
|
||||
self.register_buffer("lvlb_weights", lvlb_weights, persistent=False)
|
||||
assert not torch.isnan(self.lvlb_weights).all()
|
||||
|
||||
|
||||
class LatentDiffusion(DDPM):
|
||||
def __init__(
|
||||
self,
|
||||
diffusion_model,
|
||||
device,
|
||||
cond_stage_key="image",
|
||||
cond_stage_trainable=False,
|
||||
concat_mode=True,
|
||||
scale_factor=1.0,
|
||||
scale_by_std=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.num_timesteps_cond = 1
|
||||
self.scale_by_std = scale_by_std
|
||||
super().__init__(device, *args, **kwargs)
|
||||
self.diffusion_model = diffusion_model
|
||||
self.concat_mode = concat_mode
|
||||
self.cond_stage_trainable = cond_stage_trainable
|
||||
self.cond_stage_key = cond_stage_key
|
||||
self.num_downs = 2
|
||||
self.scale_factor = scale_factor
|
||||
|
||||
def make_cond_schedule(
|
||||
self,
|
||||
):
|
||||
self.cond_ids = torch.full(
|
||||
size=(self.num_timesteps,),
|
||||
fill_value=self.num_timesteps - 1,
|
||||
dtype=torch.long,
|
||||
)
|
||||
ids = torch.round(
|
||||
torch.linspace(0, self.num_timesteps - 1, self.num_timesteps_cond)
|
||||
).long()
|
||||
self.cond_ids[: self.num_timesteps_cond] = ids
|
||||
|
||||
def register_schedule(
|
||||
self,
|
||||
given_betas=None,
|
||||
beta_schedule="linear",
|
||||
timesteps=1000,
|
||||
linear_start=1e-4,
|
||||
linear_end=2e-2,
|
||||
cosine_s=8e-3,
|
||||
):
|
||||
super().register_schedule(
|
||||
given_betas, beta_schedule, timesteps, linear_start, linear_end, cosine_s
|
||||
)
|
||||
|
||||
self.shorten_cond_schedule = self.num_timesteps_cond > 1
|
||||
if self.shorten_cond_schedule:
|
||||
self.make_cond_schedule()
|
||||
|
||||
def apply_model(self, x_noisy, t, cond):
|
||||
# x_recon = self.model(x_noisy, t, cond['c_concat'][0]) # cond['c_concat'][0].shape 1,4,128,128
|
||||
t_emb = timestep_embedding(x_noisy.device, t, 256, repeat_only=False)
|
||||
x_recon = self.diffusion_model(x_noisy, t_emb, cond)
|
||||
return x_recon
|
||||
|
||||
|
||||
class LDM(InpaintModel):
|
||||
name = "ldm"
|
||||
pad_mod = 32
|
||||
is_erase_model = True
|
||||
|
||||
def __init__(self, device, fp16: bool = True, **kwargs):
|
||||
self.fp16 = fp16
|
||||
super().__init__(device)
|
||||
self.device = device
|
||||
|
||||
def init_model(self, device, **kwargs):
|
||||
self.diffusion_model = load_jit_model(
|
||||
LDM_DIFFUSION_MODEL_URL, device, LDM_DIFFUSION_MODEL_MD5
|
||||
)
|
||||
self.cond_stage_model_decode = load_jit_model(
|
||||
LDM_DECODE_MODEL_URL, device, LDM_DECODE_MODEL_MD5
|
||||
)
|
||||
self.cond_stage_model_encode = load_jit_model(
|
||||
LDM_ENCODE_MODEL_URL, device, LDM_ENCODE_MODEL_MD5
|
||||
)
|
||||
if self.fp16 and "cuda" in str(device):
|
||||
self.diffusion_model = self.diffusion_model.half()
|
||||
self.cond_stage_model_decode = self.cond_stage_model_decode.half()
|
||||
self.cond_stage_model_encode = self.cond_stage_model_encode.half()
|
||||
|
||||
self.model = LatentDiffusion(self.diffusion_model, device)
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
download_model(LDM_DIFFUSION_MODEL_URL, LDM_DIFFUSION_MODEL_MD5)
|
||||
download_model(LDM_DECODE_MODEL_URL, LDM_DECODE_MODEL_MD5)
|
||||
download_model(LDM_ENCODE_MODEL_URL, LDM_ENCODE_MODEL_MD5)
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
model_paths = [
|
||||
get_cache_path_by_url(LDM_DIFFUSION_MODEL_URL),
|
||||
get_cache_path_by_url(LDM_DECODE_MODEL_URL),
|
||||
get_cache_path_by_url(LDM_ENCODE_MODEL_URL),
|
||||
]
|
||||
return all([os.path.exists(it) for it in model_paths])
|
||||
|
||||
@torch.cuda.amp.autocast()
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
# image [1,3,512,512] float32
|
||||
# mask: [1,1,512,512] float32
|
||||
# masked_image: [1,3,512,512] float32
|
||||
if config.ldm_sampler == LDMSampler.ddim:
|
||||
sampler = DDIMSampler(self.model)
|
||||
elif config.ldm_sampler == LDMSampler.plms:
|
||||
sampler = PLMSSampler(self.model)
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
steps = config.ldm_steps
|
||||
image = norm_img(image)
|
||||
mask = norm_img(mask)
|
||||
|
||||
mask[mask < 0.5] = 0
|
||||
mask[mask >= 0.5] = 1
|
||||
|
||||
image = torch.from_numpy(image).unsqueeze(0).to(self.device)
|
||||
mask = torch.from_numpy(mask).unsqueeze(0).to(self.device)
|
||||
masked_image = (1 - mask) * image
|
||||
|
||||
mask = self._norm(mask)
|
||||
masked_image = self._norm(masked_image)
|
||||
|
||||
c = self.cond_stage_model_encode(masked_image)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
cc = torch.nn.functional.interpolate(mask, size=c.shape[-2:]) # 1,1,128,128
|
||||
c = torch.cat((c, cc), dim=1) # 1,4,128,128
|
||||
|
||||
shape = (c.shape[1] - 1,) + c.shape[2:]
|
||||
samples_ddim = sampler.sample(
|
||||
steps=steps, conditioning=c, batch_size=c.shape[0], shape=shape
|
||||
)
|
||||
torch.cuda.empty_cache()
|
||||
x_samples_ddim = self.cond_stage_model_decode(
|
||||
samples_ddim
|
||||
) # samples_ddim: 1, 3, 128, 128 float32
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# image = torch.clamp((image + 1.0) / 2.0, min=0.0, max=1.0)
|
||||
# mask = torch.clamp((mask + 1.0) / 2.0, min=0.0, max=1.0)
|
||||
inpainted_image = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0)
|
||||
|
||||
# inpainted = (1 - mask) * image + mask * predicted_image
|
||||
inpainted_image = inpainted_image.cpu().numpy().transpose(0, 2, 3, 1)[0] * 255
|
||||
inpainted_image = inpainted_image.astype(np.uint8)[:, :, ::-1]
|
||||
return inpainted_image
|
||||
|
||||
def _norm(self, tensor):
|
||||
return tensor * 2.0 - 1.0
|
||||
97
iopaint/model/manga.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import time
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import get_cache_path_by_url, load_jit_model, download_model
|
||||
from iopaint.model.base import InpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
MANGA_INPAINTOR_MODEL_URL = os.environ.get(
|
||||
"MANGA_INPAINTOR_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/manga/manga_inpaintor.jit",
|
||||
)
|
||||
MANGA_INPAINTOR_MODEL_MD5 = os.environ.get(
|
||||
"MANGA_INPAINTOR_MODEL_MD5", "7d8b269c4613b6b3768af714610da86c"
|
||||
)
|
||||
|
||||
MANGA_LINE_MODEL_URL = os.environ.get(
|
||||
"MANGA_LINE_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/manga/erika.jit",
|
||||
)
|
||||
MANGA_LINE_MODEL_MD5 = os.environ.get(
|
||||
"MANGA_LINE_MODEL_MD5", "0c926d5a4af8450b0d00bc5b9a095644"
|
||||
)
|
||||
|
||||
|
||||
class Manga(InpaintModel):
|
||||
name = "manga"
|
||||
pad_mod = 16
|
||||
is_erase_model = True
|
||||
|
||||
def init_model(self, device, **kwargs):
|
||||
self.inpaintor_model = load_jit_model(
|
||||
MANGA_INPAINTOR_MODEL_URL, device, MANGA_INPAINTOR_MODEL_MD5
|
||||
)
|
||||
self.line_model = load_jit_model(
|
||||
MANGA_LINE_MODEL_URL, device, MANGA_LINE_MODEL_MD5
|
||||
)
|
||||
self.seed = 42
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
download_model(MANGA_INPAINTOR_MODEL_URL, MANGA_INPAINTOR_MODEL_MD5)
|
||||
download_model(MANGA_LINE_MODEL_URL, MANGA_LINE_MODEL_MD5)
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
model_paths = [
|
||||
get_cache_path_by_url(MANGA_INPAINTOR_MODEL_URL),
|
||||
get_cache_path_by_url(MANGA_LINE_MODEL_URL),
|
||||
]
|
||||
return all([os.path.exists(it) for it in model_paths])
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
seed = self.seed
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
|
||||
gray_img = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
|
||||
gray_img = torch.from_numpy(
|
||||
gray_img[np.newaxis, np.newaxis, :, :].astype(np.float32)
|
||||
).to(self.device)
|
||||
start = time.time()
|
||||
lines = self.line_model(gray_img)
|
||||
torch.cuda.empty_cache()
|
||||
lines = torch.clamp(lines, 0, 255)
|
||||
logger.info(f"erika_model time: {time.time() - start}")
|
||||
|
||||
mask = torch.from_numpy(mask[np.newaxis, :, :, :]).to(self.device)
|
||||
mask = mask.permute(0, 3, 1, 2)
|
||||
mask = torch.where(mask > 0.5, 1.0, 0.0)
|
||||
noise = torch.randn_like(mask)
|
||||
ones = torch.ones_like(mask)
|
||||
|
||||
gray_img = gray_img / 255 * 2 - 1.0
|
||||
lines = lines / 255 * 2 - 1.0
|
||||
|
||||
start = time.time()
|
||||
inpainted_image = self.inpaintor_model(gray_img, lines, mask, noise, ones)
|
||||
logger.info(f"image_inpaintor_model time: {time.time() - start}")
|
||||
|
||||
cur_res = inpainted_image[0].permute(1, 2, 0).detach().cpu().numpy()
|
||||
cur_res = (cur_res * 127.5 + 127.5).astype(np.uint8)
|
||||
cur_res = cv2.cvtColor(cur_res, cv2.COLOR_GRAY2BGR)
|
||||
return cur_res
|
||||
1945
iopaint/model/mat.py
Normal file
110
iopaint/model/mi_gan.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
|
||||
from iopaint.helper import (
|
||||
load_jit_model,
|
||||
download_model,
|
||||
get_cache_path_by_url,
|
||||
boxes_from_mask,
|
||||
resize_max_size,
|
||||
norm_img,
|
||||
)
|
||||
from iopaint.model.base import InpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
MIGAN_MODEL_URL = os.environ.get(
|
||||
"MIGAN_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/migan/migan_traced.pt",
|
||||
)
|
||||
MIGAN_MODEL_MD5 = os.environ.get("MIGAN_MODEL_MD5", "76eb3b1a71c400ee3290524f7a11b89c")
|
||||
|
||||
|
||||
class MIGAN(InpaintModel):
|
||||
name = "migan"
|
||||
min_size = 512
|
||||
pad_mod = 512
|
||||
pad_to_square = True
|
||||
is_erase_model = True
|
||||
|
||||
def init_model(self, device, **kwargs):
|
||||
self.model = load_jit_model(MIGAN_MODEL_URL, device, MIGAN_MODEL_MD5).eval()
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
download_model(MIGAN_MODEL_URL, MIGAN_MODEL_MD5)
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
return os.path.exists(get_cache_path_by_url(MIGAN_MODEL_URL))
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
images: [H, W, C] RGB, not normalized
|
||||
masks: [H, W]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
if image.shape[0] == 512 and image.shape[1] == 512:
|
||||
return self._pad_forward(image, mask, config)
|
||||
|
||||
boxes = boxes_from_mask(mask)
|
||||
crop_result = []
|
||||
config.hd_strategy_crop_margin = 128
|
||||
for box in boxes:
|
||||
crop_image, crop_mask, crop_box = self._crop_box(image, mask, box, config)
|
||||
origin_size = crop_image.shape[:2]
|
||||
resize_image = resize_max_size(crop_image, size_limit=512)
|
||||
resize_mask = resize_max_size(crop_mask, size_limit=512)
|
||||
inpaint_result = self._pad_forward(resize_image, resize_mask, config)
|
||||
|
||||
# only paste masked area result
|
||||
inpaint_result = cv2.resize(
|
||||
inpaint_result,
|
||||
(origin_size[1], origin_size[0]),
|
||||
interpolation=cv2.INTER_CUBIC,
|
||||
)
|
||||
|
||||
original_pixel_indices = crop_mask < 127
|
||||
inpaint_result[original_pixel_indices] = crop_image[:, :, ::-1][
|
||||
original_pixel_indices
|
||||
]
|
||||
|
||||
crop_result.append((inpaint_result, crop_box))
|
||||
|
||||
inpaint_result = image[:, :, ::-1].copy()
|
||||
for crop_image, crop_box in crop_result:
|
||||
x1, y1, x2, y2 = crop_box
|
||||
inpaint_result[y1:y2, x1:x2, :] = crop_image
|
||||
|
||||
return inpaint_result
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input images and output images have same size
|
||||
images: [H, W, C] RGB
|
||||
masks: [H, W] mask area == 255
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
|
||||
image = norm_img(image) # [0, 1]
|
||||
image = image * 2 - 1 # [0, 1] -> [-1, 1]
|
||||
mask = (mask > 120) * 255
|
||||
mask = norm_img(mask)
|
||||
|
||||
image = torch.from_numpy(image).unsqueeze(0).to(self.device)
|
||||
mask = torch.from_numpy(mask).unsqueeze(0).to(self.device)
|
||||
|
||||
erased_img = image * (1 - mask)
|
||||
input_image = torch.cat([0.5 - mask, erased_img], dim=1)
|
||||
|
||||
output = self.model(input_image)
|
||||
output = (
|
||||
(output.permute(0, 2, 3, 1) * 127.5 + 127.5)
|
||||
.round()
|
||||
.clamp(0, 255)
|
||||
.to(torch.uint8)
|
||||
)
|
||||
output = output[0].cpu().numpy()
|
||||
cur_res = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return cur_res
|
||||
29
iopaint/model/opencv2.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import cv2
|
||||
from iopaint.model.base import InpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
flag_map = {"INPAINT_NS": cv2.INPAINT_NS, "INPAINT_TELEA": cv2.INPAINT_TELEA}
|
||||
|
||||
|
||||
class OpenCV2(InpaintModel):
|
||||
name = "cv2"
|
||||
pad_mod = 1
|
||||
is_erase_model = True
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
return True
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
cur_res = cv2.inpaint(
|
||||
image[:, :, ::-1],
|
||||
mask,
|
||||
inpaintRadius=config.cv2_radius,
|
||||
flags=flag_map[config.cv2_flag],
|
||||
)
|
||||
return cur_res
|
||||
66
iopaint/model/paint_by_example.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import PIL
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import decode_base64_to_image
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
class PaintByExample(DiffusionInpaintModel):
|
||||
name = "Fantasy-Studio/Paint-by-Example"
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from diffusers import DiffusionPipeline
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
model_kwargs = {}
|
||||
|
||||
if kwargs["disable_nsfw"] or kwargs.get("cpu_offload", False):
|
||||
logger.info("Disable Paint By Example Model NSFW checker")
|
||||
model_kwargs.update(
|
||||
dict(safety_checker=None, requires_safety_checker=False)
|
||||
)
|
||||
|
||||
self.model = DiffusionPipeline.from_pretrained(
|
||||
self.name, torch_dtype=torch_dtype, **model_kwargs
|
||||
)
|
||||
|
||||
# TODO: gpu_id
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
self.model.image_encoder = self.model.image_encoder.to(device)
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
if config.paint_by_example_example_image is None:
|
||||
raise ValueError("paint_by_example_example_image is required")
|
||||
example_image, _, _ = decode_base64_to_image(
|
||||
config.paint_by_example_example_image
|
||||
)
|
||||
output = self.model(
|
||||
image=PIL.Image.fromarray(image),
|
||||
mask_image=PIL.Image.fromarray(mask[:, :, -1], mode="L"),
|
||||
example_image=PIL.Image.fromarray(example_image),
|
||||
num_inference_steps=config.sd_steps,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
negative_prompt="out of frame, lowres, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, disfigured, gross proportions, malformed limbs, watermark, signature",
|
||||
output_type="np.array",
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
225
iopaint/model/plms_sampler.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# From: https://github.com/CompVis/latent-diffusion/blob/main/ldm/models/diffusion/plms.py
|
||||
import torch
|
||||
import numpy as np
|
||||
from iopaint.model.utils import make_ddim_timesteps, make_ddim_sampling_parameters, noise_like
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class PLMSSampler(object):
|
||||
def __init__(self, model, schedule="linear", **kwargs):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.ddpm_num_timesteps = model.num_timesteps
|
||||
self.schedule = schedule
|
||||
|
||||
def register_buffer(self, name, attr):
|
||||
setattr(self, name, attr)
|
||||
|
||||
def make_schedule(self, ddim_num_steps, ddim_discretize="uniform", ddim_eta=0., verbose=True):
|
||||
if ddim_eta != 0:
|
||||
raise ValueError('ddim_eta must be 0 for PLMS')
|
||||
self.ddim_timesteps = make_ddim_timesteps(ddim_discr_method=ddim_discretize, num_ddim_timesteps=ddim_num_steps,
|
||||
num_ddpm_timesteps=self.ddpm_num_timesteps, verbose=verbose)
|
||||
alphas_cumprod = self.model.alphas_cumprod
|
||||
assert alphas_cumprod.shape[0] == self.ddpm_num_timesteps, 'alphas have to be defined for each timestep'
|
||||
to_torch = lambda x: x.clone().detach().to(torch.float32).to(self.model.device)
|
||||
|
||||
self.register_buffer('betas', to_torch(self.model.betas))
|
||||
self.register_buffer('alphas_cumprod', to_torch(alphas_cumprod))
|
||||
self.register_buffer('alphas_cumprod_prev', to_torch(self.model.alphas_cumprod_prev))
|
||||
|
||||
# calculations for diffusion q(x_t | x_{t-1}) and others
|
||||
self.register_buffer('sqrt_alphas_cumprod', to_torch(np.sqrt(alphas_cumprod.cpu())))
|
||||
self.register_buffer('sqrt_one_minus_alphas_cumprod', to_torch(np.sqrt(1. - alphas_cumprod.cpu())))
|
||||
self.register_buffer('log_one_minus_alphas_cumprod', to_torch(np.log(1. - alphas_cumprod.cpu())))
|
||||
self.register_buffer('sqrt_recip_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod.cpu())))
|
||||
self.register_buffer('sqrt_recipm1_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod.cpu() - 1)))
|
||||
|
||||
# ddim sampling parameters
|
||||
ddim_sigmas, ddim_alphas, ddim_alphas_prev = make_ddim_sampling_parameters(alphacums=alphas_cumprod.cpu(),
|
||||
ddim_timesteps=self.ddim_timesteps,
|
||||
eta=ddim_eta, verbose=verbose)
|
||||
self.register_buffer('ddim_sigmas', ddim_sigmas)
|
||||
self.register_buffer('ddim_alphas', ddim_alphas)
|
||||
self.register_buffer('ddim_alphas_prev', ddim_alphas_prev)
|
||||
self.register_buffer('ddim_sqrt_one_minus_alphas', np.sqrt(1. - ddim_alphas))
|
||||
sigmas_for_original_sampling_steps = ddim_eta * torch.sqrt(
|
||||
(1 - self.alphas_cumprod_prev) / (1 - self.alphas_cumprod) * (
|
||||
1 - self.alphas_cumprod / self.alphas_cumprod_prev))
|
||||
self.register_buffer('ddim_sigmas_for_original_num_steps', sigmas_for_original_sampling_steps)
|
||||
|
||||
@torch.no_grad()
|
||||
def sample(self,
|
||||
steps,
|
||||
batch_size,
|
||||
shape,
|
||||
conditioning=None,
|
||||
callback=None,
|
||||
normals_sequence=None,
|
||||
img_callback=None,
|
||||
quantize_x0=False,
|
||||
eta=0.,
|
||||
mask=None,
|
||||
x0=None,
|
||||
temperature=1.,
|
||||
noise_dropout=0.,
|
||||
score_corrector=None,
|
||||
corrector_kwargs=None,
|
||||
verbose=False,
|
||||
x_T=None,
|
||||
log_every_t=100,
|
||||
unconditional_guidance_scale=1.,
|
||||
unconditional_conditioning=None,
|
||||
# this has to come in the same format as the conditioning, # e.g. as encoded tokens, ...
|
||||
**kwargs
|
||||
):
|
||||
if conditioning is not None:
|
||||
if isinstance(conditioning, dict):
|
||||
cbs = conditioning[list(conditioning.keys())[0]].shape[0]
|
||||
if cbs != batch_size:
|
||||
print(f"Warning: Got {cbs} conditionings but batch-size is {batch_size}")
|
||||
else:
|
||||
if conditioning.shape[0] != batch_size:
|
||||
print(f"Warning: Got {conditioning.shape[0]} conditionings but batch-size is {batch_size}")
|
||||
|
||||
self.make_schedule(ddim_num_steps=steps, ddim_eta=eta, verbose=verbose)
|
||||
# sampling
|
||||
C, H, W = shape
|
||||
size = (batch_size, C, H, W)
|
||||
print(f'Data shape for PLMS sampling is {size}')
|
||||
|
||||
samples = self.plms_sampling(conditioning, size,
|
||||
callback=callback,
|
||||
img_callback=img_callback,
|
||||
quantize_denoised=quantize_x0,
|
||||
mask=mask, x0=x0,
|
||||
ddim_use_original_steps=False,
|
||||
noise_dropout=noise_dropout,
|
||||
temperature=temperature,
|
||||
score_corrector=score_corrector,
|
||||
corrector_kwargs=corrector_kwargs,
|
||||
x_T=x_T,
|
||||
log_every_t=log_every_t,
|
||||
unconditional_guidance_scale=unconditional_guidance_scale,
|
||||
unconditional_conditioning=unconditional_conditioning,
|
||||
)
|
||||
return samples
|
||||
|
||||
@torch.no_grad()
|
||||
def plms_sampling(self, cond, shape,
|
||||
x_T=None, ddim_use_original_steps=False,
|
||||
callback=None, timesteps=None, quantize_denoised=False,
|
||||
mask=None, x0=None, img_callback=None, log_every_t=100,
|
||||
temperature=1., noise_dropout=0., score_corrector=None, corrector_kwargs=None,
|
||||
unconditional_guidance_scale=1., unconditional_conditioning=None, ):
|
||||
device = self.model.betas.device
|
||||
b = shape[0]
|
||||
if x_T is None:
|
||||
img = torch.randn(shape, device=device)
|
||||
else:
|
||||
img = x_T
|
||||
|
||||
if timesteps is None:
|
||||
timesteps = self.ddpm_num_timesteps if ddim_use_original_steps else self.ddim_timesteps
|
||||
elif timesteps is not None and not ddim_use_original_steps:
|
||||
subset_end = int(min(timesteps / self.ddim_timesteps.shape[0], 1) * self.ddim_timesteps.shape[0]) - 1
|
||||
timesteps = self.ddim_timesteps[:subset_end]
|
||||
|
||||
time_range = list(reversed(range(0, timesteps))) if ddim_use_original_steps else np.flip(timesteps)
|
||||
total_steps = timesteps if ddim_use_original_steps else timesteps.shape[0]
|
||||
print(f"Running PLMS Sampling with {total_steps} timesteps")
|
||||
|
||||
iterator = tqdm(time_range, desc='PLMS Sampler', total=total_steps)
|
||||
old_eps = []
|
||||
|
||||
for i, step in enumerate(iterator):
|
||||
index = total_steps - i - 1
|
||||
ts = torch.full((b,), step, device=device, dtype=torch.long)
|
||||
ts_next = torch.full((b,), time_range[min(i + 1, len(time_range) - 1)], device=device, dtype=torch.long)
|
||||
|
||||
if mask is not None:
|
||||
assert x0 is not None
|
||||
img_orig = self.model.q_sample(x0, ts) # TODO: deterministic forward pass?
|
||||
img = img_orig * mask + (1. - mask) * img
|
||||
|
||||
outs = self.p_sample_plms(img, cond, ts, index=index, use_original_steps=ddim_use_original_steps,
|
||||
quantize_denoised=quantize_denoised, temperature=temperature,
|
||||
noise_dropout=noise_dropout, score_corrector=score_corrector,
|
||||
corrector_kwargs=corrector_kwargs,
|
||||
unconditional_guidance_scale=unconditional_guidance_scale,
|
||||
unconditional_conditioning=unconditional_conditioning,
|
||||
old_eps=old_eps, t_next=ts_next)
|
||||
img, pred_x0, e_t = outs
|
||||
old_eps.append(e_t)
|
||||
if len(old_eps) >= 4:
|
||||
old_eps.pop(0)
|
||||
if callback: callback(i)
|
||||
if img_callback: img_callback(pred_x0, i)
|
||||
|
||||
return img
|
||||
|
||||
@torch.no_grad()
|
||||
def p_sample_plms(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False,
|
||||
temperature=1., noise_dropout=0., score_corrector=None, corrector_kwargs=None,
|
||||
unconditional_guidance_scale=1., unconditional_conditioning=None, old_eps=None, t_next=None):
|
||||
b, *_, device = *x.shape, x.device
|
||||
|
||||
def get_model_output(x, t):
|
||||
if unconditional_conditioning is None or unconditional_guidance_scale == 1.:
|
||||
e_t = self.model.apply_model(x, t, c)
|
||||
else:
|
||||
x_in = torch.cat([x] * 2)
|
||||
t_in = torch.cat([t] * 2)
|
||||
c_in = torch.cat([unconditional_conditioning, c])
|
||||
e_t_uncond, e_t = self.model.apply_model(x_in, t_in, c_in).chunk(2)
|
||||
e_t = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)
|
||||
|
||||
if score_corrector is not None:
|
||||
assert self.model.parameterization == "eps"
|
||||
e_t = score_corrector.modify_score(self.model, e_t, x, t, c, **corrector_kwargs)
|
||||
|
||||
return e_t
|
||||
|
||||
alphas = self.model.alphas_cumprod if use_original_steps else self.ddim_alphas
|
||||
alphas_prev = self.model.alphas_cumprod_prev if use_original_steps else self.ddim_alphas_prev
|
||||
sqrt_one_minus_alphas = self.model.sqrt_one_minus_alphas_cumprod if use_original_steps else self.ddim_sqrt_one_minus_alphas
|
||||
sigmas = self.model.ddim_sigmas_for_original_num_steps if use_original_steps else self.ddim_sigmas
|
||||
|
||||
def get_x_prev_and_pred_x0(e_t, index):
|
||||
# select parameters corresponding to the currently considered timestep
|
||||
a_t = torch.full((b, 1, 1, 1), alphas[index], device=device)
|
||||
a_prev = torch.full((b, 1, 1, 1), alphas_prev[index], device=device)
|
||||
sigma_t = torch.full((b, 1, 1, 1), sigmas[index], device=device)
|
||||
sqrt_one_minus_at = torch.full((b, 1, 1, 1), sqrt_one_minus_alphas[index], device=device)
|
||||
|
||||
# current prediction for x_0
|
||||
pred_x0 = (x - sqrt_one_minus_at * e_t) / a_t.sqrt()
|
||||
if quantize_denoised:
|
||||
pred_x0, _, *_ = self.model.first_stage_model.quantize(pred_x0)
|
||||
# direction pointing to x_t
|
||||
dir_xt = (1. - a_prev - sigma_t ** 2).sqrt() * e_t
|
||||
noise = sigma_t * noise_like(x.shape, device, repeat_noise) * temperature
|
||||
if noise_dropout > 0.:
|
||||
noise = torch.nn.functional.dropout(noise, p=noise_dropout)
|
||||
x_prev = a_prev.sqrt() * pred_x0 + dir_xt + noise
|
||||
return x_prev, pred_x0
|
||||
|
||||
e_t = get_model_output(x, t)
|
||||
if len(old_eps) == 0:
|
||||
# Pseudo Improved Euler (2nd order)
|
||||
x_prev, pred_x0 = get_x_prev_and_pred_x0(e_t, index)
|
||||
e_t_next = get_model_output(x_prev, t_next)
|
||||
e_t_prime = (e_t + e_t_next) / 2
|
||||
elif len(old_eps) == 1:
|
||||
# 2nd order Pseudo Linear Multistep (Adams-Bashforth)
|
||||
e_t_prime = (3 * e_t - old_eps[-1]) / 2
|
||||
elif len(old_eps) == 2:
|
||||
# 3nd order Pseudo Linear Multistep (Adams-Bashforth)
|
||||
e_t_prime = (23 * e_t - 16 * old_eps[-1] + 5 * old_eps[-2]) / 12
|
||||
elif len(old_eps) >= 3:
|
||||
# 4nd order Pseudo Linear Multistep (Adams-Bashforth)
|
||||
e_t_prime = (55 * e_t - 59 * old_eps[-1] + 37 * old_eps[-2] - 9 * old_eps[-3]) / 24
|
||||
|
||||
x_prev, pred_x0 = get_x_prev_and_pred_x0(e_t_prime, index)
|
||||
|
||||
return x_prev, pred_x0, e_t
|
||||
0
iopaint/model/power_paint/__init__.py
Normal file
1243
iopaint/model/power_paint/pipeline_powerpaint.py
Normal file
1775
iopaint/model/power_paint/pipeline_powerpaint_controlnet.py
Normal file
96
iopaint/model/power_paint/power_paint.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from PIL import Image
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.model.helper.cpu_text_encoder import CPUTextEncoderWrapper
|
||||
from iopaint.model.utils import handle_from_pretrained_exceptions
|
||||
from iopaint.schema import InpaintRequest
|
||||
from .powerpaint_tokenizer import add_task_to_prompt
|
||||
|
||||
|
||||
class PowerPaint(DiffusionInpaintModel):
|
||||
name = "Sanster/PowerPaint-V1-stable-diffusion-inpainting"
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
lcm_lora_id = "latent-consistency/lcm-lora-sdv1-5"
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from .pipeline_powerpaint import StableDiffusionInpaintPipeline
|
||||
from .powerpaint_tokenizer import PowerPaintTokenizer
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
model_kwargs = {}
|
||||
if kwargs["disable_nsfw"] or kwargs.get("cpu_offload", False):
|
||||
logger.info("Disable Stable Diffusion Model NSFW checker")
|
||||
model_kwargs.update(
|
||||
dict(
|
||||
safety_checker=None,
|
||||
feature_extractor=None,
|
||||
requires_safety_checker=False,
|
||||
)
|
||||
)
|
||||
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
|
||||
self.model = handle_from_pretrained_exceptions(
|
||||
StableDiffusionInpaintPipeline.from_pretrained,
|
||||
pretrained_model_name_or_path=self.name,
|
||||
variant="fp16",
|
||||
torch_dtype=torch_dtype,
|
||||
**model_kwargs,
|
||||
)
|
||||
self.model.tokenizer = PowerPaintTokenizer(self.model.tokenizer)
|
||||
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
logger.info("Enable sequential cpu offload")
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
if kwargs["sd_cpu_textencoder"]:
|
||||
logger.info("Run Stable Diffusion TextEncoder on CPU")
|
||||
self.model.text_encoder = CPUTextEncoderWrapper(
|
||||
self.model.text_encoder, torch_dtype
|
||||
)
|
||||
|
||||
self.callback = kwargs.pop("callback", None)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
self.set_scheduler(config)
|
||||
|
||||
img_h, img_w = image.shape[:2]
|
||||
promptA, promptB, negative_promptA, negative_promptB = add_task_to_prompt(
|
||||
config.prompt, config.negative_prompt, config.powerpaint_task
|
||||
)
|
||||
|
||||
output = self.model(
|
||||
image=PIL.Image.fromarray(image),
|
||||
promptA=promptA,
|
||||
promptB=promptB,
|
||||
tradoff=config.fitting_degree,
|
||||
tradoff_nag=config.fitting_degree,
|
||||
negative_promptA=negative_promptA,
|
||||
negative_promptB=negative_promptB,
|
||||
mask_image=PIL.Image.fromarray(mask[:, :, -1], mode="L"),
|
||||
num_inference_steps=config.sd_steps,
|
||||
strength=config.sd_strength,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
callback=self.callback,
|
||||
height=img_h,
|
||||
width=img_w,
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
callback_steps=1,
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
540
iopaint/model/power_paint/powerpaint_tokenizer.py
Normal file
@@ -0,0 +1,540 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import copy
|
||||
import random
|
||||
from typing import Any, List, Optional, Union
|
||||
from transformers import CLIPTokenizer
|
||||
|
||||
from iopaint.schema import PowerPaintTask
|
||||
|
||||
|
||||
def add_task_to_prompt(prompt, negative_prompt, task: PowerPaintTask):
|
||||
if task == PowerPaintTask.object_remove:
|
||||
promptA = prompt + " P_ctxt"
|
||||
promptB = prompt + " P_ctxt"
|
||||
negative_promptA = negative_prompt + " P_obj"
|
||||
negative_promptB = negative_prompt + " P_obj"
|
||||
elif task == PowerPaintTask.shape_guided:
|
||||
promptA = prompt + " P_shape"
|
||||
promptB = prompt + " P_ctxt"
|
||||
negative_promptA = negative_prompt
|
||||
negative_promptB = negative_prompt
|
||||
elif task == PowerPaintTask.outpainting:
|
||||
promptA = prompt + " P_ctxt"
|
||||
promptB = prompt + " P_ctxt"
|
||||
negative_promptA = negative_prompt + " P_obj"
|
||||
negative_promptB = negative_prompt + " P_obj"
|
||||
else:
|
||||
promptA = prompt + " P_obj"
|
||||
promptB = prompt + " P_obj"
|
||||
negative_promptA = negative_prompt
|
||||
negative_promptB = negative_prompt
|
||||
|
||||
return promptA, promptB, negative_promptA, negative_promptB
|
||||
|
||||
|
||||
class PowerPaintTokenizer:
|
||||
def __init__(self, tokenizer: CLIPTokenizer):
|
||||
self.wrapped = tokenizer
|
||||
self.token_map = {}
|
||||
placeholder_tokens = ["P_ctxt", "P_shape", "P_obj"]
|
||||
num_vec_per_token = 10
|
||||
for placeholder_token in placeholder_tokens:
|
||||
output = []
|
||||
for i in range(num_vec_per_token):
|
||||
ith_token = placeholder_token + f"_{i}"
|
||||
output.append(ith_token)
|
||||
self.token_map[placeholder_token] = output
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name == "wrapped":
|
||||
return super().__getattr__("wrapped")
|
||||
|
||||
try:
|
||||
return getattr(self.wrapped, name)
|
||||
except AttributeError:
|
||||
try:
|
||||
return super().__getattr__(name)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
"'name' cannot be found in both "
|
||||
f"'{self.__class__.__name__}' and "
|
||||
f"'{self.__class__.__name__}.tokenizer'."
|
||||
)
|
||||
|
||||
def try_adding_tokens(self, tokens: Union[str, List[str]], *args, **kwargs):
|
||||
"""Attempt to add tokens to the tokenizer.
|
||||
|
||||
Args:
|
||||
tokens (Union[str, List[str]]): The tokens to be added.
|
||||
"""
|
||||
num_added_tokens = self.wrapped.add_tokens(tokens, *args, **kwargs)
|
||||
assert num_added_tokens != 0, (
|
||||
f"The tokenizer already contains the token {tokens}. Please pass "
|
||||
"a different `placeholder_token` that is not already in the "
|
||||
"tokenizer."
|
||||
)
|
||||
|
||||
def get_token_info(self, token: str) -> dict:
|
||||
"""Get the information of a token, including its start and end index in
|
||||
the current tokenizer.
|
||||
|
||||
Args:
|
||||
token (str): The token to be queried.
|
||||
|
||||
Returns:
|
||||
dict: The information of the token, including its start and end
|
||||
index in current tokenizer.
|
||||
"""
|
||||
token_ids = self.__call__(token).input_ids
|
||||
start, end = token_ids[1], token_ids[-2] + 1
|
||||
return {"name": token, "start": start, "end": end}
|
||||
|
||||
def add_placeholder_token(
|
||||
self, placeholder_token: str, *args, num_vec_per_token: int = 1, **kwargs
|
||||
):
|
||||
"""Add placeholder tokens to the tokenizer.
|
||||
|
||||
Args:
|
||||
placeholder_token (str): The placeholder token to be added.
|
||||
num_vec_per_token (int, optional): The number of vectors of
|
||||
the added placeholder token.
|
||||
*args, **kwargs: The arguments for `self.wrapped.add_tokens`.
|
||||
"""
|
||||
output = []
|
||||
if num_vec_per_token == 1:
|
||||
self.try_adding_tokens(placeholder_token, *args, **kwargs)
|
||||
output.append(placeholder_token)
|
||||
else:
|
||||
output = []
|
||||
for i in range(num_vec_per_token):
|
||||
ith_token = placeholder_token + f"_{i}"
|
||||
self.try_adding_tokens(ith_token, *args, **kwargs)
|
||||
output.append(ith_token)
|
||||
|
||||
for token in self.token_map:
|
||||
if token in placeholder_token:
|
||||
raise ValueError(
|
||||
f"The tokenizer already has placeholder token {token} "
|
||||
f"that can get confused with {placeholder_token} "
|
||||
"keep placeholder tokens independent"
|
||||
)
|
||||
self.token_map[placeholder_token] = output
|
||||
|
||||
def replace_placeholder_tokens_in_text(
|
||||
self,
|
||||
text: Union[str, List[str]],
|
||||
vector_shuffle: bool = False,
|
||||
prop_tokens_to_load: float = 1.0,
|
||||
) -> Union[str, List[str]]:
|
||||
"""Replace the keywords in text with placeholder tokens. This function
|
||||
will be called in `self.__call__` and `self.encode`.
|
||||
|
||||
Args:
|
||||
text (Union[str, List[str]]): The text to be processed.
|
||||
vector_shuffle (bool, optional): Whether to shuffle the vectors.
|
||||
Defaults to False.
|
||||
prop_tokens_to_load (float, optional): The proportion of tokens to
|
||||
be loaded. If 1.0, all tokens will be loaded. Defaults to 1.0.
|
||||
|
||||
Returns:
|
||||
Union[str, List[str]]: The processed text.
|
||||
"""
|
||||
if isinstance(text, list):
|
||||
output = []
|
||||
for i in range(len(text)):
|
||||
output.append(
|
||||
self.replace_placeholder_tokens_in_text(
|
||||
text[i], vector_shuffle=vector_shuffle
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
for placeholder_token in self.token_map:
|
||||
if placeholder_token in text:
|
||||
tokens = self.token_map[placeholder_token]
|
||||
tokens = tokens[: 1 + int(len(tokens) * prop_tokens_to_load)]
|
||||
if vector_shuffle:
|
||||
tokens = copy.copy(tokens)
|
||||
random.shuffle(tokens)
|
||||
text = text.replace(placeholder_token, " ".join(tokens))
|
||||
return text
|
||||
|
||||
def replace_text_with_placeholder_tokens(
|
||||
self, text: Union[str, List[str]]
|
||||
) -> Union[str, List[str]]:
|
||||
"""Replace the placeholder tokens in text with the original keywords.
|
||||
This function will be called in `self.decode`.
|
||||
|
||||
Args:
|
||||
text (Union[str, List[str]]): The text to be processed.
|
||||
|
||||
Returns:
|
||||
Union[str, List[str]]: The processed text.
|
||||
"""
|
||||
if isinstance(text, list):
|
||||
output = []
|
||||
for i in range(len(text)):
|
||||
output.append(self.replace_text_with_placeholder_tokens(text[i]))
|
||||
return output
|
||||
|
||||
for placeholder_token, tokens in self.token_map.items():
|
||||
merged_tokens = " ".join(tokens)
|
||||
if merged_tokens in text:
|
||||
text = text.replace(merged_tokens, placeholder_token)
|
||||
return text
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
text: Union[str, List[str]],
|
||||
*args,
|
||||
vector_shuffle: bool = False,
|
||||
prop_tokens_to_load: float = 1.0,
|
||||
**kwargs,
|
||||
):
|
||||
"""The call function of the wrapper.
|
||||
|
||||
Args:
|
||||
text (Union[str, List[str]]): The text to be tokenized.
|
||||
vector_shuffle (bool, optional): Whether to shuffle the vectors.
|
||||
Defaults to False.
|
||||
prop_tokens_to_load (float, optional): The proportion of tokens to
|
||||
be loaded. If 1.0, all tokens will be loaded. Defaults to 1.0
|
||||
*args, **kwargs: The arguments for `self.wrapped.__call__`.
|
||||
"""
|
||||
replaced_text = self.replace_placeholder_tokens_in_text(
|
||||
text, vector_shuffle=vector_shuffle, prop_tokens_to_load=prop_tokens_to_load
|
||||
)
|
||||
|
||||
return self.wrapped.__call__(replaced_text, *args, **kwargs)
|
||||
|
||||
def encode(self, text: Union[str, List[str]], *args, **kwargs):
|
||||
"""Encode the passed text to token index.
|
||||
|
||||
Args:
|
||||
text (Union[str, List[str]]): The text to be encode.
|
||||
*args, **kwargs: The arguments for `self.wrapped.__call__`.
|
||||
"""
|
||||
replaced_text = self.replace_placeholder_tokens_in_text(text)
|
||||
return self.wrapped(replaced_text, *args, **kwargs)
|
||||
|
||||
def decode(
|
||||
self, token_ids, return_raw: bool = False, *args, **kwargs
|
||||
) -> Union[str, List[str]]:
|
||||
"""Decode the token index to text.
|
||||
|
||||
Args:
|
||||
token_ids: The token index to be decoded.
|
||||
return_raw: Whether keep the placeholder token in the text.
|
||||
Defaults to False.
|
||||
*args, **kwargs: The arguments for `self.wrapped.decode`.
|
||||
|
||||
Returns:
|
||||
Union[str, List[str]]: The decoded text.
|
||||
"""
|
||||
text = self.wrapped.decode(token_ids, *args, **kwargs)
|
||||
if return_raw:
|
||||
return text
|
||||
replaced_text = self.replace_text_with_placeholder_tokens(text)
|
||||
return replaced_text
|
||||
|
||||
|
||||
class EmbeddingLayerWithFixes(nn.Module):
|
||||
"""The revised embedding layer to support external embeddings. This design
|
||||
of this class is inspired by https://github.com/AUTOMATIC1111/stable-
|
||||
diffusion-webui/blob/22bcc7be428c94e9408f589966c2040187245d81/modules/sd_hi
|
||||
jack.py#L224 # noqa.
|
||||
|
||||
Args:
|
||||
wrapped (nn.Emebdding): The embedding layer to be wrapped.
|
||||
external_embeddings (Union[dict, List[dict]], optional): The external
|
||||
embeddings added to this layer. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wrapped: nn.Embedding,
|
||||
external_embeddings: Optional[Union[dict, List[dict]]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.wrapped = wrapped
|
||||
self.num_embeddings = wrapped.weight.shape[0]
|
||||
|
||||
self.external_embeddings = []
|
||||
if external_embeddings:
|
||||
self.add_embeddings(external_embeddings)
|
||||
|
||||
self.trainable_embeddings = nn.ParameterDict()
|
||||
|
||||
@property
|
||||
def weight(self):
|
||||
"""Get the weight of wrapped embedding layer."""
|
||||
return self.wrapped.weight
|
||||
|
||||
def check_duplicate_names(self, embeddings: List[dict]):
|
||||
"""Check whether duplicate names exist in list of 'external
|
||||
embeddings'.
|
||||
|
||||
Args:
|
||||
embeddings (List[dict]): A list of embedding to be check.
|
||||
"""
|
||||
names = [emb["name"] for emb in embeddings]
|
||||
assert len(names) == len(set(names)), (
|
||||
"Found duplicated names in 'external_embeddings'. Name list: " f"'{names}'"
|
||||
)
|
||||
|
||||
def check_ids_overlap(self, embeddings):
|
||||
"""Check whether overlap exist in token ids of 'external_embeddings'.
|
||||
|
||||
Args:
|
||||
embeddings (List[dict]): A list of embedding to be check.
|
||||
"""
|
||||
ids_range = [[emb["start"], emb["end"], emb["name"]] for emb in embeddings]
|
||||
ids_range.sort() # sort by 'start'
|
||||
# check if 'end' has overlapping
|
||||
for idx in range(len(ids_range) - 1):
|
||||
name1, name2 = ids_range[idx][-1], ids_range[idx + 1][-1]
|
||||
assert ids_range[idx][1] <= ids_range[idx + 1][0], (
|
||||
f"Found ids overlapping between embeddings '{name1}' " f"and '{name2}'."
|
||||
)
|
||||
|
||||
def add_embeddings(self, embeddings: Optional[Union[dict, List[dict]]]):
|
||||
"""Add external embeddings to this layer.
|
||||
|
||||
Use case:
|
||||
|
||||
>>> 1. Add token to tokenizer and get the token id.
|
||||
>>> tokenizer = TokenizerWrapper('openai/clip-vit-base-patch32')
|
||||
>>> # 'how much' in kiswahili
|
||||
>>> tokenizer.add_placeholder_tokens('ngapi', num_vec_per_token=4)
|
||||
>>>
|
||||
>>> 2. Add external embeddings to the model.
|
||||
>>> new_embedding = {
|
||||
>>> 'name': 'ngapi', # 'how much' in kiswahili
|
||||
>>> 'embedding': torch.ones(1, 15) * 4,
|
||||
>>> 'start': tokenizer.get_token_info('kwaheri')['start'],
|
||||
>>> 'end': tokenizer.get_token_info('kwaheri')['end'],
|
||||
>>> 'trainable': False # if True, will registry as a parameter
|
||||
>>> }
|
||||
>>> embedding_layer = nn.Embedding(10, 15)
|
||||
>>> embedding_layer_wrapper = EmbeddingLayerWithFixes(embedding_layer)
|
||||
>>> embedding_layer_wrapper.add_embeddings(new_embedding)
|
||||
>>>
|
||||
>>> 3. Forward tokenizer and embedding layer!
|
||||
>>> input_text = ['hello, ngapi!', 'hello my friend, ngapi?']
|
||||
>>> input_ids = tokenizer(
|
||||
>>> input_text, padding='max_length', truncation=True,
|
||||
>>> return_tensors='pt')['input_ids']
|
||||
>>> out_feat = embedding_layer_wrapper(input_ids)
|
||||
>>>
|
||||
>>> 4. Let's validate the result!
|
||||
>>> assert (out_feat[0, 3: 7] == 2.3).all()
|
||||
>>> assert (out_feat[2, 5: 9] == 2.3).all()
|
||||
|
||||
Args:
|
||||
embeddings (Union[dict, list[dict]]): The external embeddings to
|
||||
be added. Each dict must contain the following 4 fields: 'name'
|
||||
(the name of this embedding), 'embedding' (the embedding
|
||||
tensor), 'start' (the start token id of this embedding), 'end'
|
||||
(the end token id of this embedding). For example:
|
||||
`{name: NAME, start: START, end: END, embedding: torch.Tensor}`
|
||||
"""
|
||||
if isinstance(embeddings, dict):
|
||||
embeddings = [embeddings]
|
||||
|
||||
self.external_embeddings += embeddings
|
||||
self.check_duplicate_names(self.external_embeddings)
|
||||
self.check_ids_overlap(self.external_embeddings)
|
||||
|
||||
# set for trainable
|
||||
added_trainable_emb_info = []
|
||||
for embedding in embeddings:
|
||||
trainable = embedding.get("trainable", False)
|
||||
if trainable:
|
||||
name = embedding["name"]
|
||||
embedding["embedding"] = torch.nn.Parameter(embedding["embedding"])
|
||||
self.trainable_embeddings[name] = embedding["embedding"]
|
||||
added_trainable_emb_info.append(name)
|
||||
|
||||
added_emb_info = [emb["name"] for emb in embeddings]
|
||||
added_emb_info = ", ".join(added_emb_info)
|
||||
print(f"Successfully add external embeddings: {added_emb_info}.", "current")
|
||||
|
||||
if added_trainable_emb_info:
|
||||
added_trainable_emb_info = ", ".join(added_trainable_emb_info)
|
||||
print(
|
||||
"Successfully add trainable external embeddings: "
|
||||
f"{added_trainable_emb_info}",
|
||||
"current",
|
||||
)
|
||||
|
||||
def replace_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
"""Replace external input ids to 0.
|
||||
|
||||
Args:
|
||||
input_ids (torch.Tensor): The input ids to be replaced.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The replaced input ids.
|
||||
"""
|
||||
input_ids_fwd = input_ids.clone()
|
||||
input_ids_fwd[input_ids_fwd >= self.num_embeddings] = 0
|
||||
return input_ids_fwd
|
||||
|
||||
def replace_embeddings(
|
||||
self, input_ids: torch.Tensor, embedding: torch.Tensor, external_embedding: dict
|
||||
) -> torch.Tensor:
|
||||
"""Replace external embedding to the embedding layer. Noted that, in
|
||||
this function we use `torch.cat` to avoid inplace modification.
|
||||
|
||||
Args:
|
||||
input_ids (torch.Tensor): The original token ids. Shape like
|
||||
[LENGTH, ].
|
||||
embedding (torch.Tensor): The embedding of token ids after
|
||||
`replace_input_ids` function.
|
||||
external_embedding (dict): The external embedding to be replaced.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The replaced embedding.
|
||||
"""
|
||||
new_embedding = []
|
||||
|
||||
name = external_embedding["name"]
|
||||
start = external_embedding["start"]
|
||||
end = external_embedding["end"]
|
||||
target_ids_to_replace = [i for i in range(start, end)]
|
||||
ext_emb = external_embedding["embedding"]
|
||||
|
||||
# do not need to replace
|
||||
if not (input_ids == start).any():
|
||||
return embedding
|
||||
|
||||
# start replace
|
||||
s_idx, e_idx = 0, 0
|
||||
while e_idx < len(input_ids):
|
||||
if input_ids[e_idx] == start:
|
||||
if e_idx != 0:
|
||||
# add embedding do not need to replace
|
||||
new_embedding.append(embedding[s_idx:e_idx])
|
||||
|
||||
# check if the next embedding need to replace is valid
|
||||
actually_ids_to_replace = [
|
||||
int(i) for i in input_ids[e_idx : e_idx + end - start]
|
||||
]
|
||||
assert actually_ids_to_replace == target_ids_to_replace, (
|
||||
f"Invalid 'input_ids' in position: {s_idx} to {e_idx}. "
|
||||
f"Expect '{target_ids_to_replace}' for embedding "
|
||||
f"'{name}' but found '{actually_ids_to_replace}'."
|
||||
)
|
||||
|
||||
new_embedding.append(ext_emb)
|
||||
|
||||
s_idx = e_idx + end - start
|
||||
e_idx = s_idx + 1
|
||||
else:
|
||||
e_idx += 1
|
||||
|
||||
if e_idx == len(input_ids):
|
||||
new_embedding.append(embedding[s_idx:e_idx])
|
||||
|
||||
return torch.cat(new_embedding, dim=0)
|
||||
|
||||
def forward(
|
||||
self, input_ids: torch.Tensor, external_embeddings: Optional[List[dict]] = None
|
||||
):
|
||||
"""The forward function.
|
||||
|
||||
Args:
|
||||
input_ids (torch.Tensor): The token ids shape like [bz, LENGTH] or
|
||||
[LENGTH, ].
|
||||
external_embeddings (Optional[List[dict]]): The external
|
||||
embeddings. If not passed, only `self.external_embeddings`
|
||||
will be used. Defaults to None.
|
||||
|
||||
input_ids: shape like [bz, LENGTH] or [LENGTH].
|
||||
"""
|
||||
assert input_ids.ndim in [1, 2]
|
||||
if input_ids.ndim == 1:
|
||||
input_ids = input_ids.unsqueeze(0)
|
||||
|
||||
if external_embeddings is None and not self.external_embeddings:
|
||||
return self.wrapped(input_ids)
|
||||
|
||||
input_ids_fwd = self.replace_input_ids(input_ids)
|
||||
inputs_embeds = self.wrapped(input_ids_fwd)
|
||||
|
||||
vecs = []
|
||||
|
||||
if external_embeddings is None:
|
||||
external_embeddings = []
|
||||
elif isinstance(external_embeddings, dict):
|
||||
external_embeddings = [external_embeddings]
|
||||
embeddings = self.external_embeddings + external_embeddings
|
||||
|
||||
for input_id, embedding in zip(input_ids, inputs_embeds):
|
||||
new_embedding = embedding
|
||||
for external_embedding in embeddings:
|
||||
new_embedding = self.replace_embeddings(
|
||||
input_id, new_embedding, external_embedding
|
||||
)
|
||||
vecs.append(new_embedding)
|
||||
|
||||
return torch.stack(vecs)
|
||||
|
||||
|
||||
def add_tokens(
|
||||
tokenizer,
|
||||
text_encoder,
|
||||
placeholder_tokens: list,
|
||||
initialize_tokens: list = None,
|
||||
num_vectors_per_token: int = 1,
|
||||
):
|
||||
"""Add token for training.
|
||||
|
||||
# TODO: support add tokens as dict, then we can load pretrained tokens.
|
||||
"""
|
||||
if initialize_tokens is not None:
|
||||
assert len(initialize_tokens) == len(
|
||||
placeholder_tokens
|
||||
), "placeholder_token should be the same length as initialize_token"
|
||||
for ii in range(len(placeholder_tokens)):
|
||||
tokenizer.add_placeholder_token(
|
||||
placeholder_tokens[ii], num_vec_per_token=num_vectors_per_token
|
||||
)
|
||||
|
||||
# text_encoder.set_embedding_layer()
|
||||
embedding_layer = text_encoder.text_model.embeddings.token_embedding
|
||||
text_encoder.text_model.embeddings.token_embedding = EmbeddingLayerWithFixes(
|
||||
embedding_layer
|
||||
)
|
||||
embedding_layer = text_encoder.text_model.embeddings.token_embedding
|
||||
|
||||
assert embedding_layer is not None, (
|
||||
"Do not support get embedding layer for current text encoder. "
|
||||
"Please check your configuration."
|
||||
)
|
||||
initialize_embedding = []
|
||||
if initialize_tokens is not None:
|
||||
for ii in range(len(placeholder_tokens)):
|
||||
init_id = tokenizer(initialize_tokens[ii]).input_ids[1]
|
||||
temp_embedding = embedding_layer.weight[init_id]
|
||||
initialize_embedding.append(
|
||||
temp_embedding[None, ...].repeat(num_vectors_per_token, 1)
|
||||
)
|
||||
else:
|
||||
for ii in range(len(placeholder_tokens)):
|
||||
init_id = tokenizer("a").input_ids[1]
|
||||
temp_embedding = embedding_layer.weight[init_id]
|
||||
len_emb = temp_embedding.shape[0]
|
||||
init_weight = (torch.rand(num_vectors_per_token, len_emb) - 0.5) / 2.0
|
||||
initialize_embedding.append(init_weight)
|
||||
|
||||
# initialize_embedding = torch.cat(initialize_embedding,dim=0)
|
||||
|
||||
token_info_all = []
|
||||
for ii in range(len(placeholder_tokens)):
|
||||
token_info = tokenizer.get_token_info(placeholder_tokens[ii])
|
||||
token_info["embedding"] = initialize_embedding[ii]
|
||||
token_info["trainable"] = True
|
||||
token_info_all.append(token_info)
|
||||
embedding_layer.add_embeddings(token_info_all)
|
||||
114
iopaint/model/sd.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.model.helper.cpu_text_encoder import CPUTextEncoderWrapper
|
||||
from iopaint.model.utils import handle_from_pretrained_exceptions
|
||||
from iopaint.schema import InpaintRequest, ModelType
|
||||
|
||||
|
||||
class SD(DiffusionInpaintModel):
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
lcm_lora_id = "latent-consistency/lcm-lora-sdv1-5"
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from diffusers.pipelines.stable_diffusion import StableDiffusionInpaintPipeline
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
|
||||
model_kwargs = {}
|
||||
if kwargs["disable_nsfw"] or kwargs.get("cpu_offload", False):
|
||||
logger.info("Disable Stable Diffusion Model NSFW checker")
|
||||
model_kwargs.update(
|
||||
dict(
|
||||
safety_checker=None,
|
||||
feature_extractor=None,
|
||||
requires_safety_checker=False,
|
||||
)
|
||||
)
|
||||
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
|
||||
if self.model_info.is_single_file_diffusers:
|
||||
if self.model_info.model_type == ModelType.DIFFUSERS_SD:
|
||||
model_kwargs["num_in_channels"] = 4
|
||||
else:
|
||||
model_kwargs["num_in_channels"] = 9
|
||||
|
||||
self.model = StableDiffusionInpaintPipeline.from_single_file(
|
||||
self.model_id_or_path, dtype=torch_dtype, **model_kwargs
|
||||
)
|
||||
else:
|
||||
self.model = handle_from_pretrained_exceptions(
|
||||
StableDiffusionInpaintPipeline.from_pretrained,
|
||||
pretrained_model_name_or_path=self.model_id_or_path,
|
||||
variant="fp16",
|
||||
dtype=torch_dtype,
|
||||
**model_kwargs,
|
||||
)
|
||||
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
logger.info("Enable sequential cpu offload")
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
if kwargs["sd_cpu_textencoder"]:
|
||||
logger.info("Run Stable Diffusion TextEncoder on CPU")
|
||||
self.model.text_encoder = CPUTextEncoderWrapper(
|
||||
self.model.text_encoder, torch_dtype
|
||||
)
|
||||
|
||||
self.callback = kwargs.pop("callback", None)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
self.set_scheduler(config)
|
||||
|
||||
img_h, img_w = image.shape[:2]
|
||||
|
||||
output = self.model(
|
||||
image=PIL.Image.fromarray(image),
|
||||
prompt=config.prompt,
|
||||
negative_prompt=config.negative_prompt,
|
||||
mask_image=PIL.Image.fromarray(mask[:, :, -1], mode="L"),
|
||||
num_inference_steps=config.sd_steps,
|
||||
strength=config.sd_strength,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
callback_on_step_end=self.callback,
|
||||
height=img_h,
|
||||
width=img_w,
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
|
||||
|
||||
class SD15(SD):
|
||||
name = "runwayml/stable-diffusion-inpainting"
|
||||
model_id_or_path = "runwayml/stable-diffusion-inpainting"
|
||||
|
||||
|
||||
class Anything4(SD):
|
||||
name = "Sanster/anything-4.0-inpainting"
|
||||
model_id_or_path = "Sanster/anything-4.0-inpainting"
|
||||
|
||||
|
||||
class RealisticVision14(SD):
|
||||
name = "Sanster/Realistic_Vision_V1.4-inpainting"
|
||||
model_id_or_path = "Sanster/Realistic_Vision_V1.4-inpainting"
|
||||
|
||||
|
||||
class SD2(SD):
|
||||
name = "stabilityai/stable-diffusion-2-inpainting"
|
||||
model_id_or_path = "stabilityai/stable-diffusion-2-inpainting"
|
||||
89
iopaint/model/sdxl.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import os
|
||||
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import torch
|
||||
from diffusers import AutoencoderKL
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.model.base import DiffusionInpaintModel
|
||||
from iopaint.model.utils import handle_from_pretrained_exceptions
|
||||
from iopaint.schema import InpaintRequest, ModelType
|
||||
|
||||
|
||||
class SDXL(DiffusionInpaintModel):
|
||||
name = "diffusers/stable-diffusion-xl-1.0-inpainting-0.1"
|
||||
pad_mod = 8
|
||||
min_size = 512
|
||||
lcm_lora_id = "latent-consistency/lcm-lora-sdxl"
|
||||
model_id_or_path = "diffusers/stable-diffusion-xl-1.0-inpainting-0.1"
|
||||
|
||||
def init_model(self, device: torch.device, **kwargs):
|
||||
from diffusers.pipelines import StableDiffusionXLInpaintPipeline
|
||||
|
||||
fp16 = not kwargs.get("no_half", False)
|
||||
|
||||
use_gpu = device == torch.device("cuda") and torch.cuda.is_available()
|
||||
torch_dtype = torch.float16 if use_gpu and fp16 else torch.float32
|
||||
|
||||
if self.model_info.model_type == ModelType.DIFFUSERS_SDXL:
|
||||
num_in_channels = 4
|
||||
else:
|
||||
num_in_channels = 9
|
||||
|
||||
if os.path.isfile(self.model_id_or_path):
|
||||
self.model = StableDiffusionXLInpaintPipeline.from_single_file(
|
||||
self.model_id_or_path,
|
||||
dtype=torch_dtype,
|
||||
num_in_channels=num_in_channels,
|
||||
)
|
||||
else:
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
"madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch_dtype
|
||||
)
|
||||
self.model = handle_from_pretrained_exceptions(
|
||||
StableDiffusionXLInpaintPipeline.from_pretrained,
|
||||
pretrained_model_name_or_path=self.model_id_or_path,
|
||||
torch_dtype=torch_dtype,
|
||||
vae=vae,
|
||||
variant="fp16",
|
||||
)
|
||||
|
||||
if kwargs.get("cpu_offload", False) and use_gpu:
|
||||
logger.info("Enable sequential cpu offload")
|
||||
self.model.enable_sequential_cpu_offload(gpu_id=0)
|
||||
else:
|
||||
self.model = self.model.to(device)
|
||||
if kwargs["sd_cpu_textencoder"]:
|
||||
logger.warning("Stable Diffusion XL not support run TextEncoder on CPU")
|
||||
|
||||
self.callback = kwargs.pop("callback", None)
|
||||
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input image and output image have same size
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
self.set_scheduler(config)
|
||||
|
||||
img_h, img_w = image.shape[:2]
|
||||
|
||||
output = self.model(
|
||||
image=PIL.Image.fromarray(image),
|
||||
prompt=config.prompt,
|
||||
negative_prompt=config.negative_prompt,
|
||||
mask_image=PIL.Image.fromarray(mask[:, :, -1], mode="L"),
|
||||
num_inference_steps=config.sd_steps,
|
||||
strength=0.999 if config.sd_strength == 1.0 else config.sd_strength,
|
||||
guidance_scale=config.sd_guidance_scale,
|
||||
output_type="np",
|
||||
callback_on_step_end=self.callback,
|
||||
height=img_h,
|
||||
width=img_w,
|
||||
generator=torch.manual_seed(config.sd_seed),
|
||||
).images[0]
|
||||
|
||||
output = (output * 255).round().astype("uint8")
|
||||
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
||||
return output
|
||||
989
iopaint/model/utils.py
Normal file
@@ -0,0 +1,989 @@
|
||||
import copy
|
||||
import gc
|
||||
import math
|
||||
import random
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import collections
|
||||
from itertools import repeat
|
||||
|
||||
from diffusers import (
|
||||
DDIMScheduler,
|
||||
PNDMScheduler,
|
||||
LMSDiscreteScheduler,
|
||||
EulerDiscreteScheduler,
|
||||
EulerAncestralDiscreteScheduler,
|
||||
DPMSolverMultistepScheduler,
|
||||
UniPCMultistepScheduler,
|
||||
LCMScheduler,
|
||||
DPMSolverSinglestepScheduler,
|
||||
KDPM2DiscreteScheduler,
|
||||
KDPM2AncestralDiscreteScheduler,
|
||||
HeunDiscreteScheduler,
|
||||
)
|
||||
from diffusers.configuration_utils import FrozenDict
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.schema import SDSampler
|
||||
from torch import conv2d, conv_transpose2d
|
||||
|
||||
|
||||
def make_beta_schedule(
|
||||
device, schedule, n_timestep, linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3
|
||||
):
|
||||
if schedule == "linear":
|
||||
betas = (
|
||||
torch.linspace(
|
||||
linear_start**0.5, linear_end**0.5, n_timestep, dtype=torch.float64
|
||||
)
|
||||
** 2
|
||||
)
|
||||
|
||||
elif schedule == "cosine":
|
||||
timesteps = (
|
||||
torch.arange(n_timestep + 1, dtype=torch.float64) / n_timestep + cosine_s
|
||||
).to(device)
|
||||
alphas = timesteps / (1 + cosine_s) * np.pi / 2
|
||||
alphas = torch.cos(alphas).pow(2).to(device)
|
||||
alphas = alphas / alphas[0]
|
||||
betas = 1 - alphas[1:] / alphas[:-1]
|
||||
betas = np.clip(betas, a_min=0, a_max=0.999)
|
||||
|
||||
elif schedule == "sqrt_linear":
|
||||
betas = torch.linspace(
|
||||
linear_start, linear_end, n_timestep, dtype=torch.float64
|
||||
)
|
||||
elif schedule == "sqrt":
|
||||
betas = (
|
||||
torch.linspace(linear_start, linear_end, n_timestep, dtype=torch.float64)
|
||||
** 0.5
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"schedule '{schedule}' unknown.")
|
||||
return betas.numpy()
|
||||
|
||||
|
||||
def make_ddim_sampling_parameters(alphacums, ddim_timesteps, eta, verbose=True):
|
||||
# select alphas for computing the variance schedule
|
||||
alphas = alphacums[ddim_timesteps]
|
||||
alphas_prev = np.asarray([alphacums[0]] + alphacums[ddim_timesteps[:-1]].tolist())
|
||||
|
||||
# according the the formula provided in https://arxiv.org/abs/2010.02502
|
||||
sigmas = eta * np.sqrt(
|
||||
(1 - alphas_prev) / (1 - alphas) * (1 - alphas / alphas_prev)
|
||||
)
|
||||
if verbose:
|
||||
print(
|
||||
f"Selected alphas for ddim sampler: a_t: {alphas}; a_(t-1): {alphas_prev}"
|
||||
)
|
||||
print(
|
||||
f"For the chosen value of eta, which is {eta}, "
|
||||
f"this results in the following sigma_t schedule for ddim sampler {sigmas}"
|
||||
)
|
||||
return sigmas, alphas, alphas_prev
|
||||
|
||||
|
||||
def make_ddim_timesteps(
|
||||
ddim_discr_method, num_ddim_timesteps, num_ddpm_timesteps, verbose=True
|
||||
):
|
||||
if ddim_discr_method == "uniform":
|
||||
c = num_ddpm_timesteps // num_ddim_timesteps
|
||||
ddim_timesteps = np.asarray(list(range(0, num_ddpm_timesteps, c)))
|
||||
elif ddim_discr_method == "quad":
|
||||
ddim_timesteps = (
|
||||
(np.linspace(0, np.sqrt(num_ddpm_timesteps * 0.8), num_ddim_timesteps)) ** 2
|
||||
).astype(int)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'There is no ddim discretization method called "{ddim_discr_method}"'
|
||||
)
|
||||
|
||||
# assert ddim_timesteps.shape[0] == num_ddim_timesteps
|
||||
# add one to get the final alpha values right (the ones from first scale to data during sampling)
|
||||
steps_out = ddim_timesteps + 1
|
||||
if verbose:
|
||||
print(f"Selected timesteps for ddim sampler: {steps_out}")
|
||||
return steps_out
|
||||
|
||||
|
||||
def noise_like(shape, device, repeat=False):
|
||||
repeat_noise = lambda: torch.randn((1, *shape[1:]), device=device).repeat(
|
||||
shape[0], *((1,) * (len(shape) - 1))
|
||||
)
|
||||
noise = lambda: torch.randn(shape, device=device)
|
||||
return repeat_noise() if repeat else noise()
|
||||
|
||||
|
||||
def timestep_embedding(device, timesteps, dim, max_period=10000, repeat_only=False):
|
||||
"""
|
||||
Create sinusoidal timestep embeddings.
|
||||
:param timesteps: a 1-D Tensor of N indices, one per batch element.
|
||||
These may be fractional.
|
||||
:param dim: the dimension of the output.
|
||||
:param max_period: controls the minimum frequency of the embeddings.
|
||||
:return: an [N x dim] Tensor of positional embeddings.
|
||||
"""
|
||||
half = dim // 2
|
||||
freqs = torch.exp(
|
||||
-math.log(max_period)
|
||||
* torch.arange(start=0, end=half, dtype=torch.float32)
|
||||
/ half
|
||||
).to(device=device)
|
||||
|
||||
args = timesteps[:, None].float() * freqs[None]
|
||||
|
||||
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
||||
if dim % 2:
|
||||
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
||||
return embedding
|
||||
|
||||
|
||||
###### MAT and FcF #######
|
||||
|
||||
|
||||
def normalize_2nd_moment(x, dim=1):
|
||||
return (
|
||||
x * (x.square().mean(dim=dim, keepdim=True) + torch.finfo(x.dtype).eps).rsqrt()
|
||||
)
|
||||
|
||||
|
||||
class EasyDict(dict):
|
||||
"""Convenience class that behaves like a dict but allows access with the attribute syntax."""
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
self[name] = value
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
del self[name]
|
||||
|
||||
|
||||
def _bias_act_ref(x, b=None, dim=1, act="linear", alpha=None, gain=None, clamp=None):
|
||||
"""Slow reference implementation of `bias_act()` using standard TensorFlow ops."""
|
||||
assert isinstance(x, torch.Tensor)
|
||||
assert clamp is None or clamp >= 0
|
||||
spec = activation_funcs[act]
|
||||
alpha = float(alpha if alpha is not None else spec.def_alpha)
|
||||
gain = float(gain if gain is not None else spec.def_gain)
|
||||
clamp = float(clamp if clamp is not None else -1)
|
||||
|
||||
# Add bias.
|
||||
if b is not None:
|
||||
assert isinstance(b, torch.Tensor) and b.ndim == 1
|
||||
assert 0 <= dim < x.ndim
|
||||
assert b.shape[0] == x.shape[dim]
|
||||
x = x + b.reshape([-1 if i == dim else 1 for i in range(x.ndim)])
|
||||
|
||||
# Evaluate activation function.
|
||||
alpha = float(alpha)
|
||||
x = spec.func(x, alpha=alpha)
|
||||
|
||||
# Scale by gain.
|
||||
gain = float(gain)
|
||||
if gain != 1:
|
||||
x = x * gain
|
||||
|
||||
# Clamp.
|
||||
if clamp >= 0:
|
||||
x = x.clamp(-clamp, clamp) # pylint: disable=invalid-unary-operand-type
|
||||
return x
|
||||
|
||||
|
||||
def bias_act(
|
||||
x, b=None, dim=1, act="linear", alpha=None, gain=None, clamp=None, impl="ref"
|
||||
):
|
||||
r"""Fused bias and activation function.
|
||||
|
||||
Adds bias `b` to activation tensor `x`, evaluates activation function `act`,
|
||||
and scales the result by `gain`. Each of the steps is optional. In most cases,
|
||||
the fused op is considerably more efficient than performing the same calculation
|
||||
using standard PyTorch ops. It supports first and second order gradients,
|
||||
but not third order gradients.
|
||||
|
||||
Args:
|
||||
x: Input activation tensor. Can be of any shape.
|
||||
b: Bias vector, or `None` to disable. Must be a 1D tensor of the same type
|
||||
as `x`. The shape must be known, and it must match the dimension of `x`
|
||||
corresponding to `dim`.
|
||||
dim: The dimension in `x` corresponding to the elements of `b`.
|
||||
The value of `dim` is ignored if `b` is not specified.
|
||||
act: Name of the activation function to evaluate, or `"linear"` to disable.
|
||||
Can be e.g. `"relu"`, `"lrelu"`, `"tanh"`, `"sigmoid"`, `"swish"`, etc.
|
||||
See `activation_funcs` for a full list. `None` is not allowed.
|
||||
alpha: Shape parameter for the activation function, or `None` to use the default.
|
||||
gain: Scaling factor for the output tensor, or `None` to use default.
|
||||
See `activation_funcs` for the default scaling of each activation function.
|
||||
If unsure, consider specifying 1.
|
||||
clamp: Clamp the output values to `[-clamp, +clamp]`, or `None` to disable
|
||||
the clamping (default).
|
||||
impl: Name of the implementation to use. Can be `"ref"` or `"cuda"` (default).
|
||||
|
||||
Returns:
|
||||
Tensor of the same shape and datatype as `x`.
|
||||
"""
|
||||
assert isinstance(x, torch.Tensor)
|
||||
assert impl in ["ref", "cuda"]
|
||||
return _bias_act_ref(
|
||||
x=x, b=b, dim=dim, act=act, alpha=alpha, gain=gain, clamp=clamp
|
||||
)
|
||||
|
||||
|
||||
def _get_filter_size(f):
|
||||
if f is None:
|
||||
return 1, 1
|
||||
|
||||
assert isinstance(f, torch.Tensor) and f.ndim in [1, 2]
|
||||
fw = f.shape[-1]
|
||||
fh = f.shape[0]
|
||||
|
||||
fw = int(fw)
|
||||
fh = int(fh)
|
||||
assert fw >= 1 and fh >= 1
|
||||
return fw, fh
|
||||
|
||||
|
||||
def _get_weight_shape(w):
|
||||
shape = [int(sz) for sz in w.shape]
|
||||
return shape
|
||||
|
||||
|
||||
def _parse_scaling(scaling):
|
||||
if isinstance(scaling, int):
|
||||
scaling = [scaling, scaling]
|
||||
assert isinstance(scaling, (list, tuple))
|
||||
assert all(isinstance(x, int) for x in scaling)
|
||||
sx, sy = scaling
|
||||
assert sx >= 1 and sy >= 1
|
||||
return sx, sy
|
||||
|
||||
|
||||
def _parse_padding(padding):
|
||||
if isinstance(padding, int):
|
||||
padding = [padding, padding]
|
||||
assert isinstance(padding, (list, tuple))
|
||||
assert all(isinstance(x, int) for x in padding)
|
||||
if len(padding) == 2:
|
||||
padx, pady = padding
|
||||
padding = [padx, padx, pady, pady]
|
||||
padx0, padx1, pady0, pady1 = padding
|
||||
return padx0, padx1, pady0, pady1
|
||||
|
||||
|
||||
def setup_filter(
|
||||
f,
|
||||
device=torch.device("cpu"),
|
||||
normalize=True,
|
||||
flip_filter=False,
|
||||
gain=1,
|
||||
separable=None,
|
||||
):
|
||||
r"""Convenience function to setup 2D FIR filter for `upfirdn2d()`.
|
||||
|
||||
Args:
|
||||
f: Torch tensor, numpy array, or python list of the shape
|
||||
`[filter_height, filter_width]` (non-separable),
|
||||
`[filter_taps]` (separable),
|
||||
`[]` (impulse), or
|
||||
`None` (identity).
|
||||
device: Result device (default: cpu).
|
||||
normalize: Normalize the filter so that it retains the magnitude
|
||||
for constant input signal (DC)? (default: True).
|
||||
flip_filter: Flip the filter? (default: False).
|
||||
gain: Overall scaling factor for signal magnitude (default: 1).
|
||||
separable: Return a separable filter? (default: select automatically).
|
||||
|
||||
Returns:
|
||||
Float32 tensor of the shape
|
||||
`[filter_height, filter_width]` (non-separable) or
|
||||
`[filter_taps]` (separable).
|
||||
"""
|
||||
# Validate.
|
||||
if f is None:
|
||||
f = 1
|
||||
f = torch.as_tensor(f, dtype=torch.float32)
|
||||
assert f.ndim in [0, 1, 2]
|
||||
assert f.numel() > 0
|
||||
if f.ndim == 0:
|
||||
f = f[np.newaxis]
|
||||
|
||||
# Separable?
|
||||
if separable is None:
|
||||
separable = f.ndim == 1 and f.numel() >= 8
|
||||
if f.ndim == 1 and not separable:
|
||||
f = f.ger(f)
|
||||
assert f.ndim == (1 if separable else 2)
|
||||
|
||||
# Apply normalize, flip, gain, and device.
|
||||
if normalize:
|
||||
f /= f.sum()
|
||||
if flip_filter:
|
||||
f = f.flip(list(range(f.ndim)))
|
||||
f = f * (gain ** (f.ndim / 2))
|
||||
f = f.to(device=device)
|
||||
return f
|
||||
|
||||
|
||||
def _ntuple(n):
|
||||
def parse(x):
|
||||
if isinstance(x, collections.abc.Iterable):
|
||||
return x
|
||||
return tuple(repeat(x, n))
|
||||
|
||||
return parse
|
||||
|
||||
|
||||
to_2tuple = _ntuple(2)
|
||||
|
||||
activation_funcs = {
|
||||
"linear": EasyDict(
|
||||
func=lambda x, **_: x,
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=1,
|
||||
ref="",
|
||||
has_2nd_grad=False,
|
||||
),
|
||||
"relu": EasyDict(
|
||||
func=lambda x, **_: torch.nn.functional.relu(x),
|
||||
def_alpha=0,
|
||||
def_gain=np.sqrt(2),
|
||||
cuda_idx=2,
|
||||
ref="y",
|
||||
has_2nd_grad=False,
|
||||
),
|
||||
"lrelu": EasyDict(
|
||||
func=lambda x, alpha, **_: torch.nn.functional.leaky_relu(x, alpha),
|
||||
def_alpha=0.2,
|
||||
def_gain=np.sqrt(2),
|
||||
cuda_idx=3,
|
||||
ref="y",
|
||||
has_2nd_grad=False,
|
||||
),
|
||||
"tanh": EasyDict(
|
||||
func=lambda x, **_: torch.tanh(x),
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=4,
|
||||
ref="y",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
"sigmoid": EasyDict(
|
||||
func=lambda x, **_: torch.sigmoid(x),
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=5,
|
||||
ref="y",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
"elu": EasyDict(
|
||||
func=lambda x, **_: torch.nn.functional.elu(x),
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=6,
|
||||
ref="y",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
"selu": EasyDict(
|
||||
func=lambda x, **_: torch.nn.functional.selu(x),
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=7,
|
||||
ref="y",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
"softplus": EasyDict(
|
||||
func=lambda x, **_: torch.nn.functional.softplus(x),
|
||||
def_alpha=0,
|
||||
def_gain=1,
|
||||
cuda_idx=8,
|
||||
ref="y",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
"swish": EasyDict(
|
||||
func=lambda x, **_: torch.sigmoid(x) * x,
|
||||
def_alpha=0,
|
||||
def_gain=np.sqrt(2),
|
||||
cuda_idx=9,
|
||||
ref="x",
|
||||
has_2nd_grad=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def upfirdn2d(x, f, up=1, down=1, padding=0, flip_filter=False, gain=1, impl="cuda"):
|
||||
r"""Pad, upsample, filter, and downsample a batch of 2D images.
|
||||
|
||||
Performs the following sequence of operations for each channel:
|
||||
|
||||
1. Upsample the image by inserting N-1 zeros after each pixel (`up`).
|
||||
|
||||
2. Pad the image with the specified number of zeros on each side (`padding`).
|
||||
Negative padding corresponds to cropping the image.
|
||||
|
||||
3. Convolve the image with the specified 2D FIR filter (`f`), shrinking it
|
||||
so that the footprint of all output pixels lies within the input image.
|
||||
|
||||
4. Downsample the image by keeping every Nth pixel (`down`).
|
||||
|
||||
This sequence of operations bears close resemblance to scipy.signal.upfirdn().
|
||||
The fused op is considerably more efficient than performing the same calculation
|
||||
using standard PyTorch ops. It supports gradients of arbitrary order.
|
||||
|
||||
Args:
|
||||
x: Float32/float64/float16 input tensor of the shape
|
||||
`[batch_size, num_channels, in_height, in_width]`.
|
||||
f: Float32 FIR filter of the shape
|
||||
`[filter_height, filter_width]` (non-separable),
|
||||
`[filter_taps]` (separable), or
|
||||
`None` (identity).
|
||||
up: Integer upsampling factor. Can be a single int or a list/tuple
|
||||
`[x, y]` (default: 1).
|
||||
down: Integer downsampling factor. Can be a single int or a list/tuple
|
||||
`[x, y]` (default: 1).
|
||||
padding: Padding with respect to the upsampled image. Can be a single number
|
||||
or a list/tuple `[x, y]` or `[x_before, x_after, y_before, y_after]`
|
||||
(default: 0).
|
||||
flip_filter: False = convolution, True = correlation (default: False).
|
||||
gain: Overall scaling factor for signal magnitude (default: 1).
|
||||
impl: Implementation to use. Can be `'ref'` or `'cuda'` (default: `'cuda'`).
|
||||
|
||||
Returns:
|
||||
Tensor of the shape `[batch_size, num_channels, out_height, out_width]`.
|
||||
"""
|
||||
# assert isinstance(x, torch.Tensor)
|
||||
# assert impl in ['ref', 'cuda']
|
||||
return _upfirdn2d_ref(
|
||||
x, f, up=up, down=down, padding=padding, flip_filter=flip_filter, gain=gain
|
||||
)
|
||||
|
||||
|
||||
def _upfirdn2d_ref(x, f, up=1, down=1, padding=0, flip_filter=False, gain=1):
|
||||
"""Slow reference implementation of `upfirdn2d()` using standard PyTorch ops."""
|
||||
# Validate arguments.
|
||||
assert isinstance(x, torch.Tensor) and x.ndim == 4
|
||||
if f is None:
|
||||
f = torch.ones([1, 1], dtype=torch.float32, device=x.device)
|
||||
assert isinstance(f, torch.Tensor) and f.ndim in [1, 2]
|
||||
assert not f.requires_grad
|
||||
batch_size, num_channels, in_height, in_width = x.shape
|
||||
# upx, upy = _parse_scaling(up)
|
||||
# downx, downy = _parse_scaling(down)
|
||||
|
||||
upx, upy = up, up
|
||||
downx, downy = down, down
|
||||
|
||||
# padx0, padx1, pady0, pady1 = _parse_padding(padding)
|
||||
padx0, padx1, pady0, pady1 = padding[0], padding[1], padding[2], padding[3]
|
||||
|
||||
# Upsample by inserting zeros.
|
||||
x = x.reshape([batch_size, num_channels, in_height, 1, in_width, 1])
|
||||
x = torch.nn.functional.pad(x, [0, upx - 1, 0, 0, 0, upy - 1])
|
||||
x = x.reshape([batch_size, num_channels, in_height * upy, in_width * upx])
|
||||
|
||||
# Pad or crop.
|
||||
x = torch.nn.functional.pad(
|
||||
x, [max(padx0, 0), max(padx1, 0), max(pady0, 0), max(pady1, 0)]
|
||||
)
|
||||
x = x[
|
||||
:,
|
||||
:,
|
||||
max(-pady0, 0) : x.shape[2] - max(-pady1, 0),
|
||||
max(-padx0, 0) : x.shape[3] - max(-padx1, 0),
|
||||
]
|
||||
|
||||
# Setup filter.
|
||||
f = f * (gain ** (f.ndim / 2))
|
||||
f = f.to(x.dtype)
|
||||
if not flip_filter:
|
||||
f = f.flip(list(range(f.ndim)))
|
||||
|
||||
# Convolve with the filter.
|
||||
f = f[np.newaxis, np.newaxis].repeat([num_channels, 1] + [1] * f.ndim)
|
||||
if f.ndim == 4:
|
||||
x = conv2d(input=x, weight=f, groups=num_channels)
|
||||
else:
|
||||
x = conv2d(input=x, weight=f.unsqueeze(2), groups=num_channels)
|
||||
x = conv2d(input=x, weight=f.unsqueeze(3), groups=num_channels)
|
||||
|
||||
# Downsample by throwing away pixels.
|
||||
x = x[:, :, ::downy, ::downx]
|
||||
return x
|
||||
|
||||
|
||||
def downsample2d(x, f, down=2, padding=0, flip_filter=False, gain=1, impl="cuda"):
|
||||
r"""Downsample a batch of 2D images using the given 2D FIR filter.
|
||||
|
||||
By default, the result is padded so that its shape is a fraction of the input.
|
||||
User-specified padding is applied on top of that, with negative values
|
||||
indicating cropping. Pixels outside the image are assumed to be zero.
|
||||
|
||||
Args:
|
||||
x: Float32/float64/float16 input tensor of the shape
|
||||
`[batch_size, num_channels, in_height, in_width]`.
|
||||
f: Float32 FIR filter of the shape
|
||||
`[filter_height, filter_width]` (non-separable),
|
||||
`[filter_taps]` (separable), or
|
||||
`None` (identity).
|
||||
down: Integer downsampling factor. Can be a single int or a list/tuple
|
||||
`[x, y]` (default: 1).
|
||||
padding: Padding with respect to the input. Can be a single number or a
|
||||
list/tuple `[x, y]` or `[x_before, x_after, y_before, y_after]`
|
||||
(default: 0).
|
||||
flip_filter: False = convolution, True = correlation (default: False).
|
||||
gain: Overall scaling factor for signal magnitude (default: 1).
|
||||
impl: Implementation to use. Can be `'ref'` or `'cuda'` (default: `'cuda'`).
|
||||
|
||||
Returns:
|
||||
Tensor of the shape `[batch_size, num_channels, out_height, out_width]`.
|
||||
"""
|
||||
downx, downy = _parse_scaling(down)
|
||||
# padx0, padx1, pady0, pady1 = _parse_padding(padding)
|
||||
padx0, padx1, pady0, pady1 = padding, padding, padding, padding
|
||||
|
||||
fw, fh = _get_filter_size(f)
|
||||
p = [
|
||||
padx0 + (fw - downx + 1) // 2,
|
||||
padx1 + (fw - downx) // 2,
|
||||
pady0 + (fh - downy + 1) // 2,
|
||||
pady1 + (fh - downy) // 2,
|
||||
]
|
||||
return upfirdn2d(
|
||||
x, f, down=down, padding=p, flip_filter=flip_filter, gain=gain, impl=impl
|
||||
)
|
||||
|
||||
|
||||
def upsample2d(x, f, up=2, padding=0, flip_filter=False, gain=1, impl="cuda"):
|
||||
r"""Upsample a batch of 2D images using the given 2D FIR filter.
|
||||
|
||||
By default, the result is padded so that its shape is a multiple of the input.
|
||||
User-specified padding is applied on top of that, with negative values
|
||||
indicating cropping. Pixels outside the image are assumed to be zero.
|
||||
|
||||
Args:
|
||||
x: Float32/float64/float16 input tensor of the shape
|
||||
`[batch_size, num_channels, in_height, in_width]`.
|
||||
f: Float32 FIR filter of the shape
|
||||
`[filter_height, filter_width]` (non-separable),
|
||||
`[filter_taps]` (separable), or
|
||||
`None` (identity).
|
||||
up: Integer upsampling factor. Can be a single int or a list/tuple
|
||||
`[x, y]` (default: 1).
|
||||
padding: Padding with respect to the output. Can be a single number or a
|
||||
list/tuple `[x, y]` or `[x_before, x_after, y_before, y_after]`
|
||||
(default: 0).
|
||||
flip_filter: False = convolution, True = correlation (default: False).
|
||||
gain: Overall scaling factor for signal magnitude (default: 1).
|
||||
impl: Implementation to use. Can be `'ref'` or `'cuda'` (default: `'cuda'`).
|
||||
|
||||
Returns:
|
||||
Tensor of the shape `[batch_size, num_channels, out_height, out_width]`.
|
||||
"""
|
||||
upx, upy = _parse_scaling(up)
|
||||
# upx, upy = up, up
|
||||
padx0, padx1, pady0, pady1 = _parse_padding(padding)
|
||||
# padx0, padx1, pady0, pady1 = padding, padding, padding, padding
|
||||
fw, fh = _get_filter_size(f)
|
||||
p = [
|
||||
padx0 + (fw + upx - 1) // 2,
|
||||
padx1 + (fw - upx) // 2,
|
||||
pady0 + (fh + upy - 1) // 2,
|
||||
pady1 + (fh - upy) // 2,
|
||||
]
|
||||
return upfirdn2d(
|
||||
x,
|
||||
f,
|
||||
up=up,
|
||||
padding=p,
|
||||
flip_filter=flip_filter,
|
||||
gain=gain * upx * upy,
|
||||
impl=impl,
|
||||
)
|
||||
|
||||
|
||||
class MinibatchStdLayer(torch.nn.Module):
|
||||
def __init__(self, group_size, num_channels=1):
|
||||
super().__init__()
|
||||
self.group_size = group_size
|
||||
self.num_channels = num_channels
|
||||
|
||||
def forward(self, x):
|
||||
N, C, H, W = x.shape
|
||||
G = (
|
||||
torch.min(torch.as_tensor(self.group_size), torch.as_tensor(N))
|
||||
if self.group_size is not None
|
||||
else N
|
||||
)
|
||||
F = self.num_channels
|
||||
c = C // F
|
||||
|
||||
y = x.reshape(
|
||||
G, -1, F, c, H, W
|
||||
) # [GnFcHW] Split minibatch N into n groups of size G, and channels C into F groups of size c.
|
||||
y = y - y.mean(dim=0) # [GnFcHW] Subtract mean over group.
|
||||
y = y.square().mean(dim=0) # [nFcHW] Calc variance over group.
|
||||
y = (y + 1e-8).sqrt() # [nFcHW] Calc stddev over group.
|
||||
y = y.mean(dim=[2, 3, 4]) # [nF] Take average over channels and pixels.
|
||||
y = y.reshape(-1, F, 1, 1) # [nF11] Add missing dimensions.
|
||||
y = y.repeat(G, 1, H, W) # [NFHW] Replicate over group and pixels.
|
||||
x = torch.cat([x, y], dim=1) # [NCHW] Append to input as new channels.
|
||||
return x
|
||||
|
||||
|
||||
class FullyConnectedLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_features, # Number of input features.
|
||||
out_features, # Number of output features.
|
||||
bias=True, # Apply additive bias before the activation function?
|
||||
activation="linear", # Activation function: 'relu', 'lrelu', etc.
|
||||
lr_multiplier=1, # Learning rate multiplier.
|
||||
bias_init=0, # Initial value for the additive bias.
|
||||
):
|
||||
super().__init__()
|
||||
self.weight = torch.nn.Parameter(
|
||||
torch.randn([out_features, in_features]) / lr_multiplier
|
||||
)
|
||||
self.bias = (
|
||||
torch.nn.Parameter(torch.full([out_features], np.float32(bias_init)))
|
||||
if bias
|
||||
else None
|
||||
)
|
||||
self.activation = activation
|
||||
|
||||
self.weight_gain = lr_multiplier / np.sqrt(in_features)
|
||||
self.bias_gain = lr_multiplier
|
||||
|
||||
def forward(self, x):
|
||||
w = self.weight * self.weight_gain
|
||||
b = self.bias
|
||||
if b is not None and self.bias_gain != 1:
|
||||
b = b * self.bias_gain
|
||||
|
||||
if self.activation == "linear" and b is not None:
|
||||
# out = torch.addmm(b.unsqueeze(0), x, w.t())
|
||||
x = x.matmul(w.t())
|
||||
out = x + b.reshape([-1 if i == x.ndim - 1 else 1 for i in range(x.ndim)])
|
||||
else:
|
||||
x = x.matmul(w.t())
|
||||
out = bias_act(x, b, act=self.activation, dim=x.ndim - 1)
|
||||
return out
|
||||
|
||||
|
||||
def _conv2d_wrapper(
|
||||
x, w, stride=1, padding=0, groups=1, transpose=False, flip_weight=True
|
||||
):
|
||||
"""Wrapper for the underlying `conv2d()` and `conv_transpose2d()` implementations."""
|
||||
out_channels, in_channels_per_group, kh, kw = _get_weight_shape(w)
|
||||
|
||||
# Flip weight if requested.
|
||||
if (
|
||||
not flip_weight
|
||||
): # conv2d() actually performs correlation (flip_weight=True) not convolution (flip_weight=False).
|
||||
w = w.flip([2, 3])
|
||||
|
||||
# Workaround performance pitfall in cuDNN 8.0.5, triggered when using
|
||||
# 1x1 kernel + memory_format=channels_last + less than 64 channels.
|
||||
if (
|
||||
kw == 1
|
||||
and kh == 1
|
||||
and stride == 1
|
||||
and padding in [0, [0, 0], (0, 0)]
|
||||
and not transpose
|
||||
):
|
||||
if x.stride()[1] == 1 and min(out_channels, in_channels_per_group) < 64:
|
||||
if out_channels <= 4 and groups == 1:
|
||||
in_shape = x.shape
|
||||
x = w.squeeze(3).squeeze(2) @ x.reshape(
|
||||
[in_shape[0], in_channels_per_group, -1]
|
||||
)
|
||||
x = x.reshape([in_shape[0], out_channels, in_shape[2], in_shape[3]])
|
||||
else:
|
||||
x = x.to(memory_format=torch.contiguous_format)
|
||||
w = w.to(memory_format=torch.contiguous_format)
|
||||
x = conv2d(x, w, groups=groups)
|
||||
return x.to(memory_format=torch.channels_last)
|
||||
|
||||
# Otherwise => execute using conv2d_gradfix.
|
||||
op = conv_transpose2d if transpose else conv2d
|
||||
return op(x, w, stride=stride, padding=padding, groups=groups)
|
||||
|
||||
|
||||
def conv2d_resample(
|
||||
x, w, f=None, up=1, down=1, padding=0, groups=1, flip_weight=True, flip_filter=False
|
||||
):
|
||||
r"""2D convolution with optional up/downsampling.
|
||||
|
||||
Padding is performed only once at the beginning, not between the operations.
|
||||
|
||||
Args:
|
||||
x: Input tensor of shape
|
||||
`[batch_size, in_channels, in_height, in_width]`.
|
||||
w: Weight tensor of shape
|
||||
`[out_channels, in_channels//groups, kernel_height, kernel_width]`.
|
||||
f: Low-pass filter for up/downsampling. Must be prepared beforehand by
|
||||
calling setup_filter(). None = identity (default).
|
||||
up: Integer upsampling factor (default: 1).
|
||||
down: Integer downsampling factor (default: 1).
|
||||
padding: Padding with respect to the upsampled image. Can be a single number
|
||||
or a list/tuple `[x, y]` or `[x_before, x_after, y_before, y_after]`
|
||||
(default: 0).
|
||||
groups: Split input channels into N groups (default: 1).
|
||||
flip_weight: False = convolution, True = correlation (default: True).
|
||||
flip_filter: False = convolution, True = correlation (default: False).
|
||||
|
||||
Returns:
|
||||
Tensor of the shape `[batch_size, num_channels, out_height, out_width]`.
|
||||
"""
|
||||
# Validate arguments.
|
||||
assert isinstance(x, torch.Tensor) and (x.ndim == 4)
|
||||
assert isinstance(w, torch.Tensor) and (w.ndim == 4) and (w.dtype == x.dtype)
|
||||
assert f is None or (isinstance(f, torch.Tensor) and f.ndim in [1, 2])
|
||||
assert isinstance(up, int) and (up >= 1)
|
||||
assert isinstance(down, int) and (down >= 1)
|
||||
# assert isinstance(groups, int) and (groups >= 1), f"!!!!!! groups: {groups} isinstance(groups, int) {isinstance(groups, int)} {type(groups)}"
|
||||
out_channels, in_channels_per_group, kh, kw = _get_weight_shape(w)
|
||||
fw, fh = _get_filter_size(f)
|
||||
# px0, px1, py0, py1 = _parse_padding(padding)
|
||||
px0, px1, py0, py1 = padding, padding, padding, padding
|
||||
|
||||
# Adjust padding to account for up/downsampling.
|
||||
if up > 1:
|
||||
px0 += (fw + up - 1) // 2
|
||||
px1 += (fw - up) // 2
|
||||
py0 += (fh + up - 1) // 2
|
||||
py1 += (fh - up) // 2
|
||||
if down > 1:
|
||||
px0 += (fw - down + 1) // 2
|
||||
px1 += (fw - down) // 2
|
||||
py0 += (fh - down + 1) // 2
|
||||
py1 += (fh - down) // 2
|
||||
|
||||
# Fast path: 1x1 convolution with downsampling only => downsample first, then convolve.
|
||||
if kw == 1 and kh == 1 and (down > 1 and up == 1):
|
||||
x = upfirdn2d(
|
||||
x=x, f=f, down=down, padding=[px0, px1, py0, py1], flip_filter=flip_filter
|
||||
)
|
||||
x = _conv2d_wrapper(x=x, w=w, groups=groups, flip_weight=flip_weight)
|
||||
return x
|
||||
|
||||
# Fast path: 1x1 convolution with upsampling only => convolve first, then upsample.
|
||||
if kw == 1 and kh == 1 and (up > 1 and down == 1):
|
||||
x = _conv2d_wrapper(x=x, w=w, groups=groups, flip_weight=flip_weight)
|
||||
x = upfirdn2d(
|
||||
x=x,
|
||||
f=f,
|
||||
up=up,
|
||||
padding=[px0, px1, py0, py1],
|
||||
gain=up**2,
|
||||
flip_filter=flip_filter,
|
||||
)
|
||||
return x
|
||||
|
||||
# Fast path: downsampling only => use strided convolution.
|
||||
if down > 1 and up == 1:
|
||||
x = upfirdn2d(x=x, f=f, padding=[px0, px1, py0, py1], flip_filter=flip_filter)
|
||||
x = _conv2d_wrapper(
|
||||
x=x, w=w, stride=down, groups=groups, flip_weight=flip_weight
|
||||
)
|
||||
return x
|
||||
|
||||
# Fast path: upsampling with optional downsampling => use transpose strided convolution.
|
||||
if up > 1:
|
||||
if groups == 1:
|
||||
w = w.transpose(0, 1)
|
||||
else:
|
||||
w = w.reshape(groups, out_channels // groups, in_channels_per_group, kh, kw)
|
||||
w = w.transpose(1, 2)
|
||||
w = w.reshape(
|
||||
groups * in_channels_per_group, out_channels // groups, kh, kw
|
||||
)
|
||||
px0 -= kw - 1
|
||||
px1 -= kw - up
|
||||
py0 -= kh - 1
|
||||
py1 -= kh - up
|
||||
pxt = max(min(-px0, -px1), 0)
|
||||
pyt = max(min(-py0, -py1), 0)
|
||||
x = _conv2d_wrapper(
|
||||
x=x,
|
||||
w=w,
|
||||
stride=up,
|
||||
padding=[pyt, pxt],
|
||||
groups=groups,
|
||||
transpose=True,
|
||||
flip_weight=(not flip_weight),
|
||||
)
|
||||
x = upfirdn2d(
|
||||
x=x,
|
||||
f=f,
|
||||
padding=[px0 + pxt, px1 + pxt, py0 + pyt, py1 + pyt],
|
||||
gain=up**2,
|
||||
flip_filter=flip_filter,
|
||||
)
|
||||
if down > 1:
|
||||
x = upfirdn2d(x=x, f=f, down=down, flip_filter=flip_filter)
|
||||
return x
|
||||
|
||||
# Fast path: no up/downsampling, padding supported by the underlying implementation => use plain conv2d.
|
||||
if up == 1 and down == 1:
|
||||
if px0 == px1 and py0 == py1 and px0 >= 0 and py0 >= 0:
|
||||
return _conv2d_wrapper(
|
||||
x=x, w=w, padding=[py0, px0], groups=groups, flip_weight=flip_weight
|
||||
)
|
||||
|
||||
# Fallback: Generic reference implementation.
|
||||
x = upfirdn2d(
|
||||
x=x,
|
||||
f=(f if up > 1 else None),
|
||||
up=up,
|
||||
padding=[px0, px1, py0, py1],
|
||||
gain=up**2,
|
||||
flip_filter=flip_filter,
|
||||
)
|
||||
x = _conv2d_wrapper(x=x, w=w, groups=groups, flip_weight=flip_weight)
|
||||
if down > 1:
|
||||
x = upfirdn2d(x=x, f=f, down=down, flip_filter=flip_filter)
|
||||
return x
|
||||
|
||||
|
||||
class Conv2dLayer(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels, # Number of input channels.
|
||||
out_channels, # Number of output channels.
|
||||
kernel_size, # Width and height of the convolution kernel.
|
||||
bias=True, # Apply additive bias before the activation function?
|
||||
activation="linear", # Activation function: 'relu', 'lrelu', etc.
|
||||
up=1, # Integer upsampling factor.
|
||||
down=1, # Integer downsampling factor.
|
||||
resample_filter=[
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
1,
|
||||
], # Low-pass filter to apply when resampling activations.
|
||||
conv_clamp=None, # Clamp the output to +-X, None = disable clamping.
|
||||
channels_last=False, # Expect the input to have memory_format=channels_last?
|
||||
trainable=True, # Update the weights of this layer during training?
|
||||
):
|
||||
super().__init__()
|
||||
self.activation = activation
|
||||
self.up = up
|
||||
self.down = down
|
||||
self.register_buffer("resample_filter", setup_filter(resample_filter))
|
||||
self.conv_clamp = conv_clamp
|
||||
self.padding = kernel_size // 2
|
||||
self.weight_gain = 1 / np.sqrt(in_channels * (kernel_size**2))
|
||||
self.act_gain = activation_funcs[activation].def_gain
|
||||
|
||||
memory_format = (
|
||||
torch.channels_last if channels_last else torch.contiguous_format
|
||||
)
|
||||
weight = torch.randn([out_channels, in_channels, kernel_size, kernel_size]).to(
|
||||
memory_format=memory_format
|
||||
)
|
||||
bias = torch.zeros([out_channels]) if bias else None
|
||||
if trainable:
|
||||
self.weight = torch.nn.Parameter(weight)
|
||||
self.bias = torch.nn.Parameter(bias) if bias is not None else None
|
||||
else:
|
||||
self.register_buffer("weight", weight)
|
||||
if bias is not None:
|
||||
self.register_buffer("bias", bias)
|
||||
else:
|
||||
self.bias = None
|
||||
|
||||
def forward(self, x, gain=1):
|
||||
w = self.weight * self.weight_gain
|
||||
x = conv2d_resample(
|
||||
x=x,
|
||||
w=w,
|
||||
f=self.resample_filter,
|
||||
up=self.up,
|
||||
down=self.down,
|
||||
padding=self.padding,
|
||||
)
|
||||
|
||||
act_gain = self.act_gain * gain
|
||||
act_clamp = self.conv_clamp * gain if self.conv_clamp is not None else None
|
||||
out = bias_act(
|
||||
x, self.bias, act=self.activation, gain=act_gain, clamp=act_clamp
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def torch_gc():
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
gc.collect()
|
||||
|
||||
|
||||
def set_seed(seed: int):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
|
||||
|
||||
def get_scheduler(sd_sampler, scheduler_config):
|
||||
# https://github.com/huggingface/diffusers/issues/4167
|
||||
keys_to_pop = ["use_karras_sigmas", "algorithm_type"]
|
||||
scheduler_config = dict(scheduler_config)
|
||||
for it in keys_to_pop:
|
||||
scheduler_config.pop(it, None)
|
||||
|
||||
# fmt: off
|
||||
samplers = {
|
||||
SDSampler.dpm_plus_plus_2m: [DPMSolverMultistepScheduler],
|
||||
SDSampler.dpm_plus_plus_2m_karras: [DPMSolverMultistepScheduler, dict(use_karras_sigmas=True)],
|
||||
SDSampler.dpm_plus_plus_2m_sde: [DPMSolverMultistepScheduler, dict(algorithm_type="sde-dpmsolver++")],
|
||||
SDSampler.dpm_plus_plus_2m_sde_karras: [DPMSolverMultistepScheduler, dict(algorithm_type="sde-dpmsolver++", use_karras_sigmas=True)],
|
||||
SDSampler.dpm_plus_plus_sde: [DPMSolverSinglestepScheduler],
|
||||
SDSampler.dpm_plus_plus_sde_karras: [DPMSolverSinglestepScheduler, dict(use_karras_sigmas=True)],
|
||||
SDSampler.dpm2: [KDPM2DiscreteScheduler],
|
||||
SDSampler.dpm2_karras: [KDPM2DiscreteScheduler, dict(use_karras_sigmas=True)],
|
||||
SDSampler.dpm2_a: [KDPM2AncestralDiscreteScheduler],
|
||||
SDSampler.dpm2_a_karras: [KDPM2AncestralDiscreteScheduler, dict(use_karras_sigmas=True)],
|
||||
SDSampler.euler: [EulerDiscreteScheduler],
|
||||
SDSampler.euler_a: [EulerAncestralDiscreteScheduler],
|
||||
SDSampler.heun: [HeunDiscreteScheduler],
|
||||
SDSampler.lms: [LMSDiscreteScheduler],
|
||||
SDSampler.lms_karras: [LMSDiscreteScheduler, dict(use_karras_sigmas=True)],
|
||||
SDSampler.ddim: [DDIMScheduler],
|
||||
SDSampler.pndm: [PNDMScheduler],
|
||||
SDSampler.uni_pc: [UniPCMultistepScheduler],
|
||||
SDSampler.lcm: [LCMScheduler],
|
||||
}
|
||||
# fmt: on
|
||||
if sd_sampler in samplers:
|
||||
if len(samplers[sd_sampler]) == 2:
|
||||
scheduler_cls, kwargs = samplers[sd_sampler]
|
||||
else:
|
||||
scheduler_cls, kwargs = samplers[sd_sampler][0], {}
|
||||
return scheduler_cls.from_config(scheduler_config, **kwargs)
|
||||
else:
|
||||
raise ValueError(sd_sampler)
|
||||
|
||||
|
||||
def handle_from_pretrained_exceptions(func, **kwargs):
|
||||
try:
|
||||
return func(**kwargs)
|
||||
except ValueError as e:
|
||||
if "You are trying to load the model files of the `variant=fp16`" in str(e):
|
||||
logger.info("variant=fp16 not found, try revision=fp16")
|
||||
return func(**{**kwargs, "variant": None, "revision": "fp16"})
|
||||
except OSError as e:
|
||||
previous_traceback = traceback.format_exc()
|
||||
if "RevisionNotFoundError: 404 Client Error." in previous_traceback:
|
||||
logger.info("revision=fp16 not found, try revision=main")
|
||||
return func(**{**kwargs, "variant": None, "revision": "main"})
|
||||
except Exception as e:
|
||||
raise e
|
||||
476
iopaint/model/zits.py
Normal file
@@ -0,0 +1,476 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from iopaint.helper import get_cache_path_by_url, load_jit_model, download_model
|
||||
from iopaint.schema import InpaintRequest
|
||||
import numpy as np
|
||||
|
||||
from iopaint.model.base import InpaintModel
|
||||
|
||||
ZITS_INPAINT_MODEL_URL = os.environ.get(
|
||||
"ZITS_INPAINT_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_zits/zits-inpaint-0717.pt",
|
||||
)
|
||||
ZITS_INPAINT_MODEL_MD5 = os.environ.get(
|
||||
"ZITS_INPAINT_MODEL_MD5", "9978cc7157dc29699e42308d675b2154"
|
||||
)
|
||||
|
||||
ZITS_EDGE_LINE_MODEL_URL = os.environ.get(
|
||||
"ZITS_EDGE_LINE_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_zits/zits-edge-line-0717.pt",
|
||||
)
|
||||
ZITS_EDGE_LINE_MODEL_MD5 = os.environ.get(
|
||||
"ZITS_EDGE_LINE_MODEL_MD5", "55e31af21ba96bbf0c80603c76ea8c5f"
|
||||
)
|
||||
|
||||
ZITS_STRUCTURE_UPSAMPLE_MODEL_URL = os.environ.get(
|
||||
"ZITS_STRUCTURE_UPSAMPLE_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_zits/zits-structure-upsample-0717.pt",
|
||||
)
|
||||
ZITS_STRUCTURE_UPSAMPLE_MODEL_MD5 = os.environ.get(
|
||||
"ZITS_STRUCTURE_UPSAMPLE_MODEL_MD5", "3d88a07211bd41b2ec8cc0d999f29927"
|
||||
)
|
||||
|
||||
ZITS_WIRE_FRAME_MODEL_URL = os.environ.get(
|
||||
"ZITS_WIRE_FRAME_MODEL_URL",
|
||||
"https://github.com/Sanster/models/releases/download/add_zits/zits-wireframe-0717.pt",
|
||||
)
|
||||
ZITS_WIRE_FRAME_MODEL_MD5 = os.environ.get(
|
||||
"ZITS_WIRE_FRAME_MODEL_MD5", "a9727c63a8b48b65c905d351b21ce46b"
|
||||
)
|
||||
|
||||
|
||||
def resize(img, height, width, center_crop=False):
|
||||
imgh, imgw = img.shape[0:2]
|
||||
|
||||
if center_crop and imgh != imgw:
|
||||
# center crop
|
||||
side = np.minimum(imgh, imgw)
|
||||
j = (imgh - side) // 2
|
||||
i = (imgw - side) // 2
|
||||
img = img[j : j + side, i : i + side, ...]
|
||||
|
||||
if imgh > height and imgw > width:
|
||||
inter = cv2.INTER_AREA
|
||||
else:
|
||||
inter = cv2.INTER_LINEAR
|
||||
img = cv2.resize(img, (height, width), interpolation=inter)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def to_tensor(img, scale=True, norm=False):
|
||||
if img.ndim == 2:
|
||||
img = img[:, :, np.newaxis]
|
||||
c = img.shape[-1]
|
||||
|
||||
if scale:
|
||||
img_t = torch.from_numpy(img).permute(2, 0, 1).float().div(255)
|
||||
else:
|
||||
img_t = torch.from_numpy(img).permute(2, 0, 1).float()
|
||||
|
||||
if norm:
|
||||
mean = torch.tensor([0.5, 0.5, 0.5]).reshape(c, 1, 1)
|
||||
std = torch.tensor([0.5, 0.5, 0.5]).reshape(c, 1, 1)
|
||||
img_t = (img_t - mean) / std
|
||||
return img_t
|
||||
|
||||
|
||||
def load_masked_position_encoding(mask):
|
||||
ones_filter = np.ones((3, 3), dtype=np.float32)
|
||||
d_filter1 = np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=np.float32)
|
||||
d_filter2 = np.array([[0, 0, 0], [1, 1, 0], [1, 1, 0]], dtype=np.float32)
|
||||
d_filter3 = np.array([[0, 1, 1], [0, 1, 1], [0, 0, 0]], dtype=np.float32)
|
||||
d_filter4 = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], dtype=np.float32)
|
||||
str_size = 256
|
||||
pos_num = 128
|
||||
|
||||
ori_mask = mask.copy()
|
||||
ori_h, ori_w = ori_mask.shape[0:2]
|
||||
ori_mask = ori_mask / 255
|
||||
mask = cv2.resize(mask, (str_size, str_size), interpolation=cv2.INTER_AREA)
|
||||
mask[mask > 0] = 255
|
||||
h, w = mask.shape[0:2]
|
||||
mask3 = mask.copy()
|
||||
mask3 = 1.0 - (mask3 / 255.0)
|
||||
pos = np.zeros((h, w), dtype=np.int32)
|
||||
direct = np.zeros((h, w, 4), dtype=np.int32)
|
||||
i = 0
|
||||
while np.sum(1 - mask3) > 0:
|
||||
i += 1
|
||||
mask3_ = cv2.filter2D(mask3, -1, ones_filter)
|
||||
mask3_[mask3_ > 0] = 1
|
||||
sub_mask = mask3_ - mask3
|
||||
pos[sub_mask == 1] = i
|
||||
|
||||
m = cv2.filter2D(mask3, -1, d_filter1)
|
||||
m[m > 0] = 1
|
||||
m = m - mask3
|
||||
direct[m == 1, 0] = 1
|
||||
|
||||
m = cv2.filter2D(mask3, -1, d_filter2)
|
||||
m[m > 0] = 1
|
||||
m = m - mask3
|
||||
direct[m == 1, 1] = 1
|
||||
|
||||
m = cv2.filter2D(mask3, -1, d_filter3)
|
||||
m[m > 0] = 1
|
||||
m = m - mask3
|
||||
direct[m == 1, 2] = 1
|
||||
|
||||
m = cv2.filter2D(mask3, -1, d_filter4)
|
||||
m[m > 0] = 1
|
||||
m = m - mask3
|
||||
direct[m == 1, 3] = 1
|
||||
|
||||
mask3 = mask3_
|
||||
|
||||
abs_pos = pos.copy()
|
||||
rel_pos = pos / (str_size / 2) # to 0~1 maybe larger than 1
|
||||
rel_pos = (rel_pos * pos_num).astype(np.int32)
|
||||
rel_pos = np.clip(rel_pos, 0, pos_num - 1)
|
||||
|
||||
if ori_w != w or ori_h != h:
|
||||
rel_pos = cv2.resize(rel_pos, (ori_w, ori_h), interpolation=cv2.INTER_NEAREST)
|
||||
rel_pos[ori_mask == 0] = 0
|
||||
direct = cv2.resize(direct, (ori_w, ori_h), interpolation=cv2.INTER_NEAREST)
|
||||
direct[ori_mask == 0, :] = 0
|
||||
|
||||
return rel_pos, abs_pos, direct
|
||||
|
||||
|
||||
def load_image(img, mask, device, sigma256=3.0):
|
||||
"""
|
||||
Args:
|
||||
img: [H, W, C] RGB
|
||||
mask: [H, W] 255 为 masks 区域
|
||||
sigma256:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
h, w, _ = img.shape
|
||||
imgh, imgw = img.shape[0:2]
|
||||
img_256 = resize(img, 256, 256)
|
||||
|
||||
mask = (mask > 127).astype(np.uint8) * 255
|
||||
mask_256 = cv2.resize(mask, (256, 256), interpolation=cv2.INTER_AREA)
|
||||
mask_256[mask_256 > 0] = 255
|
||||
|
||||
mask_512 = cv2.resize(mask, (512, 512), interpolation=cv2.INTER_AREA)
|
||||
mask_512[mask_512 > 0] = 255
|
||||
|
||||
# original skimage implemention
|
||||
# https://scikit-image.org/docs/stable/api/skimage.feature.html#skimage.feature.canny
|
||||
# low_threshold: Lower bound for hysteresis thresholding (linking edges). If None, low_threshold is set to 10% of dtype’s max.
|
||||
# high_threshold: Upper bound for hysteresis thresholding (linking edges). If None, high_threshold is set to 20% of dtype’s max.
|
||||
|
||||
try:
|
||||
import skimage
|
||||
|
||||
gray_256 = skimage.color.rgb2gray(img_256)
|
||||
edge_256 = skimage.feature.canny(gray_256, sigma=3.0, mask=None).astype(float)
|
||||
# cv2.imwrite("skimage_gray.jpg", (gray_256*255).astype(np.uint8))
|
||||
# cv2.imwrite("skimage_edge.jpg", (edge_256*255).astype(np.uint8))
|
||||
except:
|
||||
gray_256 = cv2.cvtColor(img_256, cv2.COLOR_RGB2GRAY)
|
||||
gray_256_blured = cv2.GaussianBlur(
|
||||
gray_256, ksize=(7, 7), sigmaX=sigma256, sigmaY=sigma256
|
||||
)
|
||||
edge_256 = cv2.Canny(
|
||||
gray_256_blured, threshold1=int(255 * 0.1), threshold2=int(255 * 0.2)
|
||||
)
|
||||
|
||||
# cv2.imwrite("opencv_edge.jpg", edge_256)
|
||||
|
||||
# line
|
||||
img_512 = resize(img, 512, 512)
|
||||
|
||||
rel_pos, abs_pos, direct = load_masked_position_encoding(mask)
|
||||
|
||||
batch = dict()
|
||||
batch["images"] = to_tensor(img.copy()).unsqueeze(0).to(device)
|
||||
batch["img_256"] = to_tensor(img_256, norm=True).unsqueeze(0).to(device)
|
||||
batch["masks"] = to_tensor(mask).unsqueeze(0).to(device)
|
||||
batch["mask_256"] = to_tensor(mask_256).unsqueeze(0).to(device)
|
||||
batch["mask_512"] = to_tensor(mask_512).unsqueeze(0).to(device)
|
||||
batch["edge_256"] = to_tensor(edge_256, scale=False).unsqueeze(0).to(device)
|
||||
batch["img_512"] = to_tensor(img_512).unsqueeze(0).to(device)
|
||||
batch["rel_pos"] = torch.LongTensor(rel_pos).unsqueeze(0).to(device)
|
||||
batch["abs_pos"] = torch.LongTensor(abs_pos).unsqueeze(0).to(device)
|
||||
batch["direct"] = torch.LongTensor(direct).unsqueeze(0).to(device)
|
||||
batch["h"] = imgh
|
||||
batch["w"] = imgw
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
def to_device(data, device):
|
||||
if isinstance(data, torch.Tensor):
|
||||
return data.to(device)
|
||||
if isinstance(data, dict):
|
||||
for key in data:
|
||||
if isinstance(data[key], torch.Tensor):
|
||||
data[key] = data[key].to(device)
|
||||
return data
|
||||
if isinstance(data, list):
|
||||
return [to_device(d, device) for d in data]
|
||||
|
||||
|
||||
class ZITS(InpaintModel):
|
||||
name = "zits"
|
||||
min_size = 256
|
||||
pad_mod = 32
|
||||
pad_to_square = True
|
||||
is_erase_model = True
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
"""
|
||||
|
||||
Args:
|
||||
device:
|
||||
"""
|
||||
super().__init__(device)
|
||||
self.device = device
|
||||
self.sample_edge_line_iterations = 1
|
||||
|
||||
def init_model(self, device, **kwargs):
|
||||
self.wireframe = load_jit_model(
|
||||
ZITS_WIRE_FRAME_MODEL_URL, device, ZITS_WIRE_FRAME_MODEL_MD5
|
||||
)
|
||||
self.edge_line = load_jit_model(
|
||||
ZITS_EDGE_LINE_MODEL_URL, device, ZITS_EDGE_LINE_MODEL_MD5
|
||||
)
|
||||
self.structure_upsample = load_jit_model(
|
||||
ZITS_STRUCTURE_UPSAMPLE_MODEL_URL, device, ZITS_STRUCTURE_UPSAMPLE_MODEL_MD5
|
||||
)
|
||||
self.inpaint = load_jit_model(
|
||||
ZITS_INPAINT_MODEL_URL, device, ZITS_INPAINT_MODEL_MD5
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def download():
|
||||
download_model(ZITS_WIRE_FRAME_MODEL_URL, ZITS_WIRE_FRAME_MODEL_MD5)
|
||||
download_model(ZITS_EDGE_LINE_MODEL_URL, ZITS_EDGE_LINE_MODEL_MD5)
|
||||
download_model(
|
||||
ZITS_STRUCTURE_UPSAMPLE_MODEL_URL, ZITS_STRUCTURE_UPSAMPLE_MODEL_MD5
|
||||
)
|
||||
download_model(ZITS_INPAINT_MODEL_URL, ZITS_INPAINT_MODEL_MD5)
|
||||
|
||||
@staticmethod
|
||||
def is_downloaded() -> bool:
|
||||
model_paths = [
|
||||
get_cache_path_by_url(ZITS_WIRE_FRAME_MODEL_URL),
|
||||
get_cache_path_by_url(ZITS_EDGE_LINE_MODEL_URL),
|
||||
get_cache_path_by_url(ZITS_STRUCTURE_UPSAMPLE_MODEL_URL),
|
||||
get_cache_path_by_url(ZITS_INPAINT_MODEL_URL),
|
||||
]
|
||||
return all([os.path.exists(it) for it in model_paths])
|
||||
|
||||
def wireframe_edge_and_line(self, items, enable: bool):
|
||||
# 最终向 items 中添加 edge 和 line key
|
||||
if not enable:
|
||||
items["edge"] = torch.zeros_like(items["masks"])
|
||||
items["line"] = torch.zeros_like(items["masks"])
|
||||
return
|
||||
|
||||
start = time.time()
|
||||
try:
|
||||
line_256 = self.wireframe_forward(
|
||||
items["img_512"],
|
||||
h=256,
|
||||
w=256,
|
||||
masks=items["mask_512"],
|
||||
mask_th=0.85,
|
||||
)
|
||||
except:
|
||||
line_256 = torch.zeros_like(items["mask_256"])
|
||||
|
||||
print(f"wireframe_forward time: {(time.time() - start) * 1000:.2f}ms")
|
||||
|
||||
# np_line = (line[0][0].numpy() * 255).astype(np.uint8)
|
||||
# cv2.imwrite("line.jpg", np_line)
|
||||
|
||||
start = time.time()
|
||||
edge_pred, line_pred = self.sample_edge_line_logits(
|
||||
context=[items["img_256"], items["edge_256"], line_256],
|
||||
mask=items["mask_256"].clone(),
|
||||
iterations=self.sample_edge_line_iterations,
|
||||
add_v=0.05,
|
||||
mul_v=4,
|
||||
)
|
||||
print(f"sample_edge_line_logits time: {(time.time() - start) * 1000:.2f}ms")
|
||||
|
||||
# np_edge_pred = (edge_pred[0][0].numpy() * 255).astype(np.uint8)
|
||||
# cv2.imwrite("edge_pred.jpg", np_edge_pred)
|
||||
# np_line_pred = (line_pred[0][0].numpy() * 255).astype(np.uint8)
|
||||
# cv2.imwrite("line_pred.jpg", np_line_pred)
|
||||
# exit()
|
||||
|
||||
input_size = min(items["h"], items["w"])
|
||||
if input_size != 256 and input_size > 256:
|
||||
while edge_pred.shape[2] < input_size:
|
||||
edge_pred = self.structure_upsample(edge_pred)
|
||||
edge_pred = torch.sigmoid((edge_pred + 2) * 2)
|
||||
|
||||
line_pred = self.structure_upsample(line_pred)
|
||||
line_pred = torch.sigmoid((line_pred + 2) * 2)
|
||||
|
||||
edge_pred = F.interpolate(
|
||||
edge_pred,
|
||||
size=(input_size, input_size),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
line_pred = F.interpolate(
|
||||
line_pred,
|
||||
size=(input_size, input_size),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
# np_edge_pred = (edge_pred[0][0].numpy() * 255).astype(np.uint8)
|
||||
# cv2.imwrite("edge_pred_upsample.jpg", np_edge_pred)
|
||||
# np_line_pred = (line_pred[0][0].numpy() * 255).astype(np.uint8)
|
||||
# cv2.imwrite("line_pred_upsample.jpg", np_line_pred)
|
||||
# exit()
|
||||
|
||||
items["edge"] = edge_pred.detach()
|
||||
items["line"] = line_pred.detach()
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, image, mask, config: InpaintRequest):
|
||||
"""Input images and output images have same size
|
||||
images: [H, W, C] RGB
|
||||
masks: [H, W]
|
||||
return: BGR IMAGE
|
||||
"""
|
||||
mask = mask[:, :, 0]
|
||||
items = load_image(image, mask, device=self.device)
|
||||
|
||||
self.wireframe_edge_and_line(items, config.zits_wireframe)
|
||||
|
||||
inpainted_image = self.inpaint(
|
||||
items["images"],
|
||||
items["masks"],
|
||||
items["edge"],
|
||||
items["line"],
|
||||
items["rel_pos"],
|
||||
items["direct"],
|
||||
)
|
||||
|
||||
inpainted_image = inpainted_image * 255.0
|
||||
inpainted_image = (
|
||||
inpainted_image.cpu().permute(0, 2, 3, 1)[0].numpy().astype(np.uint8)
|
||||
)
|
||||
inpainted_image = inpainted_image[:, :, ::-1]
|
||||
|
||||
# cv2.imwrite("inpainted.jpg", inpainted_image)
|
||||
# exit()
|
||||
|
||||
return inpainted_image
|
||||
|
||||
def wireframe_forward(self, images, h, w, masks, mask_th=0.925):
|
||||
lcnn_mean = torch.tensor([109.730, 103.832, 98.681]).reshape(1, 3, 1, 1)
|
||||
lcnn_std = torch.tensor([22.275, 22.124, 23.229]).reshape(1, 3, 1, 1)
|
||||
images = images * 255.0
|
||||
# the masks value of lcnn is 127.5
|
||||
masked_images = images * (1 - masks) + torch.ones_like(images) * masks * 127.5
|
||||
masked_images = (masked_images - lcnn_mean) / lcnn_std
|
||||
|
||||
def to_int(x):
|
||||
return tuple(map(int, x))
|
||||
|
||||
lines_tensor = []
|
||||
lmap = np.zeros((h, w))
|
||||
|
||||
output_masked = self.wireframe(masked_images)
|
||||
|
||||
output_masked = to_device(output_masked, "cpu")
|
||||
if output_masked["num_proposals"] == 0:
|
||||
lines_masked = []
|
||||
scores_masked = []
|
||||
else:
|
||||
lines_masked = output_masked["lines_pred"].numpy()
|
||||
lines_masked = [
|
||||
[line[1] * h, line[0] * w, line[3] * h, line[2] * w]
|
||||
for line in lines_masked
|
||||
]
|
||||
scores_masked = output_masked["lines_score"].numpy()
|
||||
|
||||
for line, score in zip(lines_masked, scores_masked):
|
||||
if score > mask_th:
|
||||
try:
|
||||
import skimage
|
||||
|
||||
rr, cc, value = skimage.draw.line_aa(
|
||||
*to_int(line[0:2]), *to_int(line[2:4])
|
||||
)
|
||||
lmap[rr, cc] = np.maximum(lmap[rr, cc], value)
|
||||
except:
|
||||
cv2.line(
|
||||
lmap,
|
||||
to_int(line[0:2][::-1]),
|
||||
to_int(line[2:4][::-1]),
|
||||
(1, 1, 1),
|
||||
1,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
|
||||
lmap = np.clip(lmap * 255, 0, 255).astype(np.uint8)
|
||||
lines_tensor.append(to_tensor(lmap).unsqueeze(0))
|
||||
|
||||
lines_tensor = torch.cat(lines_tensor, dim=0)
|
||||
return lines_tensor.detach().to(self.device)
|
||||
|
||||
def sample_edge_line_logits(
|
||||
self, context, mask=None, iterations=1, add_v=0, mul_v=4
|
||||
):
|
||||
[img, edge, line] = context
|
||||
|
||||
img = img * (1 - mask)
|
||||
edge = edge * (1 - mask)
|
||||
line = line * (1 - mask)
|
||||
|
||||
for i in range(iterations):
|
||||
edge_logits, line_logits = self.edge_line(img, edge, line, masks=mask)
|
||||
|
||||
edge_pred = torch.sigmoid(edge_logits)
|
||||
line_pred = torch.sigmoid((line_logits + add_v) * mul_v)
|
||||
edge = edge + edge_pred * mask
|
||||
edge[edge >= 0.25] = 1
|
||||
edge[edge < 0.25] = 0
|
||||
line = line + line_pred * mask
|
||||
|
||||
b, _, h, w = edge_pred.shape
|
||||
edge_pred = edge_pred.reshape(b, -1, 1)
|
||||
line_pred = line_pred.reshape(b, -1, 1)
|
||||
mask = mask.reshape(b, -1)
|
||||
|
||||
edge_probs = torch.cat([1 - edge_pred, edge_pred], dim=-1)
|
||||
line_probs = torch.cat([1 - line_pred, line_pred], dim=-1)
|
||||
edge_probs[:, :, 1] += 0.5
|
||||
line_probs[:, :, 1] += 0.5
|
||||
edge_max_probs = edge_probs.max(dim=-1)[0] + (1 - mask) * (-100)
|
||||
line_max_probs = line_probs.max(dim=-1)[0] + (1 - mask) * (-100)
|
||||
|
||||
indices = torch.sort(
|
||||
edge_max_probs + line_max_probs, dim=-1, descending=True
|
||||
)[1]
|
||||
|
||||
for ii in range(b):
|
||||
keep = int((i + 1) / iterations * torch.sum(mask[ii, ...]))
|
||||
|
||||
assert torch.sum(mask[ii][indices[ii, :keep]]) == keep, "Error!!!"
|
||||
mask[ii][indices[ii, :keep]] = 0
|
||||
|
||||
mask = mask.reshape(b, 1, h, w)
|
||||
edge = edge * (1 - mask)
|
||||
line = line * (1 - mask)
|
||||
|
||||
edge, line = edge.to(torch.float32), line.to(torch.float32)
|
||||
return edge, line
|
||||
99
iopaint/model_info.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import computed_field, BaseModel
|
||||
|
||||
from iopaint.const import (
|
||||
SDXL_CONTROLNET_CHOICES,
|
||||
SD2_CONTROLNET_CHOICES,
|
||||
SD_CONTROLNET_CHOICES,
|
||||
)
|
||||
from iopaint.model import InstructPix2Pix, Kandinsky22, PowerPaint, SD2
|
||||
from iopaint.schema import ModelType
|
||||
|
||||
|
||||
class ModelInfo(BaseModel):
|
||||
name: str
|
||||
path: str
|
||||
model_type: ModelType
|
||||
is_single_file_diffusers: bool = False
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def need_prompt(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
] or self.name in [
|
||||
InstructPix2Pix.name,
|
||||
Kandinsky22.name,
|
||||
PowerPaint.name,
|
||||
]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def controlnets(self) -> List[str]:
|
||||
if self.model_type in [
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
]:
|
||||
return SDXL_CONTROLNET_CHOICES
|
||||
if self.model_type in [ModelType.DIFFUSERS_SD, ModelType.DIFFUSERS_SD_INPAINT]:
|
||||
if self.name in [SD2.name]:
|
||||
return SD2_CONTROLNET_CHOICES
|
||||
else:
|
||||
return SD_CONTROLNET_CHOICES
|
||||
if self.name == PowerPaint.name:
|
||||
return SD_CONTROLNET_CHOICES
|
||||
return []
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def support_strength(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def support_outpainting(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
] or self.name in [Kandinsky22.name, PowerPaint.name]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def support_lcm_lora(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def support_controlnet(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
] or self.name in [PowerPaint.name]
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def support_freeu(self) -> bool:
|
||||
return self.model_type in [
|
||||
ModelType.DIFFUSERS_SD,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
] or self.name in [InstructPix2Pix.name]
|
||||
173
iopaint/model_manager.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from typing import List, Dict
|
||||
|
||||
import torch
|
||||
from loguru import logger
|
||||
import numpy as np
|
||||
|
||||
from iopaint.download import scan_models
|
||||
from iopaint.helper import switch_mps_device
|
||||
from iopaint.model import models, ControlNet, SD, SDXL
|
||||
from iopaint.model.utils import torch_gc
|
||||
from iopaint.model_info import ModelInfo, ModelType
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
|
||||
class ModelManager:
|
||||
def __init__(self, name: str, device: torch.device, **kwargs):
|
||||
self.name = name
|
||||
self.device = device
|
||||
self.kwargs = kwargs
|
||||
self.available_models: Dict[str, ModelInfo] = {}
|
||||
self.scan_models()
|
||||
|
||||
self.enable_controlnet = kwargs.get("enable_controlnet", False)
|
||||
controlnet_method = kwargs.get("controlnet_method", None)
|
||||
if (
|
||||
controlnet_method is None
|
||||
and name in self.available_models
|
||||
and self.available_models[name].support_controlnet
|
||||
):
|
||||
controlnet_method = self.available_models[name].controlnets[0]
|
||||
self.controlnet_method = controlnet_method
|
||||
self.model = self.init_model(name, device, **kwargs)
|
||||
|
||||
@property
|
||||
def current_model(self) -> ModelInfo:
|
||||
return self.available_models[self.name]
|
||||
|
||||
def init_model(self, name: str, device, **kwargs):
|
||||
logger.info(f"Loading model: {name}")
|
||||
if name not in self.available_models:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported model: {name}. Available models: {self.available_models.keys()}"
|
||||
)
|
||||
|
||||
model_info = self.available_models[name]
|
||||
kwargs = {
|
||||
**kwargs,
|
||||
"model_info": model_info,
|
||||
"enable_controlnet": self.enable_controlnet,
|
||||
"controlnet_method": self.controlnet_method,
|
||||
}
|
||||
|
||||
if model_info.support_controlnet and self.enable_controlnet:
|
||||
return ControlNet(device, **kwargs)
|
||||
elif model_info.name in models:
|
||||
return models[name](device, **kwargs)
|
||||
else:
|
||||
if model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SD_INPAINT,
|
||||
ModelType.DIFFUSERS_SD,
|
||||
]:
|
||||
return SD(device, **kwargs)
|
||||
|
||||
if model_info.model_type in [
|
||||
ModelType.DIFFUSERS_SDXL_INPAINT,
|
||||
ModelType.DIFFUSERS_SDXL,
|
||||
]:
|
||||
return SDXL(device, **kwargs)
|
||||
|
||||
raise NotImplementedError(f"Unsupported model: {name}")
|
||||
|
||||
def __call__(self, image, mask, config: InpaintRequest):
|
||||
"""
|
||||
|
||||
Args:
|
||||
image: [H, W, C] RGB
|
||||
mask: [H, W, 1] 255 means area to repaint
|
||||
config:
|
||||
|
||||
Returns:
|
||||
BGR image
|
||||
"""
|
||||
self.switch_controlnet_method(config)
|
||||
self.enable_disable_freeu(config)
|
||||
self.enable_disable_lcm_lora(config)
|
||||
return self.model(image, mask, config).astype(np.uint8)
|
||||
|
||||
def scan_models(self) -> List[ModelInfo]:
|
||||
available_models = scan_models()
|
||||
self.available_models = {it.name: it for it in available_models}
|
||||
return available_models
|
||||
|
||||
def switch(self, new_name: str):
|
||||
if new_name == self.name:
|
||||
return
|
||||
|
||||
old_name = self.name
|
||||
old_controlnet_method = self.controlnet_method
|
||||
self.name = new_name
|
||||
|
||||
if (
|
||||
self.available_models[new_name].support_controlnet
|
||||
and self.controlnet_method
|
||||
not in self.available_models[new_name].controlnets
|
||||
):
|
||||
self.controlnet_method = self.available_models[new_name].controlnets[0]
|
||||
try:
|
||||
# TODO: enable/disable controlnet without reload model
|
||||
del self.model
|
||||
torch_gc()
|
||||
|
||||
self.model = self.init_model(
|
||||
new_name, switch_mps_device(new_name, self.device), **self.kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
self.name = old_name
|
||||
self.controlnet_method = old_controlnet_method
|
||||
logger.info(f"Switch model from {old_name} to {new_name} failed, rollback")
|
||||
self.model = self.init_model(
|
||||
old_name, switch_mps_device(old_name, self.device), **self.kwargs
|
||||
)
|
||||
raise e
|
||||
|
||||
def switch_controlnet_method(self, config):
|
||||
if not self.available_models[self.name].support_controlnet:
|
||||
return
|
||||
|
||||
if (
|
||||
self.enable_controlnet
|
||||
and config.controlnet_method
|
||||
and self.controlnet_method != config.controlnet_method
|
||||
):
|
||||
old_controlnet_method = self.controlnet_method
|
||||
self.controlnet_method = config.controlnet_method
|
||||
self.model.switch_controlnet_method(config.controlnet_method)
|
||||
logger.info(
|
||||
f"Switch Controlnet method from {old_controlnet_method} to {config.controlnet_method}"
|
||||
)
|
||||
elif self.enable_controlnet != config.enable_controlnet:
|
||||
self.enable_controlnet = config.enable_controlnet
|
||||
self.controlnet_method = config.controlnet_method
|
||||
|
||||
self.model = self.init_model(
|
||||
self.name, switch_mps_device(self.name, self.device), **self.kwargs
|
||||
)
|
||||
if not config.enable_controlnet:
|
||||
logger.info(f"Disable controlnet")
|
||||
else:
|
||||
logger.info(f"Enable controlnet: {config.controlnet_method}")
|
||||
|
||||
def enable_disable_freeu(self, config: InpaintRequest):
|
||||
if str(self.model.device) == "mps":
|
||||
return
|
||||
|
||||
if self.available_models[self.name].support_freeu:
|
||||
if config.sd_freeu:
|
||||
freeu_config = config.sd_freeu_config
|
||||
self.model.model.enable_freeu(
|
||||
s1=freeu_config.s1,
|
||||
s2=freeu_config.s2,
|
||||
b1=freeu_config.b1,
|
||||
b2=freeu_config.b2,
|
||||
)
|
||||
else:
|
||||
self.model.model.disable_freeu()
|
||||
|
||||
def enable_disable_lcm_lora(self, config: InpaintRequest):
|
||||
if self.available_models[self.name].support_lcm_lora:
|
||||
if config.sd_lcm_lora:
|
||||
if not self.model.model.get_list_adapters():
|
||||
self.model.model.load_lora_weights(self.model.lcm_lora_id)
|
||||
else:
|
||||
self.model.model.disable_lora()
|
||||
73
iopaint/plugins/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from typing import Dict
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .anime_seg import AnimeSeg
|
||||
from .gfpgan_plugin import GFPGANPlugin
|
||||
from .interactive_seg import InteractiveSeg
|
||||
from .realesrgan import RealESRGANUpscaler
|
||||
from .remove_bg import RemoveBG
|
||||
from .restoreformer import RestoreFormerPlugin
|
||||
from ..const import InteractiveSegModel, Device, RealESRGANModel
|
||||
|
||||
|
||||
def build_plugins(
|
||||
enable_interactive_seg: bool,
|
||||
interactive_seg_model: InteractiveSegModel,
|
||||
interactive_seg_device: Device,
|
||||
enable_remove_bg: bool,
|
||||
enable_anime_seg: bool,
|
||||
enable_realesrgan: bool,
|
||||
realesrgan_device: Device,
|
||||
realesrgan_model: RealESRGANModel,
|
||||
enable_gfpgan: bool,
|
||||
gfpgan_device: Device,
|
||||
enable_restoreformer: bool,
|
||||
restoreformer_device: Device,
|
||||
no_half: bool,
|
||||
) -> Dict:
|
||||
plugins = {}
|
||||
if enable_interactive_seg:
|
||||
logger.info(f"Initialize {InteractiveSeg.name} plugin")
|
||||
plugins[InteractiveSeg.name] = InteractiveSeg(
|
||||
interactive_seg_model, interactive_seg_device
|
||||
)
|
||||
|
||||
if enable_remove_bg:
|
||||
logger.info(f"Initialize {RemoveBG.name} plugin")
|
||||
plugins[RemoveBG.name] = RemoveBG()
|
||||
|
||||
if enable_anime_seg:
|
||||
logger.info(f"Initialize {AnimeSeg.name} plugin")
|
||||
plugins[AnimeSeg.name] = AnimeSeg()
|
||||
|
||||
if enable_realesrgan:
|
||||
logger.info(
|
||||
f"Initialize {RealESRGANUpscaler.name} plugin: {realesrgan_model}, {realesrgan_device}"
|
||||
)
|
||||
plugins[RealESRGANUpscaler.name] = RealESRGANUpscaler(
|
||||
realesrgan_model,
|
||||
realesrgan_device,
|
||||
no_half=no_half,
|
||||
)
|
||||
|
||||
if enable_gfpgan:
|
||||
logger.info(f"Initialize {GFPGANPlugin.name} plugin")
|
||||
if enable_realesrgan:
|
||||
logger.info("Use realesrgan as GFPGAN background upscaler")
|
||||
else:
|
||||
logger.info(
|
||||
f"GFPGAN no background upscaler, use --enable-realesrgan to enable it"
|
||||
)
|
||||
plugins[GFPGANPlugin.name] = GFPGANPlugin(
|
||||
gfpgan_device,
|
||||
upscaler=plugins.get(RealESRGANUpscaler.name, None),
|
||||
)
|
||||
|
||||
if enable_restoreformer:
|
||||
logger.info(f"Initialize {RestoreFormerPlugin.name} plugin")
|
||||
plugins[RestoreFormerPlugin.name] = RestoreFormerPlugin(
|
||||
restoreformer_device,
|
||||
upscaler=plugins.get(RealESRGANUpscaler.name, None),
|
||||
)
|
||||
return plugins
|
||||
462
iopaint/plugins/anime_seg.py
Normal file
@@ -0,0 +1,462 @@
|
||||
import cv2
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from iopaint.helper import load_model
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class REBNCONV(nn.Module):
|
||||
def __init__(self, in_ch=3, out_ch=3, dirate=1, stride=1):
|
||||
super(REBNCONV, self).__init__()
|
||||
|
||||
self.conv_s1 = nn.Conv2d(
|
||||
in_ch, out_ch, 3, padding=1 * dirate, dilation=1 * dirate, stride=stride
|
||||
)
|
||||
self.bn_s1 = nn.BatchNorm2d(out_ch)
|
||||
self.relu_s1 = nn.ReLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
xout = self.relu_s1(self.bn_s1(self.conv_s1(hx)))
|
||||
|
||||
return xout
|
||||
|
||||
|
||||
## upsample tensor 'src' to have the same spatial size with tensor 'tar'
|
||||
def _upsample_like(src, tar):
|
||||
src = F.interpolate(src, size=tar.shape[2:], mode="bilinear", align_corners=False)
|
||||
|
||||
return src
|
||||
|
||||
|
||||
### RSU-7 ###
|
||||
class RSU7(nn.Module):
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3, img_size=512):
|
||||
super(RSU7, self).__init__()
|
||||
|
||||
self.in_ch = in_ch
|
||||
self.mid_ch = mid_ch
|
||||
self.out_ch = out_ch
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1) ## 1 -> 1/2
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool5 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv6 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv7 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv6d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv5d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
b, c, h, w = x.shape
|
||||
|
||||
hx = x
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
hx = self.pool4(hx4)
|
||||
|
||||
hx5 = self.rebnconv5(hx)
|
||||
hx = self.pool5(hx5)
|
||||
|
||||
hx6 = self.rebnconv6(hx)
|
||||
|
||||
hx7 = self.rebnconv7(hx6)
|
||||
|
||||
hx6d = self.rebnconv6d(torch.cat((hx7, hx6), 1))
|
||||
hx6dup = _upsample_like(hx6d, hx5)
|
||||
|
||||
hx5d = self.rebnconv5d(torch.cat((hx6dup, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-6 ###
|
||||
class RSU6(nn.Module):
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU6, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv6 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv5d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
hx = self.pool4(hx4)
|
||||
|
||||
hx5 = self.rebnconv5(hx)
|
||||
|
||||
hx6 = self.rebnconv6(hx5)
|
||||
|
||||
hx5d = self.rebnconv5d(torch.cat((hx6, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-5 ###
|
||||
class RSU5(nn.Module):
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU5, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
|
||||
hx5 = self.rebnconv5(hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-4 ###
|
||||
class RSU4(nn.Module):
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU4, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
|
||||
hx4 = self.rebnconv4(hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-4F ###
|
||||
class RSU4F(nn.Module):
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU4F, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=4)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=8)
|
||||
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=4)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=2)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx2 = self.rebnconv2(hx1)
|
||||
hx3 = self.rebnconv3(hx2)
|
||||
|
||||
hx4 = self.rebnconv4(hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4, hx3), 1))
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3d, hx2), 1))
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2d, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
class ISNetDIS(nn.Module):
|
||||
def __init__(self, in_ch=3, out_ch=1):
|
||||
super(ISNetDIS, self).__init__()
|
||||
|
||||
self.conv_in = nn.Conv2d(in_ch, 64, 3, stride=2, padding=1)
|
||||
self.pool_in = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage1 = RSU7(64, 32, 64)
|
||||
self.pool12 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage2 = RSU6(64, 32, 128)
|
||||
self.pool23 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage3 = RSU5(128, 64, 256)
|
||||
self.pool34 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage4 = RSU4(256, 128, 512)
|
||||
self.pool45 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage5 = RSU4F(512, 256, 512)
|
||||
self.pool56 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage6 = RSU4F(512, 256, 512)
|
||||
|
||||
# decoder
|
||||
self.stage5d = RSU4F(1024, 256, 512)
|
||||
self.stage4d = RSU4(1024, 128, 256)
|
||||
self.stage3d = RSU5(512, 64, 128)
|
||||
self.stage2d = RSU6(256, 32, 64)
|
||||
self.stage1d = RSU7(128, 16, 64)
|
||||
|
||||
self.side1 = nn.Conv2d(64, out_ch, 3, padding=1)
|
||||
|
||||
def forward(self, x):
|
||||
hx = x
|
||||
|
||||
hxin = self.conv_in(hx)
|
||||
hx = self.pool_in(hxin)
|
||||
|
||||
# stage 1
|
||||
hx1 = self.stage1(hxin)
|
||||
hx = self.pool12(hx1)
|
||||
|
||||
# stage 2
|
||||
hx2 = self.stage2(hx)
|
||||
hx = self.pool23(hx2)
|
||||
|
||||
# stage 3
|
||||
hx3 = self.stage3(hx)
|
||||
hx = self.pool34(hx3)
|
||||
|
||||
# stage 4
|
||||
hx4 = self.stage4(hx)
|
||||
hx = self.pool45(hx4)
|
||||
|
||||
# stage 5
|
||||
hx5 = self.stage5(hx)
|
||||
hx = self.pool56(hx5)
|
||||
|
||||
# stage 6
|
||||
hx6 = self.stage6(hx)
|
||||
hx6up = _upsample_like(hx6, hx5)
|
||||
|
||||
# -------------------- decoder --------------------
|
||||
hx5d = self.stage5d(torch.cat((hx6up, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.stage4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.stage3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.stage2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.stage1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
# side output
|
||||
d1 = self.side1(hx1d)
|
||||
d1 = _upsample_like(d1, x)
|
||||
return d1.sigmoid()
|
||||
|
||||
|
||||
# 从小到大
|
||||
ANIME_SEG_MODELS = {
|
||||
"url": "https://github.com/Sanster/models/releases/download/isnetis/isnetis.pth",
|
||||
"md5": "5f25479076b73074730ab8de9e8f2051",
|
||||
}
|
||||
|
||||
|
||||
class AnimeSeg(BasePlugin):
|
||||
# Model from: https://github.com/SkyTNT/anime-segmentation
|
||||
name = "AnimeSeg"
|
||||
support_gen_image = True
|
||||
support_gen_mask = True
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.model = load_model(
|
||||
ISNetDIS(),
|
||||
ANIME_SEG_MODELS["url"],
|
||||
"cpu",
|
||||
ANIME_SEG_MODELS["md5"],
|
||||
)
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
mask = self.forward(rgb_np_img)
|
||||
mask = Image.fromarray(mask, mode="L")
|
||||
h0, w0 = rgb_np_img.shape[0], rgb_np_img.shape[1]
|
||||
empty = Image.new("RGBA", (w0, h0), 0)
|
||||
img = Image.fromarray(rgb_np_img)
|
||||
cutout = Image.composite(img, empty, mask)
|
||||
return np.asarray(cutout)
|
||||
|
||||
def gen_mask(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
return self.forward(rgb_np_img)
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, rgb_np_img):
|
||||
s = 1024
|
||||
|
||||
h0, w0 = h, w = rgb_np_img.shape[0], rgb_np_img.shape[1]
|
||||
if h > w:
|
||||
h, w = s, int(s * w / h)
|
||||
else:
|
||||
h, w = int(s * h / w), s
|
||||
ph, pw = s - h, s - w
|
||||
tmpImg = np.zeros([s, s, 3], dtype=np.float32)
|
||||
tmpImg[ph // 2 : ph // 2 + h, pw // 2 : pw // 2 + w] = (
|
||||
cv2.resize(rgb_np_img, (w, h)) / 255
|
||||
)
|
||||
tmpImg = tmpImg.transpose((2, 0, 1))
|
||||
tmpImg = torch.from_numpy(tmpImg).unsqueeze(0).type(torch.FloatTensor)
|
||||
mask = self.model(tmpImg)
|
||||
mask = mask[0, :, ph // 2 : ph // 2 + h, pw // 2 : pw // 2 + w]
|
||||
mask = cv2.resize(mask.cpu().numpy().transpose((1, 2, 0)), (w0, h0))
|
||||
return (mask * 255).astype("uint8")
|
||||
27
iopaint/plugins/base_plugin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from loguru import logger
|
||||
import numpy as np
|
||||
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
name: str
|
||||
support_gen_image: bool = False
|
||||
support_gen_mask: bool = False
|
||||
|
||||
def __init__(self):
|
||||
err_msg = self.check_dep()
|
||||
if err_msg:
|
||||
logger.error(err_msg)
|
||||
exit(-1)
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
# return RGBA np image or BGR np image
|
||||
...
|
||||
|
||||
def gen_mask(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
# return GRAY or BGR np image, 255 means foreground, 0 means background
|
||||
...
|
||||
|
||||
def check_dep(self):
|
||||
...
|
||||
74
iopaint/plugins/gfpgan_plugin.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import download_model
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class GFPGANPlugin(BasePlugin):
|
||||
name = "GFPGAN"
|
||||
support_gen_image = True
|
||||
|
||||
def __init__(self, device, upscaler=None):
|
||||
super().__init__()
|
||||
from .gfpganer import MyGFPGANer
|
||||
|
||||
url = "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth"
|
||||
model_md5 = "94d735072630ab734561130a47bc44f8"
|
||||
model_path = download_model(url, model_md5)
|
||||
logger.info(f"GFPGAN model path: {model_path}")
|
||||
|
||||
import facexlib
|
||||
|
||||
if hasattr(facexlib.detection.retinaface, "device"):
|
||||
facexlib.detection.retinaface.device = device
|
||||
|
||||
# Use GFPGAN for face enhancement
|
||||
self.face_enhancer = MyGFPGANer(
|
||||
model_path=model_path,
|
||||
upscale=1,
|
||||
arch="clean",
|
||||
channel_multiplier=2,
|
||||
device=device,
|
||||
bg_upsampler=upscaler.model if upscaler is not None else None,
|
||||
)
|
||||
self.face_enhancer.face_helper.face_det.mean_tensor.to(device)
|
||||
self.face_enhancer.face_helper.face_det = (
|
||||
self.face_enhancer.face_helper.face_det.to(device)
|
||||
)
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
weight = 0.5
|
||||
bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR)
|
||||
logger.info(f"GFPGAN input shape: {bgr_np_img.shape}")
|
||||
_, _, bgr_output = self.face_enhancer.enhance(
|
||||
bgr_np_img,
|
||||
has_aligned=False,
|
||||
only_center_face=False,
|
||||
paste_back=True,
|
||||
weight=weight,
|
||||
)
|
||||
logger.info(f"GFPGAN output shape: {bgr_output.shape}")
|
||||
|
||||
# try:
|
||||
# if scale != 2:
|
||||
# interpolation = cv2.INTER_AREA if scale < 2 else cv2.INTER_LANCZOS4
|
||||
# h, w = img.shape[0:2]
|
||||
# output = cv2.resize(
|
||||
# output,
|
||||
# (int(w * scale / 2), int(h * scale / 2)),
|
||||
# interpolation=interpolation,
|
||||
# )
|
||||
# except Exception as error:
|
||||
# print("wrong scale input.", error)
|
||||
return bgr_output
|
||||
|
||||
def check_dep(self):
|
||||
try:
|
||||
import gfpgan
|
||||
except ImportError:
|
||||
return (
|
||||
"gfpgan is not installed, please install it first. pip install gfpgan"
|
||||
)
|
||||
84
iopaint/plugins/gfpganer.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
|
||||
import torch
|
||||
from facexlib.utils.face_restoration_helper import FaceRestoreHelper
|
||||
from gfpgan import GFPGANv1Clean, GFPGANer
|
||||
from torch.hub import get_dir
|
||||
|
||||
|
||||
class MyGFPGANer(GFPGANer):
|
||||
"""Helper for restoration with GFPGAN.
|
||||
|
||||
It will detect and crop faces, and then resize the faces to 512x512.
|
||||
GFPGAN is used to restored the resized faces.
|
||||
The background is upsampled with the bg_upsampler.
|
||||
Finally, the faces will be pasted back to the upsample background image.
|
||||
|
||||
Args:
|
||||
model_path (str): The path to the GFPGAN model. It can be urls (will first download it automatically).
|
||||
upscale (float): The upscale of the final output. Default: 2.
|
||||
arch (str): The GFPGAN architecture. Option: clean | original. Default: clean.
|
||||
channel_multiplier (int): Channel multiplier for large networks of StyleGAN2. Default: 2.
|
||||
bg_upsampler (nn.Module): The upsampler for the background. Default: None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_path,
|
||||
upscale=2,
|
||||
arch="clean",
|
||||
channel_multiplier=2,
|
||||
bg_upsampler=None,
|
||||
device=None,
|
||||
):
|
||||
self.upscale = upscale
|
||||
self.bg_upsampler = bg_upsampler
|
||||
|
||||
# initialize model
|
||||
self.device = (
|
||||
torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
if device is None
|
||||
else device
|
||||
)
|
||||
# initialize the GFP-GAN
|
||||
if arch == "clean":
|
||||
self.gfpgan = GFPGANv1Clean(
|
||||
out_size=512,
|
||||
num_style_feat=512,
|
||||
channel_multiplier=channel_multiplier,
|
||||
decoder_load_path=None,
|
||||
fix_decoder=False,
|
||||
num_mlp=8,
|
||||
input_is_latent=True,
|
||||
different_w=True,
|
||||
narrow=1,
|
||||
sft_half=True,
|
||||
)
|
||||
elif arch == "RestoreFormer":
|
||||
from gfpgan.archs.restoreformer_arch import RestoreFormer
|
||||
|
||||
self.gfpgan = RestoreFormer()
|
||||
|
||||
hub_dir = get_dir()
|
||||
model_dir = os.path.join(hub_dir, "checkpoints")
|
||||
|
||||
# initialize face helper
|
||||
self.face_helper = FaceRestoreHelper(
|
||||
upscale,
|
||||
face_size=512,
|
||||
crop_ratio=(1, 1),
|
||||
det_model="retinaface_resnet50",
|
||||
save_ext="png",
|
||||
use_parse=True,
|
||||
device=self.device,
|
||||
model_rootpath=model_dir,
|
||||
)
|
||||
|
||||
loadnet = torch.load(model_path)
|
||||
if "params_ema" in loadnet:
|
||||
keyname = "params_ema"
|
||||
else:
|
||||
keyname = "params"
|
||||
self.gfpgan.load_state_dict(loadnet[keyname], strict=True)
|
||||
self.gfpgan.eval()
|
||||
self.gfpgan = self.gfpgan.to(self.device)
|
||||
76
iopaint/plugins/interactive_seg.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import hashlib
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import download_model
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.plugins.segment_anything import SamPredictor, sam_model_registry
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
# 从小到大
|
||||
SEGMENT_ANYTHING_MODELS = {
|
||||
"vit_b": {
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
|
||||
"md5": "01ec64d29a2fca3f0661936605ae66f8",
|
||||
},
|
||||
"vit_l": {
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
|
||||
"md5": "0b3195507c641ddb6910d2bb5adee89c",
|
||||
},
|
||||
"vit_h": {
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
|
||||
"md5": "4b8939a88964f0f4ff5f5b2642c598a6",
|
||||
},
|
||||
"mobile_sam": {
|
||||
"url": "https://github.com/Sanster/models/releases/download/MobileSAM/mobile_sam.pt",
|
||||
"md5": "f3c0d8cda613564d499310dab6c812cd",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class InteractiveSeg(BasePlugin):
|
||||
name = "InteractiveSeg"
|
||||
support_gen_mask = True
|
||||
|
||||
def __init__(self, model_name, device):
|
||||
super().__init__()
|
||||
model_path = download_model(
|
||||
SEGMENT_ANYTHING_MODELS[model_name]["url"],
|
||||
SEGMENT_ANYTHING_MODELS[model_name]["md5"],
|
||||
)
|
||||
logger.info(f"SegmentAnything model path: {model_path}")
|
||||
self.predictor = SamPredictor(
|
||||
sam_model_registry[model_name](checkpoint=model_path).to(device)
|
||||
)
|
||||
self.prev_img_md5 = None
|
||||
|
||||
def gen_mask(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
img_md5 = hashlib.md5(req.image.encode("utf-8")).hexdigest()
|
||||
return self.forward(rgb_np_img, req.clicks, img_md5)
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, rgb_np_img, clicks: List[List], img_md5: str):
|
||||
input_point = []
|
||||
input_label = []
|
||||
for click in clicks:
|
||||
x = click[0]
|
||||
y = click[1]
|
||||
input_point.append([x, y])
|
||||
input_label.append(click[2])
|
||||
|
||||
if img_md5 and img_md5 != self.prev_img_md5:
|
||||
self.prev_img_md5 = img_md5
|
||||
self.predictor.set_image(rgb_np_img)
|
||||
|
||||
masks, scores, _ = self.predictor.predict(
|
||||
point_coords=np.array(input_point),
|
||||
point_labels=np.array(input_label),
|
||||
multimask_output=False,
|
||||
)
|
||||
mask = masks[0].astype(np.uint8) * 255
|
||||
return mask
|
||||
100
iopaint/plugins/realesrgan.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from enum import Enum
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.const import RealESRGANModel
|
||||
from iopaint.helper import download_model
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class RealESRGANUpscaler(BasePlugin):
|
||||
name = "RealESRGAN"
|
||||
support_gen_image = True
|
||||
|
||||
def __init__(self, name, device, no_half=False):
|
||||
super().__init__()
|
||||
from basicsr.archs.rrdbnet_arch import RRDBNet
|
||||
from realesrgan import RealESRGANer
|
||||
from realesrgan.archs.srvgg_arch import SRVGGNetCompact
|
||||
|
||||
REAL_ESRGAN_MODELS = {
|
||||
RealESRGANModel.realesr_general_x4v3: {
|
||||
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-general-x4v3.pth",
|
||||
"scale": 4,
|
||||
"model": lambda: SRVGGNetCompact(
|
||||
num_in_ch=3,
|
||||
num_out_ch=3,
|
||||
num_feat=64,
|
||||
num_conv=32,
|
||||
upscale=4,
|
||||
act_type="prelu",
|
||||
),
|
||||
"model_md5": "91a7644643c884ee00737db24e478156",
|
||||
},
|
||||
RealESRGANModel.RealESRGAN_x4plus: {
|
||||
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth",
|
||||
"scale": 4,
|
||||
"model": lambda: RRDBNet(
|
||||
num_in_ch=3,
|
||||
num_out_ch=3,
|
||||
num_feat=64,
|
||||
num_block=23,
|
||||
num_grow_ch=32,
|
||||
scale=4,
|
||||
),
|
||||
"model_md5": "99ec365d4afad750833258a1a24f44ca",
|
||||
},
|
||||
RealESRGANModel.RealESRGAN_x4plus_anime_6B: {
|
||||
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth",
|
||||
"scale": 4,
|
||||
"model": lambda: RRDBNet(
|
||||
num_in_ch=3,
|
||||
num_out_ch=3,
|
||||
num_feat=64,
|
||||
num_block=6,
|
||||
num_grow_ch=32,
|
||||
scale=4,
|
||||
),
|
||||
"model_md5": "d58ce384064ec1591c2ea7b79dbf47ba",
|
||||
},
|
||||
}
|
||||
if name not in REAL_ESRGAN_MODELS:
|
||||
raise ValueError(f"Unknown RealESRGAN model name: {name}")
|
||||
model_info = REAL_ESRGAN_MODELS[name]
|
||||
|
||||
model_path = download_model(model_info["url"], model_info["model_md5"])
|
||||
logger.info(f"RealESRGAN model path: {model_path}")
|
||||
|
||||
self.model = RealESRGANer(
|
||||
scale=model_info["scale"],
|
||||
model_path=model_path,
|
||||
model=model_info["model"](),
|
||||
half=True if "cuda" in str(device) and not no_half else False,
|
||||
tile=512,
|
||||
tile_pad=10,
|
||||
pre_pad=10,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR)
|
||||
logger.info(f"RealESRGAN input shape: {bgr_np_img.shape}, scale: {req.scale}")
|
||||
result = self.forward(bgr_np_img, req.scale)
|
||||
logger.info(f"RealESRGAN output shape: {result.shape}")
|
||||
return result
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, bgr_np_img, scale: float):
|
||||
# 输出是 BGR
|
||||
upsampled = self.model.enhance(bgr_np_img, outscale=scale)[0]
|
||||
return upsampled
|
||||
|
||||
def check_dep(self):
|
||||
try:
|
||||
import realesrgan
|
||||
except ImportError:
|
||||
return "RealESRGAN is not installed, please install it first. pip install realesrgan"
|
||||
49
iopaint/plugins/remove_bg.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from torch.hub import get_dir
|
||||
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class RemoveBG(BasePlugin):
|
||||
name = "RemoveBG"
|
||||
support_gen_mask = True
|
||||
support_gen_image = True
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
from rembg import new_session
|
||||
|
||||
hub_dir = get_dir()
|
||||
model_dir = os.path.join(hub_dir, "checkpoints")
|
||||
os.environ["U2NET_HOME"] = model_dir
|
||||
|
||||
self.session = new_session(model_name="u2net")
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
from rembg import remove
|
||||
|
||||
bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# return BGRA image
|
||||
output = remove(bgr_np_img, session=self.session)
|
||||
return cv2.cvtColor(output, cv2.COLOR_BGRA2RGBA)
|
||||
|
||||
def gen_mask(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
from rembg import remove
|
||||
|
||||
bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# return BGR image, 255 means foreground, 0 means background
|
||||
output = remove(bgr_np_img, session=self.session, only_mask=True)
|
||||
return output
|
||||
|
||||
def check_dep(self):
|
||||
try:
|
||||
import rembg
|
||||
except ImportError:
|
||||
return (
|
||||
"RemoveBG is not installed, please install it first. pip install rembg"
|
||||
)
|
||||
57
iopaint/plugins/restoreformer.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.helper import download_model
|
||||
from iopaint.plugins.base_plugin import BasePlugin
|
||||
from iopaint.schema import RunPluginRequest
|
||||
|
||||
|
||||
class RestoreFormerPlugin(BasePlugin):
|
||||
name = "RestoreFormer"
|
||||
support_gen_image = True
|
||||
|
||||
def __init__(self, device, upscaler=None):
|
||||
super().__init__()
|
||||
from .gfpganer import MyGFPGANer
|
||||
|
||||
url = "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/RestoreFormer.pth"
|
||||
model_md5 = "eaeeff6c4a1caa1673977cb374e6f699"
|
||||
model_path = download_model(url, model_md5)
|
||||
logger.info(f"RestoreFormer model path: {model_path}")
|
||||
|
||||
import facexlib
|
||||
|
||||
if hasattr(facexlib.detection.retinaface, "device"):
|
||||
facexlib.detection.retinaface.device = device
|
||||
|
||||
self.face_enhancer = MyGFPGANer(
|
||||
model_path=model_path,
|
||||
upscale=1,
|
||||
arch="RestoreFormer",
|
||||
channel_multiplier=2,
|
||||
device=device,
|
||||
bg_upsampler=upscaler.model if upscaler is not None else None,
|
||||
)
|
||||
|
||||
def gen_image(self, rgb_np_img, req: RunPluginRequest) -> np.ndarray:
|
||||
weight = 0.5
|
||||
bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR)
|
||||
logger.info(f"RestoreFormer input shape: {bgr_np_img.shape}")
|
||||
_, _, bgr_output = self.face_enhancer.enhance(
|
||||
bgr_np_img,
|
||||
has_aligned=False,
|
||||
only_center_face=False,
|
||||
paste_back=True,
|
||||
weight=weight,
|
||||
)
|
||||
logger.info(f"RestoreFormer output shape: {bgr_output.shape}")
|
||||
return bgr_output
|
||||
|
||||
def check_dep(self):
|
||||
try:
|
||||
import gfpgan
|
||||
except ImportError:
|
||||
return (
|
||||
"gfpgan is not installed, please install it first. pip install gfpgan"
|
||||
)
|
||||
14
iopaint/plugins/segment_anything/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from .build_sam import (
|
||||
build_sam,
|
||||
build_sam_vit_h,
|
||||
build_sam_vit_l,
|
||||
build_sam_vit_b,
|
||||
sam_model_registry,
|
||||
)
|
||||
from .predictor import SamPredictor
|
||||
168
iopaint/plugins/segment_anything/build_sam.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
|
||||
from functools import partial
|
||||
|
||||
from iopaint.plugins.segment_anything.modeling.tiny_vit_sam import TinyViT
|
||||
|
||||
from .modeling import (
|
||||
ImageEncoderViT,
|
||||
MaskDecoder,
|
||||
PromptEncoder,
|
||||
Sam,
|
||||
TwoWayTransformer,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_h(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1280,
|
||||
encoder_depth=32,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[7, 15, 23, 31],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
build_sam = build_sam_vit_h
|
||||
|
||||
|
||||
def build_sam_vit_l(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1024,
|
||||
encoder_depth=24,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[5, 11, 17, 23],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_b(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=768,
|
||||
encoder_depth=12,
|
||||
encoder_num_heads=12,
|
||||
encoder_global_attn_indexes=[2, 5, 8, 11],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_t(checkpoint=None):
|
||||
prompt_embed_dim = 256
|
||||
image_size = 1024
|
||||
vit_patch_size = 16
|
||||
image_embedding_size = image_size // vit_patch_size
|
||||
mobile_sam = Sam(
|
||||
image_encoder=TinyViT(
|
||||
img_size=1024,
|
||||
in_chans=3,
|
||||
num_classes=1000,
|
||||
embed_dims=[64, 128, 160, 320],
|
||||
depths=[2, 2, 6, 2],
|
||||
num_heads=[2, 4, 5, 10],
|
||||
window_sizes=[7, 7, 14, 7],
|
||||
mlp_ratio=4.0,
|
||||
drop_rate=0.0,
|
||||
drop_path_rate=0.0,
|
||||
use_checkpoint=False,
|
||||
mbconv_expand_ratio=4.0,
|
||||
local_conv_size=3,
|
||||
layer_lr_decay=0.8,
|
||||
),
|
||||
prompt_encoder=PromptEncoder(
|
||||
embed_dim=prompt_embed_dim,
|
||||
image_embedding_size=(image_embedding_size, image_embedding_size),
|
||||
input_image_size=(image_size, image_size),
|
||||
mask_in_chans=16,
|
||||
),
|
||||
mask_decoder=MaskDecoder(
|
||||
num_multimask_outputs=3,
|
||||
transformer=TwoWayTransformer(
|
||||
depth=2,
|
||||
embedding_dim=prompt_embed_dim,
|
||||
mlp_dim=2048,
|
||||
num_heads=8,
|
||||
),
|
||||
transformer_dim=prompt_embed_dim,
|
||||
iou_head_depth=3,
|
||||
iou_head_hidden_dim=256,
|
||||
),
|
||||
pixel_mean=[123.675, 116.28, 103.53],
|
||||
pixel_std=[58.395, 57.12, 57.375],
|
||||
)
|
||||
|
||||
mobile_sam.eval()
|
||||
if checkpoint is not None:
|
||||
with open(checkpoint, "rb") as f:
|
||||
state_dict = torch.load(f)
|
||||
mobile_sam.load_state_dict(state_dict)
|
||||
return mobile_sam
|
||||
|
||||
|
||||
sam_model_registry = {
|
||||
"default": build_sam,
|
||||
"vit_h": build_sam,
|
||||
"vit_l": build_sam_vit_l,
|
||||
"vit_b": build_sam_vit_b,
|
||||
"mobile_sam": build_sam_vit_t,
|
||||
}
|
||||
|
||||
|
||||
def _build_sam(
|
||||
encoder_embed_dim,
|
||||
encoder_depth,
|
||||
encoder_num_heads,
|
||||
encoder_global_attn_indexes,
|
||||
checkpoint=None,
|
||||
):
|
||||
prompt_embed_dim = 256
|
||||
image_size = 1024
|
||||
vit_patch_size = 16
|
||||
image_embedding_size = image_size // vit_patch_size
|
||||
sam = Sam(
|
||||
image_encoder=ImageEncoderViT(
|
||||
depth=encoder_depth,
|
||||
embed_dim=encoder_embed_dim,
|
||||
img_size=image_size,
|
||||
mlp_ratio=4,
|
||||
norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
|
||||
num_heads=encoder_num_heads,
|
||||
patch_size=vit_patch_size,
|
||||
qkv_bias=True,
|
||||
use_rel_pos=True,
|
||||
global_attn_indexes=encoder_global_attn_indexes,
|
||||
window_size=14,
|
||||
out_chans=prompt_embed_dim,
|
||||
),
|
||||
prompt_encoder=PromptEncoder(
|
||||
embed_dim=prompt_embed_dim,
|
||||
image_embedding_size=(image_embedding_size, image_embedding_size),
|
||||
input_image_size=(image_size, image_size),
|
||||
mask_in_chans=16,
|
||||
),
|
||||
mask_decoder=MaskDecoder(
|
||||
num_multimask_outputs=3,
|
||||
transformer=TwoWayTransformer(
|
||||
depth=2,
|
||||
embedding_dim=prompt_embed_dim,
|
||||
mlp_dim=2048,
|
||||
num_heads=8,
|
||||
),
|
||||
transformer_dim=prompt_embed_dim,
|
||||
iou_head_depth=3,
|
||||
iou_head_hidden_dim=256,
|
||||
),
|
||||
pixel_mean=[123.675, 116.28, 103.53],
|
||||
pixel_std=[58.395, 57.12, 57.375],
|
||||
)
|
||||
sam.eval()
|
||||
if checkpoint is not None:
|
||||
with open(checkpoint, "rb") as f:
|
||||
state_dict = torch.load(f)
|
||||
sam.load_state_dict(state_dict)
|
||||
return sam
|
||||
11
iopaint/plugins/segment_anything/modeling/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from .sam import Sam
|
||||
from .image_encoder import ImageEncoderViT
|
||||
from .mask_decoder import MaskDecoder
|
||||
from .prompt_encoder import PromptEncoder
|
||||
from .transformer import TwoWayTransformer
|
||||
43
iopaint/plugins/segment_anything/modeling/common.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from typing import Type
|
||||
|
||||
|
||||
class MLPBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
mlp_dim: int,
|
||||
act: Type[nn.Module] = nn.GELU,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.lin1 = nn.Linear(embedding_dim, mlp_dim)
|
||||
self.lin2 = nn.Linear(mlp_dim, embedding_dim)
|
||||
self.act = act()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.lin2(self.act(self.lin1(x)))
|
||||
|
||||
|
||||
# From https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/batch_norm.py # noqa
|
||||
# Itself from https://github.com/facebookresearch/ConvNeXt/blob/d1fa8f6fef0a165b27399986cc2bdacc92777e40/models/convnext.py#L119 # noqa
|
||||
class LayerNorm2d(nn.Module):
|
||||
def __init__(self, num_channels: int, eps: float = 1e-6) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(num_channels))
|
||||
self.bias = nn.Parameter(torch.zeros(num_channels))
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
u = x.mean(1, keepdim=True)
|
||||
s = (x - u).pow(2).mean(1, keepdim=True)
|
||||
x = (x - u) / torch.sqrt(s + self.eps)
|
||||
x = self.weight[:, None, None] * x + self.bias[:, None, None]
|
||||
return x
|
||||
395
iopaint/plugins/segment_anything/modeling/image_encoder.py
Normal file
@@ -0,0 +1,395 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from typing import Optional, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d, MLPBlock
|
||||
|
||||
|
||||
# This class and its supporting functions below lightly adapted from the ViTDet backbone available at: https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/vit.py # noqa
|
||||
class ImageEncoderViT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
img_size: int = 1024,
|
||||
patch_size: int = 16,
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
depth: int = 12,
|
||||
num_heads: int = 12,
|
||||
mlp_ratio: float = 4.0,
|
||||
out_chans: int = 256,
|
||||
qkv_bias: bool = True,
|
||||
norm_layer: Type[nn.Module] = nn.LayerNorm,
|
||||
act_layer: Type[nn.Module] = nn.GELU,
|
||||
use_abs_pos: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
global_attn_indexes: Tuple[int, ...] = (),
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
img_size (int): Input image size.
|
||||
patch_size (int): Patch size.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): Patch embedding dimension.
|
||||
depth (int): Depth of ViT.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
norm_layer (nn.Module): Normalization layer.
|
||||
act_layer (nn.Module): Activation layer.
|
||||
use_abs_pos (bool): If True, use absolute positional embeddings.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks.
|
||||
global_attn_indexes (list): Indexes for blocks using global attention.
|
||||
"""
|
||||
super().__init__()
|
||||
self.img_size = img_size
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
kernel_size=(patch_size, patch_size),
|
||||
stride=(patch_size, patch_size),
|
||||
in_chans=in_chans,
|
||||
embed_dim=embed_dim,
|
||||
)
|
||||
|
||||
self.pos_embed: Optional[nn.Parameter] = None
|
||||
if use_abs_pos:
|
||||
# Initialize absolute positional embedding with pretrain image size.
|
||||
self.pos_embed = nn.Parameter(
|
||||
torch.zeros(1, img_size // patch_size, img_size // patch_size, embed_dim)
|
||||
)
|
||||
|
||||
self.blocks = nn.ModuleList()
|
||||
for i in range(depth):
|
||||
block = Block(
|
||||
dim=embed_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_ratio=mlp_ratio,
|
||||
qkv_bias=qkv_bias,
|
||||
norm_layer=norm_layer,
|
||||
act_layer=act_layer,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
window_size=window_size if i not in global_attn_indexes else 0,
|
||||
input_size=(img_size // patch_size, img_size // patch_size),
|
||||
)
|
||||
self.blocks.append(block)
|
||||
|
||||
self.neck = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
embed_dim,
|
||||
out_chans,
|
||||
kernel_size=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
nn.Conv2d(
|
||||
out_chans,
|
||||
out_chans,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.patch_embed(x)
|
||||
if self.pos_embed is not None:
|
||||
x = x + self.pos_embed
|
||||
|
||||
for blk in self.blocks:
|
||||
x = blk(x)
|
||||
|
||||
x = self.neck(x.permute(0, 3, 1, 2))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Block(nn.Module):
|
||||
"""Transformer blocks with support of window attention and residual propagation blocks"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
qkv_bias: bool = True,
|
||||
norm_layer: Type[nn.Module] = nn.LayerNorm,
|
||||
act_layer: Type[nn.Module] = nn.GELU,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
input_size: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
norm_layer (nn.Module): Normalization layer.
|
||||
act_layer (nn.Module): Activation layer.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks. If it equals 0, then
|
||||
use global attention.
|
||||
input_size (int or None): Input resolution for calculating the relative positional
|
||||
parameter size.
|
||||
"""
|
||||
super().__init__()
|
||||
self.norm1 = norm_layer(dim)
|
||||
self.attn = Attention(
|
||||
dim,
|
||||
num_heads=num_heads,
|
||||
qkv_bias=qkv_bias,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
input_size=input_size if window_size == 0 else (window_size, window_size),
|
||||
)
|
||||
|
||||
self.norm2 = norm_layer(dim)
|
||||
self.mlp = MLPBlock(embedding_dim=dim, mlp_dim=int(dim * mlp_ratio), act=act_layer)
|
||||
|
||||
self.window_size = window_size
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
# Window partition
|
||||
if self.window_size > 0:
|
||||
H, W = x.shape[1], x.shape[2]
|
||||
x, pad_hw = window_partition(x, self.window_size)
|
||||
|
||||
x = self.attn(x)
|
||||
# Reverse window partition
|
||||
if self.window_size > 0:
|
||||
x = window_unpartition(x, self.window_size, pad_hw, (H, W))
|
||||
|
||||
x = shortcut + x
|
||||
x = x + self.mlp(self.norm2(x))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""Multi-head Attention block with relative position embeddings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int = 8,
|
||||
qkv_bias: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
input_size: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads.
|
||||
qkv_bias (bool: If True, add a learnable bias to query, key, value.
|
||||
rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
input_size (int or None): Input resolution for calculating the relative positional
|
||||
parameter size.
|
||||
"""
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
head_dim = dim // num_heads
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
||||
self.proj = nn.Linear(dim, dim)
|
||||
|
||||
self.use_rel_pos = use_rel_pos
|
||||
if self.use_rel_pos:
|
||||
assert (
|
||||
input_size is not None
|
||||
), "Input size must be provided if using relative positional encoding."
|
||||
# initialize relative positional embeddings
|
||||
self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, head_dim))
|
||||
self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, head_dim))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
B, H, W, _ = x.shape
|
||||
# qkv with shape (3, B, nHead, H * W, C)
|
||||
qkv = self.qkv(x).reshape(B, H * W, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
|
||||
# q, k, v with shape (B * nHead, H * W, C)
|
||||
q, k, v = qkv.reshape(3, B * self.num_heads, H * W, -1).unbind(0)
|
||||
|
||||
attn = (q * self.scale) @ k.transpose(-2, -1)
|
||||
|
||||
if self.use_rel_pos:
|
||||
attn = add_decomposed_rel_pos(attn, q, self.rel_pos_h, self.rel_pos_w, (H, W), (H, W))
|
||||
|
||||
attn = attn.softmax(dim=-1)
|
||||
x = (attn @ v).view(B, self.num_heads, H, W, -1).permute(0, 2, 3, 1, 4).reshape(B, H, W, -1)
|
||||
x = self.proj(x)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def window_partition(x: torch.Tensor, window_size: int) -> Tuple[torch.Tensor, Tuple[int, int]]:
|
||||
"""
|
||||
Partition into non-overlapping windows with padding if needed.
|
||||
Args:
|
||||
x (tensor): input tokens with [B, H, W, C].
|
||||
window_size (int): window size.
|
||||
|
||||
Returns:
|
||||
windows: windows after partition with [B * num_windows, window_size, window_size, C].
|
||||
(Hp, Wp): padded height and width before partition
|
||||
"""
|
||||
B, H, W, C = x.shape
|
||||
|
||||
pad_h = (window_size - H % window_size) % window_size
|
||||
pad_w = (window_size - W % window_size) % window_size
|
||||
if pad_h > 0 or pad_w > 0:
|
||||
x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h))
|
||||
Hp, Wp = H + pad_h, W + pad_w
|
||||
|
||||
x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C)
|
||||
windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
|
||||
return windows, (Hp, Wp)
|
||||
|
||||
|
||||
def window_unpartition(
|
||||
windows: torch.Tensor, window_size: int, pad_hw: Tuple[int, int], hw: Tuple[int, int]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Window unpartition into original sequences and removing padding.
|
||||
Args:
|
||||
x (tensor): input tokens with [B * num_windows, window_size, window_size, C].
|
||||
window_size (int): window size.
|
||||
pad_hw (Tuple): padded height and width (Hp, Wp).
|
||||
hw (Tuple): original height and width (H, W) before padding.
|
||||
|
||||
Returns:
|
||||
x: unpartitioned sequences with [B, H, W, C].
|
||||
"""
|
||||
Hp, Wp = pad_hw
|
||||
H, W = hw
|
||||
B = windows.shape[0] // (Hp * Wp // window_size // window_size)
|
||||
x = windows.view(B, Hp // window_size, Wp // window_size, window_size, window_size, -1)
|
||||
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1)
|
||||
|
||||
if Hp > H or Wp > W:
|
||||
x = x[:, :H, :W, :].contiguous()
|
||||
return x
|
||||
|
||||
|
||||
def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Get relative positional embeddings according to the relative positions of
|
||||
query and key sizes.
|
||||
Args:
|
||||
q_size (int): size of query q.
|
||||
k_size (int): size of key k.
|
||||
rel_pos (Tensor): relative position embeddings (L, C).
|
||||
|
||||
Returns:
|
||||
Extracted positional embeddings according to relative positions.
|
||||
"""
|
||||
max_rel_dist = int(2 * max(q_size, k_size) - 1)
|
||||
# Interpolate rel pos if needed.
|
||||
if rel_pos.shape[0] != max_rel_dist:
|
||||
# Interpolate rel pos.
|
||||
rel_pos_resized = F.interpolate(
|
||||
rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1),
|
||||
size=max_rel_dist,
|
||||
mode="linear",
|
||||
)
|
||||
rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0)
|
||||
else:
|
||||
rel_pos_resized = rel_pos
|
||||
|
||||
# Scale the coords with short length if shapes for q and k are different.
|
||||
q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0)
|
||||
k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0)
|
||||
relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0)
|
||||
|
||||
return rel_pos_resized[relative_coords.long()]
|
||||
|
||||
|
||||
def add_decomposed_rel_pos(
|
||||
attn: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
rel_pos_h: torch.Tensor,
|
||||
rel_pos_w: torch.Tensor,
|
||||
q_size: Tuple[int, int],
|
||||
k_size: Tuple[int, int],
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Calculate decomposed Relative Positional Embeddings from :paper:`mvitv2`.
|
||||
https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py # noqa B950
|
||||
Args:
|
||||
attn (Tensor): attention map.
|
||||
q (Tensor): query q in the attention layer with shape (B, q_h * q_w, C).
|
||||
rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis.
|
||||
rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis.
|
||||
q_size (Tuple): spatial sequence size of query q with (q_h, q_w).
|
||||
k_size (Tuple): spatial sequence size of key k with (k_h, k_w).
|
||||
|
||||
Returns:
|
||||
attn (Tensor): attention map with added relative positional embeddings.
|
||||
"""
|
||||
q_h, q_w = q_size
|
||||
k_h, k_w = k_size
|
||||
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
|
||||
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
|
||||
|
||||
B, _, dim = q.shape
|
||||
r_q = q.reshape(B, q_h, q_w, dim)
|
||||
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh)
|
||||
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw)
|
||||
|
||||
attn = (
|
||||
attn.view(B, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :]
|
||||
).view(B, q_h * q_w, k_h * k_w)
|
||||
|
||||
return attn
|
||||
|
||||
|
||||
class PatchEmbed(nn.Module):
|
||||
"""
|
||||
Image to Patch Embedding.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
kernel_size: Tuple[int, int] = (16, 16),
|
||||
stride: Tuple[int, int] = (16, 16),
|
||||
padding: Tuple[int, int] = (0, 0),
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
kernel_size (Tuple): kernel size of the projection layer.
|
||||
stride (Tuple): stride of the projection layer.
|
||||
padding (Tuple): padding size of the projection layer.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): embed_dim (int): Patch embedding dimension.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.proj = nn.Conv2d(
|
||||
in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.proj(x)
|
||||
# B C H W -> B H W C
|
||||
x = x.permute(0, 2, 3, 1)
|
||||
return x
|
||||
176
iopaint/plugins/segment_anything/modeling/mask_decoder.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from typing import List, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d
|
||||
|
||||
|
||||
class MaskDecoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transformer_dim: int,
|
||||
transformer: nn.Module,
|
||||
num_multimask_outputs: int = 3,
|
||||
activation: Type[nn.Module] = nn.GELU,
|
||||
iou_head_depth: int = 3,
|
||||
iou_head_hidden_dim: int = 256,
|
||||
) -> None:
|
||||
"""
|
||||
Predicts masks given an image and prompt embeddings, using a
|
||||
tranformer architecture.
|
||||
|
||||
Arguments:
|
||||
transformer_dim (int): the channel dimension of the transformer
|
||||
transformer (nn.Module): the transformer used to predict masks
|
||||
num_multimask_outputs (int): the number of masks to predict
|
||||
when disambiguating masks
|
||||
activation (nn.Module): the type of activation to use when
|
||||
upscaling masks
|
||||
iou_head_depth (int): the depth of the MLP used to predict
|
||||
mask quality
|
||||
iou_head_hidden_dim (int): the hidden dimension of the MLP
|
||||
used to predict mask quality
|
||||
"""
|
||||
super().__init__()
|
||||
self.transformer_dim = transformer_dim
|
||||
self.transformer = transformer
|
||||
|
||||
self.num_multimask_outputs = num_multimask_outputs
|
||||
|
||||
self.iou_token = nn.Embedding(1, transformer_dim)
|
||||
self.num_mask_tokens = num_multimask_outputs + 1
|
||||
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
|
||||
|
||||
self.output_upscaling = nn.Sequential(
|
||||
nn.ConvTranspose2d(transformer_dim, transformer_dim // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(transformer_dim // 4),
|
||||
activation(),
|
||||
nn.ConvTranspose2d(transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2),
|
||||
activation(),
|
||||
)
|
||||
self.output_hypernetworks_mlps = nn.ModuleList(
|
||||
[
|
||||
MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3)
|
||||
for i in range(self.num_mask_tokens)
|
||||
]
|
||||
)
|
||||
|
||||
self.iou_prediction_head = MLP(
|
||||
transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
multimask_output: bool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Predict masks given image and prompt embeddings.
|
||||
|
||||
Arguments:
|
||||
image_embeddings (torch.Tensor): the embeddings from the image encoder
|
||||
image_pe (torch.Tensor): positional encoding with the shape of image_embeddings
|
||||
sparse_prompt_embeddings (torch.Tensor): the embeddings of the points and boxes
|
||||
dense_prompt_embeddings (torch.Tensor): the embeddings of the mask inputs
|
||||
multimask_output (bool): Whether to return multiple masks or a single
|
||||
mask.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: batched predicted masks
|
||||
torch.Tensor: batched predictions of mask quality
|
||||
"""
|
||||
masks, iou_pred = self.predict_masks(
|
||||
image_embeddings=image_embeddings,
|
||||
image_pe=image_pe,
|
||||
sparse_prompt_embeddings=sparse_prompt_embeddings,
|
||||
dense_prompt_embeddings=dense_prompt_embeddings,
|
||||
)
|
||||
|
||||
# Select the correct mask or masks for outptu
|
||||
if multimask_output:
|
||||
mask_slice = slice(1, None)
|
||||
else:
|
||||
mask_slice = slice(0, 1)
|
||||
masks = masks[:, mask_slice, :, :]
|
||||
iou_pred = iou_pred[:, mask_slice]
|
||||
|
||||
# Prepare output
|
||||
return masks, iou_pred
|
||||
|
||||
def predict_masks(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Predicts masks. See 'forward' for more details."""
|
||||
# Concatenate output tokens
|
||||
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
|
||||
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.size(0), -1, -1)
|
||||
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
|
||||
|
||||
# Expand per-image data in batch direction to be per-mask
|
||||
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
|
||||
src = src + dense_prompt_embeddings
|
||||
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
|
||||
b, c, h, w = src.shape
|
||||
|
||||
# Run the transformer
|
||||
hs, src = self.transformer(src, pos_src, tokens)
|
||||
iou_token_out = hs[:, 0, :]
|
||||
mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :]
|
||||
|
||||
# Upscale mask embeddings and predict masks using the mask tokens
|
||||
src = src.transpose(1, 2).view(b, c, h, w)
|
||||
upscaled_embedding = self.output_upscaling(src)
|
||||
hyper_in_list: List[torch.Tensor] = []
|
||||
for i in range(self.num_mask_tokens):
|
||||
hyper_in_list.append(self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]))
|
||||
hyper_in = torch.stack(hyper_in_list, dim=1)
|
||||
b, c, h, w = upscaled_embedding.shape
|
||||
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
|
||||
|
||||
# Generate mask quality predictions
|
||||
iou_pred = self.iou_prediction_head(iou_token_out)
|
||||
|
||||
return masks, iou_pred
|
||||
|
||||
|
||||
# Lightly adapted from
|
||||
# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa
|
||||
class MLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
hidden_dim: int,
|
||||
output_dim: int,
|
||||
num_layers: int,
|
||||
sigmoid_output: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.num_layers = num_layers
|
||||
h = [hidden_dim] * (num_layers - 1)
|
||||
self.layers = nn.ModuleList(
|
||||
nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])
|
||||
)
|
||||
self.sigmoid_output = sigmoid_output
|
||||
|
||||
def forward(self, x):
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x)
|
||||
if self.sigmoid_output:
|
||||
x = F.sigmoid(x)
|
||||
return x
|
||||
214
iopaint/plugins/segment_anything/modeling/prompt_encoder.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from typing import Any, Optional, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d
|
||||
|
||||
|
||||
class PromptEncoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim: int,
|
||||
image_embedding_size: Tuple[int, int],
|
||||
input_image_size: Tuple[int, int],
|
||||
mask_in_chans: int,
|
||||
activation: Type[nn.Module] = nn.GELU,
|
||||
) -> None:
|
||||
"""
|
||||
Encodes prompts for input to SAM's mask decoder.
|
||||
|
||||
Arguments:
|
||||
embed_dim (int): The prompts' embedding dimension
|
||||
image_embedding_size (tuple(int, int)): The spatial size of the
|
||||
image embedding, as (H, W).
|
||||
input_image_size (int): The padded size of the image as input
|
||||
to the image encoder, as (H, W).
|
||||
mask_in_chans (int): The number of hidden channels used for
|
||||
encoding input masks.
|
||||
activation (nn.Module): The activation to use when encoding
|
||||
input masks.
|
||||
"""
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.input_image_size = input_image_size
|
||||
self.image_embedding_size = image_embedding_size
|
||||
self.pe_layer = PositionEmbeddingRandom(embed_dim // 2)
|
||||
|
||||
self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners
|
||||
point_embeddings = [nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings)]
|
||||
self.point_embeddings = nn.ModuleList(point_embeddings)
|
||||
self.not_a_point_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
self.mask_input_size = (4 * image_embedding_size[0], 4 * image_embedding_size[1])
|
||||
self.mask_downscaling = nn.Sequential(
|
||||
nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans // 4),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1),
|
||||
)
|
||||
self.no_mask_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
def get_dense_pe(self) -> torch.Tensor:
|
||||
"""
|
||||
Returns the positional encoding used to encode point prompts,
|
||||
applied to a dense set of points the shape of the image encoding.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Positional encoding with shape
|
||||
1x(embed_dim)x(embedding_h)x(embedding_w)
|
||||
"""
|
||||
return self.pe_layer(self.image_embedding_size).unsqueeze(0)
|
||||
|
||||
def _embed_points(
|
||||
self,
|
||||
points: torch.Tensor,
|
||||
labels: torch.Tensor,
|
||||
pad: bool,
|
||||
) -> torch.Tensor:
|
||||
"""Embeds point prompts."""
|
||||
points = points + 0.5 # Shift to center of pixel
|
||||
if pad:
|
||||
padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device)
|
||||
padding_label = -torch.ones((labels.shape[0], 1), device=labels.device)
|
||||
points = torch.cat([points, padding_point], dim=1)
|
||||
labels = torch.cat([labels, padding_label], dim=1)
|
||||
point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size)
|
||||
point_embedding[labels == -1] = 0.0
|
||||
point_embedding[labels == -1] += self.not_a_point_embed.weight
|
||||
point_embedding[labels == 0] += self.point_embeddings[0].weight
|
||||
point_embedding[labels == 1] += self.point_embeddings[1].weight
|
||||
return point_embedding
|
||||
|
||||
def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor:
|
||||
"""Embeds box prompts."""
|
||||
boxes = boxes + 0.5 # Shift to center of pixel
|
||||
coords = boxes.reshape(-1, 2, 2)
|
||||
corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size)
|
||||
corner_embedding[:, 0, :] += self.point_embeddings[2].weight
|
||||
corner_embedding[:, 1, :] += self.point_embeddings[3].weight
|
||||
return corner_embedding
|
||||
|
||||
def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor:
|
||||
"""Embeds mask inputs."""
|
||||
mask_embedding = self.mask_downscaling(masks)
|
||||
return mask_embedding
|
||||
|
||||
def _get_batch_size(
|
||||
self,
|
||||
points: Optional[Tuple[torch.Tensor, torch.Tensor]],
|
||||
boxes: Optional[torch.Tensor],
|
||||
masks: Optional[torch.Tensor],
|
||||
) -> int:
|
||||
"""
|
||||
Gets the batch size of the output given the batch size of the input prompts.
|
||||
"""
|
||||
if points is not None:
|
||||
return points[0].shape[0]
|
||||
elif boxes is not None:
|
||||
return boxes.shape[0]
|
||||
elif masks is not None:
|
||||
return masks.shape[0]
|
||||
else:
|
||||
return 1
|
||||
|
||||
def _get_device(self) -> torch.device:
|
||||
return self.point_embeddings[0].weight.device
|
||||
|
||||
def forward(
|
||||
self,
|
||||
points: Optional[Tuple[torch.Tensor, torch.Tensor]],
|
||||
boxes: Optional[torch.Tensor],
|
||||
masks: Optional[torch.Tensor],
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Embeds different types of prompts, returning both sparse and dense
|
||||
embeddings.
|
||||
|
||||
Arguments:
|
||||
points (tuple(torch.Tensor, torch.Tensor) or none): point coordinates
|
||||
and labels to embed.
|
||||
boxes (torch.Tensor or none): boxes to embed
|
||||
masks (torch.Tensor or none): masks to embed
|
||||
|
||||
Returns:
|
||||
torch.Tensor: sparse embeddings for the points and boxes, with shape
|
||||
BxNx(embed_dim), where N is determined by the number of input points
|
||||
and boxes.
|
||||
torch.Tensor: dense embeddings for the masks, in the shape
|
||||
Bx(embed_dim)x(embed_H)x(embed_W)
|
||||
"""
|
||||
bs = self._get_batch_size(points, boxes, masks)
|
||||
sparse_embeddings = torch.empty((bs, 0, self.embed_dim), device=self._get_device())
|
||||
if points is not None:
|
||||
coords, labels = points
|
||||
point_embeddings = self._embed_points(coords, labels, pad=(boxes is None))
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1)
|
||||
if boxes is not None:
|
||||
box_embeddings = self._embed_boxes(boxes)
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1)
|
||||
|
||||
if masks is not None:
|
||||
dense_embeddings = self._embed_masks(masks)
|
||||
else:
|
||||
dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand(
|
||||
bs, -1, self.image_embedding_size[0], self.image_embedding_size[1]
|
||||
)
|
||||
|
||||
return sparse_embeddings, dense_embeddings
|
||||
|
||||
|
||||
class PositionEmbeddingRandom(nn.Module):
|
||||
"""
|
||||
Positional encoding using random spatial frequencies.
|
||||
"""
|
||||
|
||||
def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None:
|
||||
super().__init__()
|
||||
if scale is None or scale <= 0.0:
|
||||
scale = 1.0
|
||||
self.register_buffer(
|
||||
"positional_encoding_gaussian_matrix",
|
||||
scale * torch.randn((2, num_pos_feats)),
|
||||
)
|
||||
|
||||
def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor:
|
||||
"""Positionally encode points that are normalized to [0,1]."""
|
||||
# assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape
|
||||
coords = 2 * coords - 1
|
||||
coords = coords @ self.positional_encoding_gaussian_matrix
|
||||
coords = 2 * np.pi * coords
|
||||
# outputs d_1 x ... x d_n x C shape
|
||||
return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1)
|
||||
|
||||
def forward(self, size: Tuple[int, int]) -> torch.Tensor:
|
||||
"""Generate positional encoding for a grid of the specified size."""
|
||||
h, w = size
|
||||
device: Any = self.positional_encoding_gaussian_matrix.device
|
||||
grid = torch.ones((h, w), device=device, dtype=torch.float32)
|
||||
y_embed = grid.cumsum(dim=0) - 0.5
|
||||
x_embed = grid.cumsum(dim=1) - 0.5
|
||||
y_embed = y_embed / h
|
||||
x_embed = x_embed / w
|
||||
|
||||
pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1))
|
||||
return pe.permute(2, 0, 1) # C x H x W
|
||||
|
||||
def forward_with_coords(
|
||||
self, coords_input: torch.Tensor, image_size: Tuple[int, int]
|
||||
) -> torch.Tensor:
|
||||
"""Positionally encode points that are not normalized to [0,1]."""
|
||||
coords = coords_input.clone()
|
||||
coords[:, :, 0] = coords[:, :, 0] / image_size[1]
|
||||
coords[:, :, 1] = coords[:, :, 1] / image_size[0]
|
||||
return self._pe_encoding(coords.to(torch.float)) # B x N x C
|
||||
174
iopaint/plugins/segment_anything/modeling/sam.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from .image_encoder import ImageEncoderViT
|
||||
from .mask_decoder import MaskDecoder
|
||||
from .prompt_encoder import PromptEncoder
|
||||
|
||||
|
||||
class Sam(nn.Module):
|
||||
mask_threshold: float = 0.0
|
||||
image_format: str = "RGB"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image_encoder: ImageEncoderViT,
|
||||
prompt_encoder: PromptEncoder,
|
||||
mask_decoder: MaskDecoder,
|
||||
pixel_mean: List[float] = [123.675, 116.28, 103.53],
|
||||
pixel_std: List[float] = [58.395, 57.12, 57.375],
|
||||
) -> None:
|
||||
"""
|
||||
SAM predicts object masks from an image and input prompts.
|
||||
|
||||
Arguments:
|
||||
image_encoder (ImageEncoderViT): The backbone used to encode the
|
||||
image into image embeddings that allow for efficient mask prediction.
|
||||
prompt_encoder (PromptEncoder): Encodes various types of input prompts.
|
||||
mask_decoder (MaskDecoder): Predicts masks from the image embeddings
|
||||
and encoded prompts.
|
||||
pixel_mean (list(float)): Mean values for normalizing pixels in the input image.
|
||||
pixel_std (list(float)): Std values for normalizing pixels in the input image.
|
||||
"""
|
||||
super().__init__()
|
||||
self.image_encoder = image_encoder
|
||||
self.prompt_encoder = prompt_encoder
|
||||
self.mask_decoder = mask_decoder
|
||||
self.register_buffer("pixel_mean", torch.Tensor(pixel_mean).view(-1, 1, 1), False)
|
||||
self.register_buffer("pixel_std", torch.Tensor(pixel_std).view(-1, 1, 1), False)
|
||||
|
||||
@property
|
||||
def device(self) -> Any:
|
||||
return self.pixel_mean.device
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
batched_input: List[Dict[str, Any]],
|
||||
multimask_output: bool,
|
||||
) -> List[Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Predicts masks end-to-end from provided images and prompts.
|
||||
If prompts are not known in advance, using SamPredictor is
|
||||
recommended over calling the model directly.
|
||||
|
||||
Arguments:
|
||||
batched_input (list(dict)): A list over input images, each a
|
||||
dictionary with the following keys. A prompt key can be
|
||||
excluded if it is not present.
|
||||
'image': The image as a torch tensor in 3xHxW format,
|
||||
already transformed for input to the model.
|
||||
'original_size': (tuple(int, int)) The original size of
|
||||
the image before transformation, as (H, W).
|
||||
'point_coords': (torch.Tensor) Batched point prompts for
|
||||
this image, with shape BxNx2. Already transformed to the
|
||||
input frame of the model.
|
||||
'point_labels': (torch.Tensor) Batched labels for point prompts,
|
||||
with shape BxN.
|
||||
'boxes': (torch.Tensor) Batched box inputs, with shape Bx4.
|
||||
Already transformed to the input frame of the model.
|
||||
'mask_inputs': (torch.Tensor) Batched mask inputs to the model,
|
||||
in the form Bx1xHxW.
|
||||
multimask_output (bool): Whether the model should predict multiple
|
||||
disambiguating masks, or return a single mask.
|
||||
|
||||
Returns:
|
||||
(list(dict)): A list over input images, where each element is
|
||||
as dictionary with the following keys.
|
||||
'masks': (torch.Tensor) Batched binary mask predictions,
|
||||
with shape BxCxHxW, where B is the number of input promts,
|
||||
C is determiend by multimask_output, and (H, W) is the
|
||||
original size of the image.
|
||||
'iou_predictions': (torch.Tensor) The model's predictions
|
||||
of mask quality, in shape BxC.
|
||||
'low_res_logits': (torch.Tensor) Low resolution logits with
|
||||
shape BxCxHxW, where H=W=256. Can be passed as mask input
|
||||
to subsequent iterations of prediction.
|
||||
"""
|
||||
input_images = torch.stack([self.preprocess(x["image"]) for x in batched_input], dim=0)
|
||||
image_embeddings = self.image_encoder(input_images)
|
||||
|
||||
outputs = []
|
||||
for image_record, curr_embedding in zip(batched_input, image_embeddings):
|
||||
if "point_coords" in image_record:
|
||||
points = (image_record["point_coords"], image_record["point_labels"])
|
||||
else:
|
||||
points = None
|
||||
sparse_embeddings, dense_embeddings = self.prompt_encoder(
|
||||
points=points,
|
||||
boxes=image_record.get("boxes", None),
|
||||
masks=image_record.get("mask_inputs", None),
|
||||
)
|
||||
low_res_masks, iou_predictions = self.mask_decoder(
|
||||
image_embeddings=curr_embedding.unsqueeze(0),
|
||||
image_pe=self.prompt_encoder.get_dense_pe(),
|
||||
sparse_prompt_embeddings=sparse_embeddings,
|
||||
dense_prompt_embeddings=dense_embeddings,
|
||||
multimask_output=multimask_output,
|
||||
)
|
||||
masks = self.postprocess_masks(
|
||||
low_res_masks,
|
||||
input_size=image_record["image"].shape[-2:],
|
||||
original_size=image_record["original_size"],
|
||||
)
|
||||
masks = masks > self.mask_threshold
|
||||
outputs.append(
|
||||
{
|
||||
"masks": masks,
|
||||
"iou_predictions": iou_predictions,
|
||||
"low_res_logits": low_res_masks,
|
||||
}
|
||||
)
|
||||
return outputs
|
||||
|
||||
def postprocess_masks(
|
||||
self,
|
||||
masks: torch.Tensor,
|
||||
input_size: Tuple[int, ...],
|
||||
original_size: Tuple[int, ...],
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Remove padding and upscale masks to the original image size.
|
||||
|
||||
Arguments:
|
||||
masks (torch.Tensor): Batched masks from the mask_decoder,
|
||||
in BxCxHxW format.
|
||||
input_size (tuple(int, int)): The size of the image input to the
|
||||
model, in (H, W) format. Used to remove padding.
|
||||
original_size (tuple(int, int)): The original size of the image
|
||||
before resizing for input to the model, in (H, W) format.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Batched masks in BxCxHxW format, where (H, W)
|
||||
is given by original_size.
|
||||
"""
|
||||
masks = F.interpolate(
|
||||
masks,
|
||||
(self.image_encoder.img_size, self.image_encoder.img_size),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
masks = masks[..., : input_size[0], : input_size[1]]
|
||||
masks = F.interpolate(masks, original_size, mode="bilinear", align_corners=False)
|
||||
return masks
|
||||
|
||||
def preprocess(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Normalize pixel values and pad to a square input."""
|
||||
# Normalize colors
|
||||
x = (x - self.pixel_mean) / self.pixel_std
|
||||
|
||||
# Pad
|
||||
h, w = x.shape[-2:]
|
||||
padh = self.image_encoder.img_size - h
|
||||
padw = self.image_encoder.img_size - w
|
||||
x = F.pad(x, (0, padw, 0, padh))
|
||||
return x
|
||||
822
iopaint/plugins/segment_anything/modeling/tiny_vit_sam.py
Normal file
@@ -0,0 +1,822 @@
|
||||
# --------------------------------------------------------
|
||||
# TinyViT Model Architecture
|
||||
# Copyright (c) 2022 Microsoft
|
||||
# Adapted from LeViT and Swin Transformer
|
||||
# LeViT: (https://github.com/facebookresearch/levit)
|
||||
# Swin: (https://github.com/microsoft/swin-transformer)
|
||||
# Build the TinyViT Model
|
||||
# --------------------------------------------------------
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import math
|
||||
import warnings
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.checkpoint as checkpoint
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def _ntuple(n):
|
||||
def parse(x):
|
||||
if isinstance(x, collections.abc.Iterable) and not isinstance(x, str):
|
||||
return x
|
||||
return tuple(itertools.repeat(x, n))
|
||||
|
||||
return parse
|
||||
|
||||
|
||||
to_2tuple = _ntuple(2)
|
||||
|
||||
|
||||
def _trunc_normal_(tensor, mean, std, a, b):
|
||||
# Cut & paste from PyTorch official master until it's in a few official releases - RW
|
||||
# Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
|
||||
def norm_cdf(x):
|
||||
# Computes standard normal cumulative distribution function
|
||||
return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0
|
||||
|
||||
if (mean < a - 2 * std) or (mean > b + 2 * std):
|
||||
warnings.warn(
|
||||
"mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
|
||||
"The distribution of values may be incorrect.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Values are generated by using a truncated uniform distribution and
|
||||
# then using the inverse CDF for the normal distribution.
|
||||
# Get upper and lower cdf values
|
||||
l = norm_cdf((a - mean) / std)
|
||||
u = norm_cdf((b - mean) / std)
|
||||
|
||||
# Uniformly fill tensor with values from [l, u], then translate to
|
||||
# [2l-1, 2u-1].
|
||||
tensor.uniform_(2 * l - 1, 2 * u - 1)
|
||||
|
||||
# Use inverse cdf transform for normal distribution to get truncated
|
||||
# standard normal
|
||||
tensor.erfinv_()
|
||||
|
||||
# Transform to proper mean, std
|
||||
tensor.mul_(std * math.sqrt(2.0))
|
||||
tensor.add_(mean)
|
||||
|
||||
# Clamp to ensure it's in the proper range
|
||||
tensor.clamp_(min=a, max=b)
|
||||
return tensor
|
||||
|
||||
|
||||
def trunc_normal_(tensor, mean=0.0, std=1.0, a=-2.0, b=2.0):
|
||||
# type: (Tensor, float, float, float, float) -> Tensor
|
||||
r"""Fills the input Tensor with values drawn from a truncated
|
||||
normal distribution. The values are effectively drawn from the
|
||||
normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)`
|
||||
with values outside :math:`[a, b]` redrawn until they are within
|
||||
the bounds. The method used for generating the random values works
|
||||
best when :math:`a \leq \text{mean} \leq b`.
|
||||
|
||||
NOTE: this impl is similar to the PyTorch trunc_normal_, the bounds [a, b] are
|
||||
applied while sampling the normal with mean/std applied, therefore a, b args
|
||||
should be adjusted to match the range of mean, std args.
|
||||
|
||||
Args:
|
||||
tensor: an n-dimensional `torch.Tensor`
|
||||
mean: the mean of the normal distribution
|
||||
std: the standard deviation of the normal distribution
|
||||
a: the minimum cutoff value
|
||||
b: the maximum cutoff value
|
||||
Examples:
|
||||
>>> w = torch.empty(3, 5)
|
||||
>>> nn.init.trunc_normal_(w)
|
||||
"""
|
||||
with torch.no_grad():
|
||||
return _trunc_normal_(tensor, mean, std, a, b)
|
||||
|
||||
|
||||
def drop_path(
|
||||
x, drop_prob: float = 0.0, training: bool = False, scale_by_keep: bool = True
|
||||
):
|
||||
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
|
||||
|
||||
This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
|
||||
the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
|
||||
See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
|
||||
changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
|
||||
'survival rate' as the argument.
|
||||
|
||||
"""
|
||||
if drop_prob == 0.0 or not training:
|
||||
return x
|
||||
keep_prob = 1 - drop_prob
|
||||
shape = (x.shape[0],) + (1,) * (
|
||||
x.ndim - 1
|
||||
) # work with diff dim tensors, not just 2D ConvNets
|
||||
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
|
||||
if keep_prob > 0.0 and scale_by_keep:
|
||||
random_tensor.div_(keep_prob)
|
||||
return x * random_tensor
|
||||
|
||||
|
||||
class TimmDropPath(nn.Module):
|
||||
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
|
||||
|
||||
def __init__(self, drop_prob: float = 0.0, scale_by_keep: bool = True):
|
||||
super(TimmDropPath, self).__init__()
|
||||
self.drop_prob = drop_prob
|
||||
self.scale_by_keep = scale_by_keep
|
||||
|
||||
def forward(self, x):
|
||||
return drop_path(x, self.drop_prob, self.training, self.scale_by_keep)
|
||||
|
||||
def extra_repr(self):
|
||||
return f"drop_prob={round(self.drop_prob,3):0.3f}"
|
||||
|
||||
|
||||
class Conv2d_BN(torch.nn.Sequential):
|
||||
def __init__(
|
||||
self, a, b, ks=1, stride=1, pad=0, dilation=1, groups=1, bn_weight_init=1
|
||||
):
|
||||
super().__init__()
|
||||
self.add_module(
|
||||
"c", torch.nn.Conv2d(a, b, ks, stride, pad, dilation, groups, bias=False)
|
||||
)
|
||||
bn = torch.nn.BatchNorm2d(b)
|
||||
torch.nn.init.constant_(bn.weight, bn_weight_init)
|
||||
torch.nn.init.constant_(bn.bias, 0)
|
||||
self.add_module("bn", bn)
|
||||
|
||||
@torch.no_grad()
|
||||
def fuse(self):
|
||||
c, bn = self._modules.values()
|
||||
w = bn.weight / (bn.running_var + bn.eps) ** 0.5
|
||||
w = c.weight * w[:, None, None, None]
|
||||
b = bn.bias - bn.running_mean * bn.weight / (bn.running_var + bn.eps) ** 0.5
|
||||
m = torch.nn.Conv2d(
|
||||
w.size(1) * self.c.groups,
|
||||
w.size(0),
|
||||
w.shape[2:],
|
||||
stride=self.c.stride,
|
||||
padding=self.c.padding,
|
||||
dilation=self.c.dilation,
|
||||
groups=self.c.groups,
|
||||
)
|
||||
m.weight.data.copy_(w)
|
||||
m.bias.data.copy_(b)
|
||||
return m
|
||||
|
||||
|
||||
class DropPath(TimmDropPath):
|
||||
def __init__(self, drop_prob=None):
|
||||
super().__init__(drop_prob=drop_prob)
|
||||
self.drop_prob = drop_prob
|
||||
|
||||
def __repr__(self):
|
||||
msg = super().__repr__()
|
||||
msg += f"(drop_prob={self.drop_prob})"
|
||||
return msg
|
||||
|
||||
|
||||
class PatchEmbed(nn.Module):
|
||||
def __init__(self, in_chans, embed_dim, resolution, activation):
|
||||
super().__init__()
|
||||
img_size: Tuple[int, int] = to_2tuple(resolution)
|
||||
self.patches_resolution = (img_size[0] // 4, img_size[1] // 4)
|
||||
self.num_patches = self.patches_resolution[0] * self.patches_resolution[1]
|
||||
self.in_chans = in_chans
|
||||
self.embed_dim = embed_dim
|
||||
n = embed_dim
|
||||
self.seq = nn.Sequential(
|
||||
Conv2d_BN(in_chans, n // 2, 3, 2, 1),
|
||||
activation(),
|
||||
Conv2d_BN(n // 2, n, 3, 2, 1),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.seq(x)
|
||||
|
||||
|
||||
class MBConv(nn.Module):
|
||||
def __init__(self, in_chans, out_chans, expand_ratio, activation, drop_path):
|
||||
super().__init__()
|
||||
self.in_chans = in_chans
|
||||
self.hidden_chans = int(in_chans * expand_ratio)
|
||||
self.out_chans = out_chans
|
||||
|
||||
self.conv1 = Conv2d_BN(in_chans, self.hidden_chans, ks=1)
|
||||
self.act1 = activation()
|
||||
|
||||
self.conv2 = Conv2d_BN(
|
||||
self.hidden_chans,
|
||||
self.hidden_chans,
|
||||
ks=3,
|
||||
stride=1,
|
||||
pad=1,
|
||||
groups=self.hidden_chans,
|
||||
)
|
||||
self.act2 = activation()
|
||||
|
||||
self.conv3 = Conv2d_BN(self.hidden_chans, out_chans, ks=1, bn_weight_init=0.0)
|
||||
self.act3 = activation()
|
||||
|
||||
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
||||
|
||||
def forward(self, x):
|
||||
shortcut = x
|
||||
|
||||
x = self.conv1(x)
|
||||
x = self.act1(x)
|
||||
|
||||
x = self.conv2(x)
|
||||
x = self.act2(x)
|
||||
|
||||
x = self.conv3(x)
|
||||
|
||||
x = self.drop_path(x)
|
||||
|
||||
x += shortcut
|
||||
x = self.act3(x)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class PatchMerging(nn.Module):
|
||||
def __init__(self, input_resolution, dim, out_dim, activation):
|
||||
super().__init__()
|
||||
|
||||
self.input_resolution = input_resolution
|
||||
self.dim = dim
|
||||
self.out_dim = out_dim
|
||||
self.act = activation()
|
||||
self.conv1 = Conv2d_BN(dim, out_dim, 1, 1, 0)
|
||||
stride_c = 2
|
||||
if out_dim == 320 or out_dim == 448 or out_dim == 576:
|
||||
stride_c = 1
|
||||
self.conv2 = Conv2d_BN(out_dim, out_dim, 3, stride_c, 1, groups=out_dim)
|
||||
self.conv3 = Conv2d_BN(out_dim, out_dim, 1, 1, 0)
|
||||
|
||||
def forward(self, x):
|
||||
if x.ndim == 3:
|
||||
H, W = self.input_resolution
|
||||
B = len(x)
|
||||
# (B, C, H, W)
|
||||
x = x.view(B, H, W, -1).permute(0, 3, 1, 2)
|
||||
|
||||
x = self.conv1(x)
|
||||
x = self.act(x)
|
||||
|
||||
x = self.conv2(x)
|
||||
x = self.act(x)
|
||||
x = self.conv3(x)
|
||||
x = x.flatten(2).transpose(1, 2)
|
||||
return x
|
||||
|
||||
|
||||
class ConvLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
input_resolution,
|
||||
depth,
|
||||
activation,
|
||||
drop_path=0.0,
|
||||
downsample=None,
|
||||
use_checkpoint=False,
|
||||
out_dim=None,
|
||||
conv_expand_ratio=4.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.depth = depth
|
||||
self.use_checkpoint = use_checkpoint
|
||||
|
||||
# build blocks
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
MBConv(
|
||||
dim,
|
||||
dim,
|
||||
conv_expand_ratio,
|
||||
activation,
|
||||
drop_path[i] if isinstance(drop_path, list) else drop_path,
|
||||
)
|
||||
for i in range(depth)
|
||||
]
|
||||
)
|
||||
|
||||
# patch merging layer
|
||||
if downsample is not None:
|
||||
self.downsample = downsample(
|
||||
input_resolution, dim=dim, out_dim=out_dim, activation=activation
|
||||
)
|
||||
else:
|
||||
self.downsample = None
|
||||
|
||||
def forward(self, x):
|
||||
for blk in self.blocks:
|
||||
if self.use_checkpoint:
|
||||
x = checkpoint.checkpoint(blk, x)
|
||||
else:
|
||||
x = blk(x)
|
||||
if self.downsample is not None:
|
||||
x = self.downsample(x)
|
||||
return x
|
||||
|
||||
|
||||
class Mlp(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_features,
|
||||
hidden_features=None,
|
||||
out_features=None,
|
||||
act_layer=nn.GELU,
|
||||
drop=0.0,
|
||||
):
|
||||
super().__init__()
|
||||
out_features = out_features or in_features
|
||||
hidden_features = hidden_features or in_features
|
||||
self.norm = nn.LayerNorm(in_features)
|
||||
self.fc1 = nn.Linear(in_features, hidden_features)
|
||||
self.fc2 = nn.Linear(hidden_features, out_features)
|
||||
self.act = act_layer()
|
||||
self.drop = nn.Dropout(drop)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.norm(x)
|
||||
|
||||
x = self.fc1(x)
|
||||
x = self.act(x)
|
||||
x = self.drop(x)
|
||||
x = self.fc2(x)
|
||||
x = self.drop(x)
|
||||
return x
|
||||
|
||||
|
||||
class Attention(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
key_dim,
|
||||
num_heads=8,
|
||||
attn_ratio=4,
|
||||
resolution=(14, 14),
|
||||
):
|
||||
super().__init__()
|
||||
# (h, w)
|
||||
assert isinstance(resolution, tuple) and len(resolution) == 2
|
||||
self.num_heads = num_heads
|
||||
self.scale = key_dim**-0.5
|
||||
self.key_dim = key_dim
|
||||
self.nh_kd = nh_kd = key_dim * num_heads
|
||||
self.d = int(attn_ratio * key_dim)
|
||||
self.dh = int(attn_ratio * key_dim) * num_heads
|
||||
self.attn_ratio = attn_ratio
|
||||
h = self.dh + nh_kd * 2
|
||||
|
||||
self.norm = nn.LayerNorm(dim)
|
||||
self.qkv = nn.Linear(dim, h)
|
||||
self.proj = nn.Linear(self.dh, dim)
|
||||
|
||||
points = list(itertools.product(range(resolution[0]), range(resolution[1])))
|
||||
N = len(points)
|
||||
attention_offsets = {}
|
||||
idxs = []
|
||||
for p1 in points:
|
||||
for p2 in points:
|
||||
offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
|
||||
if offset not in attention_offsets:
|
||||
attention_offsets[offset] = len(attention_offsets)
|
||||
idxs.append(attention_offsets[offset])
|
||||
self.attention_biases = torch.nn.Parameter(
|
||||
torch.zeros(num_heads, len(attention_offsets))
|
||||
)
|
||||
self.register_buffer(
|
||||
"attention_bias_idxs", torch.LongTensor(idxs).view(N, N), persistent=False
|
||||
)
|
||||
|
||||
@torch.no_grad()
|
||||
def train(self, mode=True):
|
||||
super().train(mode)
|
||||
if mode and hasattr(self, "ab"):
|
||||
del self.ab
|
||||
else:
|
||||
self.register_buffer(
|
||||
"ab",
|
||||
self.attention_biases[:, self.attention_bias_idxs],
|
||||
persistent=False,
|
||||
)
|
||||
|
||||
def forward(self, x): # x (B,N,C)
|
||||
B, N, _ = x.shape
|
||||
|
||||
# Normalization
|
||||
x = self.norm(x)
|
||||
|
||||
qkv = self.qkv(x)
|
||||
# (B, N, num_heads, d)
|
||||
q, k, v = qkv.view(B, N, self.num_heads, -1).split(
|
||||
[self.key_dim, self.key_dim, self.d], dim=3
|
||||
)
|
||||
# (B, num_heads, N, d)
|
||||
q = q.permute(0, 2, 1, 3)
|
||||
k = k.permute(0, 2, 1, 3)
|
||||
v = v.permute(0, 2, 1, 3)
|
||||
|
||||
attn = (q @ k.transpose(-2, -1)) * self.scale + (
|
||||
self.attention_biases[:, self.attention_bias_idxs]
|
||||
if self.training
|
||||
else self.ab
|
||||
)
|
||||
attn = attn.softmax(dim=-1)
|
||||
x = (attn @ v).transpose(1, 2).reshape(B, N, self.dh)
|
||||
x = self.proj(x)
|
||||
return x
|
||||
|
||||
|
||||
class TinyViTBlock(nn.Module):
|
||||
r"""TinyViT Block.
|
||||
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
input_resolution (tuple[int, int]): Input resolution.
|
||||
num_heads (int): Number of attention heads.
|
||||
window_size (int): Window size.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
drop (float, optional): Dropout rate. Default: 0.0
|
||||
drop_path (float, optional): Stochastic depth rate. Default: 0.0
|
||||
local_conv_size (int): the kernel size of the convolution between
|
||||
Attention and MLP. Default: 3
|
||||
activation: the activation function. Default: nn.GELU
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
input_resolution,
|
||||
num_heads,
|
||||
window_size=7,
|
||||
mlp_ratio=4.0,
|
||||
drop=0.0,
|
||||
drop_path=0.0,
|
||||
local_conv_size=3,
|
||||
activation=nn.GELU,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.num_heads = num_heads
|
||||
assert window_size > 0, "window_size must be greater than 0"
|
||||
self.window_size = window_size
|
||||
self.mlp_ratio = mlp_ratio
|
||||
|
||||
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
||||
|
||||
assert dim % num_heads == 0, "dim must be divisible by num_heads"
|
||||
head_dim = dim // num_heads
|
||||
|
||||
window_resolution = (window_size, window_size)
|
||||
self.attn = Attention(
|
||||
dim, head_dim, num_heads, attn_ratio=1, resolution=window_resolution
|
||||
)
|
||||
|
||||
mlp_hidden_dim = int(dim * mlp_ratio)
|
||||
mlp_activation = activation
|
||||
self.mlp = Mlp(
|
||||
in_features=dim,
|
||||
hidden_features=mlp_hidden_dim,
|
||||
act_layer=mlp_activation,
|
||||
drop=drop,
|
||||
)
|
||||
|
||||
pad = local_conv_size // 2
|
||||
self.local_conv = Conv2d_BN(
|
||||
dim, dim, ks=local_conv_size, stride=1, pad=pad, groups=dim
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
H, W = self.input_resolution
|
||||
B, L, C = x.shape
|
||||
assert L == H * W, "input feature has wrong size"
|
||||
res_x = x
|
||||
if H == self.window_size and W == self.window_size:
|
||||
x = self.attn(x)
|
||||
else:
|
||||
x = x.view(B, H, W, C)
|
||||
pad_b = (self.window_size - H % self.window_size) % self.window_size
|
||||
pad_r = (self.window_size - W % self.window_size) % self.window_size
|
||||
padding = pad_b > 0 or pad_r > 0
|
||||
|
||||
if padding:
|
||||
x = F.pad(x, (0, 0, 0, pad_r, 0, pad_b))
|
||||
|
||||
pH, pW = H + pad_b, W + pad_r
|
||||
nH = pH // self.window_size
|
||||
nW = pW // self.window_size
|
||||
# window partition
|
||||
x = (
|
||||
x.view(B, nH, self.window_size, nW, self.window_size, C)
|
||||
.transpose(2, 3)
|
||||
.reshape(B * nH * nW, self.window_size * self.window_size, C)
|
||||
)
|
||||
x = self.attn(x)
|
||||
# window reverse
|
||||
x = (
|
||||
x.view(B, nH, nW, self.window_size, self.window_size, C)
|
||||
.transpose(2, 3)
|
||||
.reshape(B, pH, pW, C)
|
||||
)
|
||||
|
||||
if padding:
|
||||
x = x[:, :H, :W].contiguous()
|
||||
|
||||
x = x.view(B, L, C)
|
||||
|
||||
x = res_x + self.drop_path(x)
|
||||
|
||||
x = x.transpose(1, 2).reshape(B, C, H, W)
|
||||
x = self.local_conv(x)
|
||||
x = x.view(B, C, L).transpose(1, 2)
|
||||
|
||||
x = x + self.drop_path(self.mlp(x))
|
||||
return x
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
return (
|
||||
f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, "
|
||||
f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}"
|
||||
)
|
||||
|
||||
|
||||
class BasicLayer(nn.Module):
|
||||
"""A basic TinyViT layer for one stage.
|
||||
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
input_resolution (tuple[int]): Input resolution.
|
||||
depth (int): Number of blocks.
|
||||
num_heads (int): Number of attention heads.
|
||||
window_size (int): Local window size.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
drop (float, optional): Dropout rate. Default: 0.0
|
||||
drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0
|
||||
downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None
|
||||
use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False.
|
||||
local_conv_size: the kernel size of the depthwise convolution between attention and MLP. Default: 3
|
||||
activation: the activation function. Default: nn.GELU
|
||||
out_dim: the output dimension of the layer. Default: dim
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
input_resolution,
|
||||
depth,
|
||||
num_heads,
|
||||
window_size,
|
||||
mlp_ratio=4.0,
|
||||
drop=0.0,
|
||||
drop_path=0.0,
|
||||
downsample=None,
|
||||
use_checkpoint=False,
|
||||
local_conv_size=3,
|
||||
activation=nn.GELU,
|
||||
out_dim=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.depth = depth
|
||||
self.use_checkpoint = use_checkpoint
|
||||
|
||||
# build blocks
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
TinyViTBlock(
|
||||
dim=dim,
|
||||
input_resolution=input_resolution,
|
||||
num_heads=num_heads,
|
||||
window_size=window_size,
|
||||
mlp_ratio=mlp_ratio,
|
||||
drop=drop,
|
||||
drop_path=drop_path[i]
|
||||
if isinstance(drop_path, list)
|
||||
else drop_path,
|
||||
local_conv_size=local_conv_size,
|
||||
activation=activation,
|
||||
)
|
||||
for i in range(depth)
|
||||
]
|
||||
)
|
||||
|
||||
# patch merging layer
|
||||
if downsample is not None:
|
||||
self.downsample = downsample(
|
||||
input_resolution, dim=dim, out_dim=out_dim, activation=activation
|
||||
)
|
||||
else:
|
||||
self.downsample = None
|
||||
|
||||
def forward(self, x):
|
||||
for blk in self.blocks:
|
||||
if self.use_checkpoint:
|
||||
x = checkpoint.checkpoint(blk, x)
|
||||
else:
|
||||
x = blk(x)
|
||||
if self.downsample is not None:
|
||||
x = self.downsample(x)
|
||||
return x
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"
|
||||
|
||||
|
||||
class LayerNorm2d(nn.Module):
|
||||
def __init__(self, num_channels: int, eps: float = 1e-6) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(num_channels))
|
||||
self.bias = nn.Parameter(torch.zeros(num_channels))
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
u = x.mean(1, keepdim=True)
|
||||
s = (x - u).pow(2).mean(1, keepdim=True)
|
||||
x = (x - u) / torch.sqrt(s + self.eps)
|
||||
x = self.weight[:, None, None] * x + self.bias[:, None, None]
|
||||
return x
|
||||
|
||||
|
||||
class TinyViT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
img_size=224,
|
||||
in_chans=3,
|
||||
num_classes=1000,
|
||||
embed_dims=[96, 192, 384, 768],
|
||||
depths=[2, 2, 6, 2],
|
||||
num_heads=[3, 6, 12, 24],
|
||||
window_sizes=[7, 7, 14, 7],
|
||||
mlp_ratio=4.0,
|
||||
drop_rate=0.0,
|
||||
drop_path_rate=0.1,
|
||||
use_checkpoint=False,
|
||||
mbconv_expand_ratio=4.0,
|
||||
local_conv_size=3,
|
||||
layer_lr_decay=1.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.img_size = img_size
|
||||
self.num_classes = num_classes
|
||||
self.depths = depths
|
||||
self.num_layers = len(depths)
|
||||
self.mlp_ratio = mlp_ratio
|
||||
|
||||
activation = nn.GELU
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
in_chans=in_chans,
|
||||
embed_dim=embed_dims[0],
|
||||
resolution=img_size,
|
||||
activation=activation,
|
||||
)
|
||||
|
||||
patches_resolution = self.patch_embed.patches_resolution
|
||||
self.patches_resolution = patches_resolution
|
||||
|
||||
# stochastic depth
|
||||
dpr = [
|
||||
x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))
|
||||
] # stochastic depth decay rule
|
||||
|
||||
# build layers
|
||||
self.layers = nn.ModuleList()
|
||||
for i_layer in range(self.num_layers):
|
||||
kwargs = dict(
|
||||
dim=embed_dims[i_layer],
|
||||
input_resolution=(
|
||||
patches_resolution[0]
|
||||
// (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
|
||||
patches_resolution[1]
|
||||
// (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
|
||||
),
|
||||
# input_resolution=(patches_resolution[0] // (2 ** i_layer),
|
||||
# patches_resolution[1] // (2 ** i_layer)),
|
||||
depth=depths[i_layer],
|
||||
drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])],
|
||||
downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
|
||||
use_checkpoint=use_checkpoint,
|
||||
out_dim=embed_dims[min(i_layer + 1, len(embed_dims) - 1)],
|
||||
activation=activation,
|
||||
)
|
||||
if i_layer == 0:
|
||||
layer = ConvLayer(
|
||||
conv_expand_ratio=mbconv_expand_ratio,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
layer = BasicLayer(
|
||||
num_heads=num_heads[i_layer],
|
||||
window_size=window_sizes[i_layer],
|
||||
mlp_ratio=self.mlp_ratio,
|
||||
drop=drop_rate,
|
||||
local_conv_size=local_conv_size,
|
||||
**kwargs,
|
||||
)
|
||||
self.layers.append(layer)
|
||||
|
||||
# Classifier head
|
||||
self.norm_head = nn.LayerNorm(embed_dims[-1])
|
||||
self.head = (
|
||||
nn.Linear(embed_dims[-1], num_classes)
|
||||
if num_classes > 0
|
||||
else torch.nn.Identity()
|
||||
)
|
||||
|
||||
# init weights
|
||||
self.apply(self._init_weights)
|
||||
self.set_layer_lr_decay(layer_lr_decay)
|
||||
self.neck = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
embed_dims[-1],
|
||||
256,
|
||||
kernel_size=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(256),
|
||||
nn.Conv2d(
|
||||
256,
|
||||
256,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(256),
|
||||
)
|
||||
|
||||
def set_layer_lr_decay(self, layer_lr_decay):
|
||||
decay_rate = layer_lr_decay
|
||||
|
||||
# layers -> blocks (depth)
|
||||
depth = sum(self.depths)
|
||||
lr_scales = [decay_rate ** (depth - i - 1) for i in range(depth)]
|
||||
# print("LR SCALES:", lr_scales)
|
||||
|
||||
def _set_lr_scale(m, scale):
|
||||
for p in m.parameters():
|
||||
p.lr_scale = scale
|
||||
|
||||
self.patch_embed.apply(lambda x: _set_lr_scale(x, lr_scales[0]))
|
||||
i = 0
|
||||
for layer in self.layers:
|
||||
for block in layer.blocks:
|
||||
block.apply(lambda x: _set_lr_scale(x, lr_scales[i]))
|
||||
i += 1
|
||||
if layer.downsample is not None:
|
||||
layer.downsample.apply(lambda x: _set_lr_scale(x, lr_scales[i - 1]))
|
||||
assert i == depth
|
||||
for m in [self.norm_head, self.head]:
|
||||
m.apply(lambda x: _set_lr_scale(x, lr_scales[-1]))
|
||||
|
||||
for k, p in self.named_parameters():
|
||||
p.param_name = k
|
||||
|
||||
def _check_lr_scale(m):
|
||||
for p in m.parameters():
|
||||
assert hasattr(p, "lr_scale"), p.param_name
|
||||
|
||||
self.apply(_check_lr_scale)
|
||||
|
||||
def _init_weights(self, m):
|
||||
if isinstance(m, nn.Linear):
|
||||
trunc_normal_(m.weight, std=0.02)
|
||||
if isinstance(m, nn.Linear) and m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
elif isinstance(m, nn.LayerNorm):
|
||||
nn.init.constant_(m.bias, 0)
|
||||
nn.init.constant_(m.weight, 1.0)
|
||||
|
||||
@torch.jit.ignore
|
||||
def no_weight_decay_keywords(self):
|
||||
return {"attention_biases"}
|
||||
|
||||
def forward_features(self, x):
|
||||
# x: (N, C, H, W)
|
||||
x = self.patch_embed(x)
|
||||
|
||||
x = self.layers[0](x)
|
||||
start_i = 1
|
||||
|
||||
for i in range(start_i, len(self.layers)):
|
||||
layer = self.layers[i]
|
||||
x = layer(x)
|
||||
B, _, C = x.size()
|
||||
x = x.view(B, 64, 64, C)
|
||||
x = x.permute(0, 3, 1, 2)
|
||||
x = self.neck(x)
|
||||
return x
|
||||
|
||||
def forward(self, x):
|
||||
x = self.forward_features(x)
|
||||
# x = self.norm_head(x)
|
||||
# x = self.head(x)
|
||||
return x
|
||||
240
iopaint/plugins/segment_anything/modeling/transformer.py
Normal file
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
import math
|
||||
from typing import Tuple, Type
|
||||
|
||||
from .common import MLPBlock
|
||||
|
||||
|
||||
class TwoWayTransformer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
depth: int,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int,
|
||||
activation: Type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
) -> None:
|
||||
"""
|
||||
A transformer decoder that attends to an input image using
|
||||
queries whose positional embedding is supplied.
|
||||
|
||||
Args:
|
||||
depth (int): number of layers in the transformer
|
||||
embedding_dim (int): the channel dimension for the input embeddings
|
||||
num_heads (int): the number of heads for multihead attention. Must
|
||||
divide embedding_dim
|
||||
mlp_dim (int): the channel dimension internal to the MLP block
|
||||
activation (nn.Module): the activation to use in the MLP block
|
||||
"""
|
||||
super().__init__()
|
||||
self.depth = depth
|
||||
self.embedding_dim = embedding_dim
|
||||
self.num_heads = num_heads
|
||||
self.mlp_dim = mlp_dim
|
||||
self.layers = nn.ModuleList()
|
||||
|
||||
for i in range(depth):
|
||||
self.layers.append(
|
||||
TwoWayAttentionBlock(
|
||||
embedding_dim=embedding_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_dim=mlp_dim,
|
||||
activation=activation,
|
||||
attention_downsample_rate=attention_downsample_rate,
|
||||
skip_first_layer_pe=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
self.final_attn_token_to_image = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
self.norm_final_attn = nn.LayerNorm(embedding_dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embedding: Tensor,
|
||||
image_pe: Tensor,
|
||||
point_embedding: Tensor,
|
||||
) -> Tuple[Tensor, Tensor]:
|
||||
"""
|
||||
Args:
|
||||
image_embedding (torch.Tensor): image to attend to. Should be shape
|
||||
B x embedding_dim x h x w for any h and w.
|
||||
image_pe (torch.Tensor): the positional encoding to add to the image. Must
|
||||
have the same shape as image_embedding.
|
||||
point_embedding (torch.Tensor): the embedding to add to the query points.
|
||||
Must have shape B x N_points x embedding_dim for any N_points.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: the processed point_embedding
|
||||
torch.Tensor: the processed image_embedding
|
||||
"""
|
||||
# BxCxHxW -> BxHWxC == B x N_image_tokens x C
|
||||
bs, c, h, w = image_embedding.shape
|
||||
image_embedding = image_embedding.flatten(2).permute(0, 2, 1)
|
||||
image_pe = image_pe.flatten(2).permute(0, 2, 1)
|
||||
|
||||
# Prepare queries
|
||||
queries = point_embedding
|
||||
keys = image_embedding
|
||||
|
||||
# Apply transformer blocks and final layernorm
|
||||
for layer in self.layers:
|
||||
queries, keys = layer(
|
||||
queries=queries,
|
||||
keys=keys,
|
||||
query_pe=point_embedding,
|
||||
key_pe=image_pe,
|
||||
)
|
||||
|
||||
# Apply the final attenion layer from the points to the image
|
||||
q = queries + point_embedding
|
||||
k = keys + image_pe
|
||||
attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm_final_attn(queries)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class TwoWayAttentionBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int = 2048,
|
||||
activation: Type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
skip_first_layer_pe: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
A transformer block with four layers: (1) self-attention of sparse
|
||||
inputs, (2) cross attention of sparse inputs to dense inputs, (3) mlp
|
||||
block on sparse inputs, and (4) cross attention of dense inputs to sparse
|
||||
inputs.
|
||||
|
||||
Arguments:
|
||||
embedding_dim (int): the channel dimension of the embeddings
|
||||
num_heads (int): the number of heads in the attention layers
|
||||
mlp_dim (int): the hidden dimension of the mlp block
|
||||
activation (nn.Module): the activation of the mlp block
|
||||
skip_first_layer_pe (bool): skip the PE on the first layer
|
||||
"""
|
||||
super().__init__()
|
||||
self.self_attn = Attention(embedding_dim, num_heads)
|
||||
self.norm1 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.cross_attn_token_to_image = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
self.norm2 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.mlp = MLPBlock(embedding_dim, mlp_dim, activation)
|
||||
self.norm3 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.norm4 = nn.LayerNorm(embedding_dim)
|
||||
self.cross_attn_image_to_token = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
|
||||
self.skip_first_layer_pe = skip_first_layer_pe
|
||||
|
||||
def forward(
|
||||
self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor
|
||||
) -> Tuple[Tensor, Tensor]:
|
||||
# Self attention block
|
||||
if self.skip_first_layer_pe:
|
||||
queries = self.self_attn(q=queries, k=queries, v=queries)
|
||||
else:
|
||||
q = queries + query_pe
|
||||
attn_out = self.self_attn(q=q, k=q, v=queries)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm1(queries)
|
||||
|
||||
# Cross attention block, tokens attending to image embedding
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm2(queries)
|
||||
|
||||
# MLP block
|
||||
mlp_out = self.mlp(queries)
|
||||
queries = queries + mlp_out
|
||||
queries = self.norm3(queries)
|
||||
|
||||
# Cross attention block, image embedding attending to tokens
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries)
|
||||
keys = keys + attn_out
|
||||
keys = self.norm4(keys)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""
|
||||
An attention layer that allows for downscaling the size of the embedding
|
||||
after projection to queries, keys, and values.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
downsample_rate: int = 1,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.embedding_dim = embedding_dim
|
||||
self.internal_dim = embedding_dim // downsample_rate
|
||||
self.num_heads = num_heads
|
||||
assert self.internal_dim % num_heads == 0, "num_heads must divide embedding_dim."
|
||||
|
||||
self.q_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.k_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.v_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.out_proj = nn.Linear(self.internal_dim, embedding_dim)
|
||||
|
||||
def _separate_heads(self, x: Tensor, num_heads: int) -> Tensor:
|
||||
b, n, c = x.shape
|
||||
x = x.reshape(b, n, num_heads, c // num_heads)
|
||||
return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head
|
||||
|
||||
def _recombine_heads(self, x: Tensor) -> Tensor:
|
||||
b, n_heads, n_tokens, c_per_head = x.shape
|
||||
x = x.transpose(1, 2)
|
||||
return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C
|
||||
|
||||
def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor:
|
||||
# Input projections
|
||||
q = self.q_proj(q)
|
||||
k = self.k_proj(k)
|
||||
v = self.v_proj(v)
|
||||
|
||||
# Separate into heads
|
||||
q = self._separate_heads(q, self.num_heads)
|
||||
k = self._separate_heads(k, self.num_heads)
|
||||
v = self._separate_heads(v, self.num_heads)
|
||||
|
||||
# Attention
|
||||
_, _, _, c_per_head = q.shape
|
||||
attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens
|
||||
attn = attn / math.sqrt(c_per_head)
|
||||
attn = torch.softmax(attn, dim=-1)
|
||||
|
||||
# Get output
|
||||
out = attn @ v
|
||||
out = self._recombine_heads(out)
|
||||
out = self.out_proj(out)
|
||||
|
||||
return out
|
||||
285
iopaint/plugins/segment_anything/predictor.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from .modeling import Sam
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class SamPredictor:
|
||||
def __init__(
|
||||
self,
|
||||
sam_model: Sam,
|
||||
) -> None:
|
||||
"""
|
||||
Uses SAM to calculate the image embedding for an image, and then
|
||||
allow repeated, efficient mask prediction given prompts.
|
||||
|
||||
Arguments:
|
||||
sam_model (Sam): The model to use for mask prediction.
|
||||
"""
|
||||
super().__init__()
|
||||
self.model = sam_model
|
||||
from .utils.transforms import ResizeLongestSide
|
||||
|
||||
self.transform = ResizeLongestSide(sam_model.image_encoder.img_size)
|
||||
self.reset_image()
|
||||
|
||||
def set_image(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
image_format: str = "RGB",
|
||||
) -> None:
|
||||
"""
|
||||
Calculates the image embeddings for the provided image, allowing
|
||||
masks to be predicted with the 'predict' method.
|
||||
|
||||
Arguments:
|
||||
image (np.ndarray): The image for calculating masks. Expects an
|
||||
image in HWC uint8 format, with pixel values in [0, 255].
|
||||
image_format (str): The color format of the image, in ['RGB', 'BGR'].
|
||||
"""
|
||||
assert image_format in [
|
||||
"RGB",
|
||||
"BGR",
|
||||
], f"image_format must be in ['RGB', 'BGR'], is {image_format}."
|
||||
if image_format != self.model.image_format:
|
||||
image = image[..., ::-1]
|
||||
|
||||
# Transform the image to the form expected by the model
|
||||
input_image = self.transform.apply_image(image)
|
||||
input_image_torch = torch.as_tensor(input_image, device=self.device)
|
||||
input_image_torch = input_image_torch.permute(2, 0, 1).contiguous()[
|
||||
None, :, :, :
|
||||
]
|
||||
|
||||
self.set_torch_image(input_image_torch, image.shape[:2])
|
||||
|
||||
@torch.no_grad()
|
||||
def set_torch_image(
|
||||
self,
|
||||
transformed_image: torch.Tensor,
|
||||
original_image_size: Tuple[int, ...],
|
||||
) -> None:
|
||||
"""
|
||||
Calculates the image embeddings for the provided image, allowing
|
||||
masks to be predicted with the 'predict' method. Expects the input
|
||||
image to be already transformed to the format expected by the model.
|
||||
|
||||
Arguments:
|
||||
transformed_image (torch.Tensor): The input image, with shape
|
||||
1x3xHxW, which has been transformed with ResizeLongestSide.
|
||||
original_image_size (tuple(int, int)): The size of the image
|
||||
before transformation, in (H, W) format.
|
||||
"""
|
||||
assert (
|
||||
len(transformed_image.shape) == 4
|
||||
and transformed_image.shape[1] == 3
|
||||
and max(*transformed_image.shape[2:]) == self.model.image_encoder.img_size
|
||||
), f"set_torch_image input must be BCHW with long side {self.model.image_encoder.img_size}."
|
||||
self.reset_image()
|
||||
|
||||
self.original_size = original_image_size
|
||||
self.input_size = tuple(transformed_image.shape[-2:])
|
||||
input_image = self.model.preprocess(transformed_image)
|
||||
self.features = self.model.image_encoder(input_image)
|
||||
self.is_image_set = True
|
||||
|
||||
def predict(
|
||||
self,
|
||||
point_coords: Optional[np.ndarray] = None,
|
||||
point_labels: Optional[np.ndarray] = None,
|
||||
box: Optional[np.ndarray] = None,
|
||||
mask_input: Optional[np.ndarray] = None,
|
||||
multimask_output: bool = True,
|
||||
return_logits: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Predict masks for the given input prompts, using the currently set image.
|
||||
|
||||
Arguments:
|
||||
point_coords (np.ndarray or None): A Nx2 array of point prompts to the
|
||||
model. Each point is in (X,Y) in pixels.
|
||||
point_labels (np.ndarray or None): A length N array of labels for the
|
||||
point prompts. 1 indicates a foreground point and 0 indicates a
|
||||
background point.
|
||||
box (np.ndarray or None): A length 4 array given a box prompt to the
|
||||
model, in XYXY format.
|
||||
mask_input (np.ndarray): A low resolution mask input to the model, typically
|
||||
coming from a previous prediction iteration. Has form 1xHxW, where
|
||||
for SAM, H=W=256.
|
||||
multimask_output (bool): If true, the model will return three masks.
|
||||
For ambiguous input prompts (such as a single click), this will often
|
||||
produce better masks than a single prediction. If only a single
|
||||
mask is needed, the model's predicted quality score can be used
|
||||
to select the best mask. For non-ambiguous prompts, such as multiple
|
||||
input prompts, multimask_output=False can give better results.
|
||||
return_logits (bool): If true, returns un-thresholded masks logits
|
||||
instead of a binary mask.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The output masks in CxHxW format, where C is the
|
||||
number of masks, and (H, W) is the original image size.
|
||||
(np.ndarray): An array of length C containing the model's
|
||||
predictions for the quality of each mask.
|
||||
(np.ndarray): An array of shape CxHxW, where C is the number
|
||||
of masks and H=W=256. These low resolution logits can be passed to
|
||||
a subsequent iteration as mask input.
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) before mask prediction."
|
||||
)
|
||||
|
||||
# Transform input prompts
|
||||
coords_torch, labels_torch, box_torch, mask_input_torch = None, None, None, None
|
||||
if point_coords is not None:
|
||||
assert (
|
||||
point_labels is not None
|
||||
), "point_labels must be supplied if point_coords is supplied."
|
||||
point_coords = self.transform.apply_coords(point_coords, self.original_size)
|
||||
coords_torch = torch.as_tensor(
|
||||
point_coords, dtype=torch.float, device=self.device
|
||||
)
|
||||
labels_torch = torch.as_tensor(
|
||||
point_labels, dtype=torch.int, device=self.device
|
||||
)
|
||||
coords_torch, labels_torch = coords_torch[None, :, :], labels_torch[None, :]
|
||||
if box is not None:
|
||||
box = self.transform.apply_boxes(box, self.original_size)
|
||||
box_torch = torch.as_tensor(box, dtype=torch.float, device=self.device)
|
||||
box_torch = box_torch[None, :]
|
||||
if mask_input is not None:
|
||||
mask_input_torch = torch.as_tensor(
|
||||
mask_input, dtype=torch.float, device=self.device
|
||||
)
|
||||
mask_input_torch = mask_input_torch[None, :, :, :]
|
||||
|
||||
masks, iou_predictions, low_res_masks = self.predict_torch(
|
||||
coords_torch,
|
||||
labels_torch,
|
||||
box_torch,
|
||||
mask_input_torch,
|
||||
multimask_output,
|
||||
return_logits=return_logits,
|
||||
)
|
||||
|
||||
masks = masks[0].detach().cpu().numpy()
|
||||
iou_predictions = iou_predictions[0].detach().cpu().numpy()
|
||||
low_res_masks = low_res_masks[0].detach().cpu().numpy()
|
||||
return masks, iou_predictions, low_res_masks
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_torch(
|
||||
self,
|
||||
point_coords: Optional[torch.Tensor],
|
||||
point_labels: Optional[torch.Tensor],
|
||||
boxes: Optional[torch.Tensor] = None,
|
||||
mask_input: Optional[torch.Tensor] = None,
|
||||
multimask_output: bool = True,
|
||||
return_logits: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Predict masks for the given input prompts, using the currently set image.
|
||||
Input prompts are batched torch tensors and are expected to already be
|
||||
transformed to the input frame using ResizeLongestSide.
|
||||
|
||||
Arguments:
|
||||
point_coords (torch.Tensor or None): A BxNx2 array of point prompts to the
|
||||
model. Each point is in (X,Y) in pixels.
|
||||
point_labels (torch.Tensor or None): A BxN array of labels for the
|
||||
point prompts. 1 indicates a foreground point and 0 indicates a
|
||||
background point.
|
||||
box (np.ndarray or None): A Bx4 array given a box prompt to the
|
||||
model, in XYXY format.
|
||||
mask_input (np.ndarray): A low resolution mask input to the model, typically
|
||||
coming from a previous prediction iteration. Has form Bx1xHxW, where
|
||||
for SAM, H=W=256. Masks returned by a previous iteration of the
|
||||
predict method do not need further transformation.
|
||||
multimask_output (bool): If true, the model will return three masks.
|
||||
For ambiguous input prompts (such as a single click), this will often
|
||||
produce better masks than a single prediction. If only a single
|
||||
mask is needed, the model's predicted quality score can be used
|
||||
to select the best mask. For non-ambiguous prompts, such as multiple
|
||||
input prompts, multimask_output=False can give better results.
|
||||
return_logits (bool): If true, returns un-thresholded masks logits
|
||||
instead of a binary mask.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): The output masks in BxCxHxW format, where C is the
|
||||
number of masks, and (H, W) is the original image size.
|
||||
(torch.Tensor): An array of shape BxC containing the model's
|
||||
predictions for the quality of each mask.
|
||||
(torch.Tensor): An array of shape BxCxHxW, where C is the number
|
||||
of masks and H=W=256. These low res logits can be passed to
|
||||
a subsequent iteration as mask input.
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) before mask prediction."
|
||||
)
|
||||
|
||||
if point_coords is not None:
|
||||
points = (point_coords, point_labels)
|
||||
else:
|
||||
points = None
|
||||
|
||||
# Embed prompts
|
||||
sparse_embeddings, dense_embeddings = self.model.prompt_encoder(
|
||||
points=points,
|
||||
boxes=boxes,
|
||||
masks=mask_input,
|
||||
)
|
||||
|
||||
# Predict masks
|
||||
low_res_masks, iou_predictions = self.model.mask_decoder(
|
||||
image_embeddings=self.features,
|
||||
image_pe=self.model.prompt_encoder.get_dense_pe(),
|
||||
sparse_prompt_embeddings=sparse_embeddings,
|
||||
dense_prompt_embeddings=dense_embeddings,
|
||||
multimask_output=multimask_output,
|
||||
)
|
||||
|
||||
# Upscale the masks to the original image resolution
|
||||
masks = self.model.postprocess_masks(
|
||||
low_res_masks, self.input_size, self.original_size
|
||||
)
|
||||
|
||||
if not return_logits:
|
||||
masks = masks > self.model.mask_threshold
|
||||
|
||||
return masks, iou_predictions, low_res_masks
|
||||
|
||||
def get_image_embedding(self) -> torch.Tensor:
|
||||
"""
|
||||
Returns the image embeddings for the currently set image, with
|
||||
shape 1xCxHxW, where C is the embedding dimension and (H,W) are
|
||||
the embedding spatial dimension of SAM (typically C=256, H=W=64).
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) to generate an embedding."
|
||||
)
|
||||
assert (
|
||||
self.features is not None
|
||||
), "Features must exist if an image has been set."
|
||||
return self.features
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return self.model.device
|
||||
|
||||
def reset_image(self) -> None:
|
||||
"""Resets the currently set image."""
|
||||
self.is_image_set = False
|
||||
self.features = None
|
||||
self.orig_h = None
|
||||
self.orig_w = None
|
||||
self.input_h = None
|
||||
self.input_w = None
|
||||
5
iopaint/plugins/segment_anything/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
112
iopaint/plugins/segment_anything/utils/transforms.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
from torchvision.transforms.functional import resize, to_pil_image # type: ignore
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class ResizeLongestSide:
|
||||
"""
|
||||
Resizes images to longest side 'target_length', as well as provides
|
||||
methods for resizing coordinates and boxes. Provides methods for
|
||||
transforming both numpy array and batched torch tensors.
|
||||
"""
|
||||
|
||||
def __init__(self, target_length: int) -> None:
|
||||
self.target_length = target_length
|
||||
|
||||
def apply_image(self, image: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array with shape HxWxC in uint8 format.
|
||||
"""
|
||||
target_size = self.get_preprocess_shape(
|
||||
image.shape[0], image.shape[1], self.target_length
|
||||
)
|
||||
return np.array(resize(to_pil_image(image), target_size))
|
||||
|
||||
def apply_coords(
|
||||
self, coords: np.ndarray, original_size: Tuple[int, ...]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array of length 2 in the final dimension. Requires the
|
||||
original image size in (H, W) format.
|
||||
"""
|
||||
old_h, old_w = original_size
|
||||
new_h, new_w = self.get_preprocess_shape(
|
||||
original_size[0], original_size[1], self.target_length
|
||||
)
|
||||
coords = deepcopy(coords).astype(float)
|
||||
coords[..., 0] = coords[..., 0] * (new_w / old_w)
|
||||
coords[..., 1] = coords[..., 1] * (new_h / old_h)
|
||||
return coords
|
||||
|
||||
def apply_boxes(
|
||||
self, boxes: np.ndarray, original_size: Tuple[int, ...]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array shape Bx4. Requires the original image size
|
||||
in (H, W) format.
|
||||
"""
|
||||
boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size)
|
||||
return boxes.reshape(-1, 4)
|
||||
|
||||
def apply_image_torch(self, image: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Expects batched images with shape BxCxHxW and float format. This
|
||||
transformation may not exactly match apply_image. apply_image is
|
||||
the transformation expected by the model.
|
||||
"""
|
||||
# Expects an image in BCHW format. May not exactly match apply_image.
|
||||
target_size = self.get_preprocess_shape(
|
||||
image.shape[0], image.shape[1], self.target_length
|
||||
)
|
||||
return F.interpolate(
|
||||
image, target_size, mode="bilinear", align_corners=False, antialias=True
|
||||
)
|
||||
|
||||
def apply_coords_torch(
|
||||
self, coords: torch.Tensor, original_size: Tuple[int, ...]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Expects a torch tensor with length 2 in the last dimension. Requires the
|
||||
original image size in (H, W) format.
|
||||
"""
|
||||
old_h, old_w = original_size
|
||||
new_h, new_w = self.get_preprocess_shape(
|
||||
original_size[0], original_size[1], self.target_length
|
||||
)
|
||||
coords = deepcopy(coords).to(torch.float)
|
||||
coords[..., 0] = coords[..., 0] * (new_w / old_w)
|
||||
coords[..., 1] = coords[..., 1] * (new_h / old_h)
|
||||
return coords
|
||||
|
||||
def apply_boxes_torch(
|
||||
self, boxes: torch.Tensor, original_size: Tuple[int, ...]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Expects a torch tensor with shape Bx4. Requires the original image
|
||||
size in (H, W) format.
|
||||
"""
|
||||
boxes = self.apply_coords_torch(boxes.reshape(-1, 2, 2), original_size)
|
||||
return boxes.reshape(-1, 4)
|
||||
|
||||
@staticmethod
|
||||
def get_preprocess_shape(
|
||||
oldh: int, oldw: int, long_side_length: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Compute the output size given input size and target long side length.
|
||||
"""
|
||||
scale = long_side_length * 1.0 / max(oldh, oldw)
|
||||
newh, neww = oldh * scale, oldw * scale
|
||||
neww = int(neww + 0.5)
|
||||
newh = int(newh + 0.5)
|
||||
return (newh, neww)
|
||||
86
iopaint/runtime.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# https://github.com/huggingface/huggingface_hub/blob/5a12851f54bf614be39614034ed3a9031922d297/src/huggingface_hub/utils/_runtime.py
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import packaging.version
|
||||
from loguru import logger
|
||||
from rich import print
|
||||
from typing import Dict, Any
|
||||
|
||||
from iopaint.const import Device
|
||||
|
||||
_PY_VERSION: str = sys.version.split()[0].rstrip("+")
|
||||
|
||||
if packaging.version.Version(_PY_VERSION) < packaging.version.Version("3.8.0"):
|
||||
import importlib_metadata # type: ignore
|
||||
else:
|
||||
import importlib.metadata as importlib_metadata # type: ignore
|
||||
|
||||
_package_versions = {}
|
||||
|
||||
_CANDIDATES = [
|
||||
"torch",
|
||||
"torchvision",
|
||||
"Pillow",
|
||||
"diffusers",
|
||||
"transformers",
|
||||
"opencv-python",
|
||||
"accelerate",
|
||||
"iopaint",
|
||||
"rembg",
|
||||
"realesrgan",
|
||||
"gfpgan",
|
||||
]
|
||||
# Check once at runtime
|
||||
for name in _CANDIDATES:
|
||||
_package_versions[name] = "N/A"
|
||||
try:
|
||||
_package_versions[name] = importlib_metadata.version(name)
|
||||
except importlib_metadata.PackageNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def dump_environment_info() -> Dict[str, str]:
|
||||
"""Dump information about the machine to help debugging issues."""
|
||||
|
||||
# Generic machine info
|
||||
info: Dict[str, Any] = {
|
||||
"Platform": platform.platform(),
|
||||
"Python version": platform.python_version(),
|
||||
}
|
||||
info.update(_package_versions)
|
||||
print("\n".join([f"- {prop}: {val}" for prop, val in info.items()]) + "\n")
|
||||
return info
|
||||
|
||||
|
||||
def check_device(device: Device) -> Device:
|
||||
if device == Device.cuda:
|
||||
import platform
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
logger.warning("MacOS does not support cuda, use cpu instead")
|
||||
return Device.cpu
|
||||
else:
|
||||
import torch
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
logger.warning("CUDA is not available, use cpu instead")
|
||||
return Device.cpu
|
||||
elif device == Device.mps:
|
||||
import torch
|
||||
|
||||
if not torch.backends.mps.is_available():
|
||||
logger.warning("mps is not available, use cpu instead")
|
||||
return Device.cpu
|
||||
return device
|
||||
|
||||
|
||||
def setup_model_dir(model_dir: Path):
|
||||
model_dir = model_dir.expanduser().absolute()
|
||||
os.environ["U2NET_HOME"] = str(model_dir)
|
||||
os.environ["XDG_CACHE_HOME"] = str(model_dir)
|
||||
if not model_dir.exists():
|
||||
logger.info(f"Create model directory: {model_dir}")
|
||||
model_dir.mkdir(exist_ok=True, parents=True)
|
||||
300
iopaint/schema.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import random
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Literal, List
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from iopaint.const import Device, InteractiveSegModel, RealESRGANModel
|
||||
|
||||
|
||||
class PluginInfo(BaseModel):
|
||||
name: str
|
||||
support_gen_image: bool = False
|
||||
support_gen_mask: bool = False
|
||||
|
||||
|
||||
class CV2Flag(str, Enum):
|
||||
INPAINT_NS = "INPAINT_NS"
|
||||
INPAINT_TELEA = "INPAINT_TELEA"
|
||||
|
||||
|
||||
class ModelType(str, Enum):
|
||||
INPAINT = "inpaint" # LaMa, MAT...
|
||||
DIFFUSERS_SD = "diffusers_sd"
|
||||
DIFFUSERS_SD_INPAINT = "diffusers_sd_inpaint"
|
||||
DIFFUSERS_SDXL = "diffusers_sdxl"
|
||||
DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
|
||||
DIFFUSERS_OTHER = "diffusers_other"
|
||||
|
||||
|
||||
class HDStrategy(str, Enum):
|
||||
# Use original image size
|
||||
ORIGINAL = "Original"
|
||||
# Resize the longer side of the image to a specific size(hd_strategy_resize_limit),
|
||||
# then do inpainting on the resized image. Finally, resize the inpainting result to the original size.
|
||||
# The area outside the mask will not lose quality.
|
||||
RESIZE = "Resize"
|
||||
# Crop masking area(with a margin controlled by hd_strategy_crop_margin) from the original image to do inpainting
|
||||
CROP = "Crop"
|
||||
|
||||
|
||||
class LDMSampler(str, Enum):
|
||||
ddim = "ddim"
|
||||
plms = "plms"
|
||||
|
||||
|
||||
class SDSampler(str, Enum):
|
||||
dpm_plus_plus_2m = "DPM++ 2M"
|
||||
dpm_plus_plus_2m_karras = "DPM++ 2M Karras"
|
||||
dpm_plus_plus_2m_sde = "DPM++ 2M SDE"
|
||||
dpm_plus_plus_2m_sde_karras = "DPM++ 2M SDE Karras"
|
||||
dpm_plus_plus_sde = "DPM++ SDE"
|
||||
dpm_plus_plus_sde_karras = "DPM++ SDE Karras"
|
||||
dpm2 = "DPM2"
|
||||
dpm2_karras = "DPM2 Karras"
|
||||
dpm2_a = "DPM2 a"
|
||||
dpm2_a_karras = "DPM2 a Karras"
|
||||
euler = "Euler"
|
||||
euler_a = "Euler a"
|
||||
heun = "Heun"
|
||||
lms = "LMS"
|
||||
lms_karras = "LMS Karras"
|
||||
|
||||
ddim = "DDIM"
|
||||
pndm = "PNDM"
|
||||
uni_pc = "UniPC"
|
||||
lcm = "LCM"
|
||||
|
||||
|
||||
class FREEUConfig(BaseModel):
|
||||
s1: float = 0.9
|
||||
s2: float = 0.2
|
||||
b1: float = 1.2
|
||||
b2: float = 1.4
|
||||
|
||||
|
||||
class PowerPaintTask(str, Enum):
|
||||
text_guided = "text-guided"
|
||||
shape_guided = "shape-guided"
|
||||
object_remove = "object-remove"
|
||||
outpainting = "outpainting"
|
||||
|
||||
|
||||
class ApiConfig(BaseModel):
|
||||
host: str
|
||||
port: int
|
||||
model: str
|
||||
no_half: bool
|
||||
cpu_offload: bool
|
||||
disable_nsfw_checker: bool
|
||||
cpu_textencoder: bool
|
||||
device: Device
|
||||
gui: bool
|
||||
disable_model_switch: bool
|
||||
input: Path
|
||||
output_dir: Path
|
||||
quality: int
|
||||
enable_interactive_seg: bool
|
||||
interactive_seg_model: InteractiveSegModel
|
||||
interactive_seg_device: Device
|
||||
enable_remove_bg: bool
|
||||
enable_anime_seg: bool
|
||||
enable_realesrgan: bool
|
||||
realesrgan_device: Device
|
||||
realesrgan_model: RealESRGANModel
|
||||
enable_gfpgan: bool
|
||||
gfpgan_device: Device
|
||||
enable_restoreformer: bool
|
||||
restoreformer_device: Device
|
||||
|
||||
|
||||
class InpaintRequest(BaseModel):
|
||||
image: Optional[str] = Field(None, description="base64 encoded image")
|
||||
mask: Optional[str] = Field(None, description="base64 encoded mask")
|
||||
|
||||
ldm_steps: int = Field(20, description="Steps for ldm model.")
|
||||
ldm_sampler: str = Field(LDMSampler.plms, discription="Sampler for ldm model.")
|
||||
zits_wireframe: bool = Field(True, description="Enable wireframe for zits model.")
|
||||
|
||||
hd_strategy: str = Field(
|
||||
HDStrategy.CROP,
|
||||
description="Different way to preprocess image, only used by erase models(e.g. lama/mat)",
|
||||
)
|
||||
hd_strategy_crop_trigger_size: int = Field(
|
||||
800,
|
||||
description="Crop trigger size for hd_strategy=CROP, if the longer side of the image is larger than this value, use crop strategy",
|
||||
)
|
||||
hd_strategy_crop_margin: int = Field(
|
||||
128, description="Crop margin for hd_strategy=CROP"
|
||||
)
|
||||
hd_strategy_resize_limit: int = Field(
|
||||
1280, description="Resize limit for hd_strategy=RESIZE"
|
||||
)
|
||||
|
||||
prompt: str = Field("", description="Prompt for diffusion models.")
|
||||
negative_prompt: str = Field(
|
||||
"", description="Negative prompt for diffusion models."
|
||||
)
|
||||
use_croper: bool = Field(
|
||||
False, description="Crop image before doing diffusion inpainting"
|
||||
)
|
||||
croper_x: int = Field(0, description="Crop x for croper")
|
||||
croper_y: int = Field(0, description="Crop y for croper")
|
||||
croper_height: int = Field(512, description="Crop height for croper")
|
||||
croper_width: int = Field(512, description="Crop width for croper")
|
||||
|
||||
use_extender: bool = Field(
|
||||
False, description="Extend image before doing sd outpainting"
|
||||
)
|
||||
extender_x: int = Field(0, description="Extend x for extender")
|
||||
extender_y: int = Field(0, description="Extend y for extender")
|
||||
extender_height: int = Field(640, description="Extend height for extender")
|
||||
extender_width: int = Field(640, description="Extend width for extender")
|
||||
|
||||
sd_scale: float = Field(
|
||||
1.0,
|
||||
description="Resize the image before doing sd inpainting, the area outside the mask will not lose quality.",
|
||||
gt=0.0,
|
||||
le=1.0,
|
||||
)
|
||||
sd_mask_blur: int = Field(
|
||||
11,
|
||||
description="Blur the edge of mask area. The higher the number the smoother blend with the original image",
|
||||
)
|
||||
sd_strength: float = Field(
|
||||
1.0,
|
||||
description="Strength is a measure of how much noise is added to the base image, which influences how similar the output is to the base image. Higher value means more noise and more different from the base image",
|
||||
le=1.0,
|
||||
)
|
||||
sd_steps: int = Field(
|
||||
50,
|
||||
description="The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference.",
|
||||
)
|
||||
sd_guidance_scale: float = Field(
|
||||
7.5,
|
||||
help="Higher guidance scale encourages to generate images that are closely linked to the text prompt, usually at the expense of lower image quality.",
|
||||
)
|
||||
sd_sampler: str = Field(
|
||||
SDSampler.uni_pc, description="Sampler for diffusion model."
|
||||
)
|
||||
sd_seed: int = Field(
|
||||
42,
|
||||
description="Seed for diffusion model. -1 mean random seed",
|
||||
validate_default=True,
|
||||
)
|
||||
sd_match_histograms: bool = Field(
|
||||
False,
|
||||
description="Match histograms between inpainting area and original image.",
|
||||
)
|
||||
|
||||
sd_outpainting_softness: float = Field(20.0)
|
||||
sd_outpainting_space: float = Field(20.0)
|
||||
|
||||
sd_freeu: bool = Field(
|
||||
False,
|
||||
description="Enable freeu mode. https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu",
|
||||
)
|
||||
sd_freeu_config: FREEUConfig = FREEUConfig()
|
||||
|
||||
sd_lcm_lora: bool = Field(
|
||||
False,
|
||||
description="Enable lcm-lora mode. https://huggingface.co/docs/diffusers/main/en/using-diffusers/inference_with_lcm#texttoimage",
|
||||
)
|
||||
|
||||
sd_keep_unmasked_area: bool = Field(
|
||||
True, description="Keep unmasked area unchanged"
|
||||
)
|
||||
|
||||
cv2_flag: CV2Flag = Field(
|
||||
CV2Flag.INPAINT_NS,
|
||||
description="Flag for opencv inpainting: https://docs.opencv.org/4.6.0/d7/d8b/group__photo__inpaint.html#gga8002a65f5a3328fbf15df81b842d3c3ca05e763003a805e6c11c673a9f4ba7d07",
|
||||
)
|
||||
cv2_radius: int = Field(
|
||||
4,
|
||||
description="Radius of a circular neighborhood of each point inpainted that is considered by the algorithm",
|
||||
)
|
||||
|
||||
# Paint by Example
|
||||
paint_by_example_example_image: Optional[str] = Field(
|
||||
None, description="Base64 encoded example image for paint by example model"
|
||||
)
|
||||
|
||||
# InstructPix2Pix
|
||||
p2p_image_guidance_scale: float = Field(1.5, description="Image guidance scale")
|
||||
|
||||
# ControlNet
|
||||
enable_controlnet: bool = Field(False, description="Enable controlnet")
|
||||
controlnet_conditioning_scale: float = Field(
|
||||
0.4, description="Conditioning scale", gt=0.0, le=1.0
|
||||
)
|
||||
controlnet_method: str = Field(
|
||||
"lllyasviel/control_v11p_sd15_canny", description="Controlnet method"
|
||||
)
|
||||
|
||||
# PowerPaint
|
||||
powerpaint_task: PowerPaintTask = Field(
|
||||
PowerPaintTask.text_guided, description="PowerPaint task"
|
||||
)
|
||||
fitting_degree: float = Field(
|
||||
1.0,
|
||||
description="Control the fitting degree of the generated objects to the mask shape.",
|
||||
gt=0.0,
|
||||
le=1.0,
|
||||
)
|
||||
|
||||
@field_validator("sd_seed")
|
||||
@classmethod
|
||||
def sd_seed_validator(cls, v: int) -> int:
|
||||
if v == -1:
|
||||
return random.randint(1, 99999999)
|
||||
return v
|
||||
|
||||
|
||||
class RunPluginRequest(BaseModel):
|
||||
name: str
|
||||
image: str = Field(..., description="base64 encoded image")
|
||||
clicks: List[List[int]] = Field(
|
||||
[], description="Clicks for interactive seg, [[x,y,0/1], [x2,y2,0/1]]"
|
||||
)
|
||||
scale: float = Field(2.0, description="Scale for upscaling")
|
||||
|
||||
|
||||
MediaTab = Literal["input", "output"]
|
||||
|
||||
|
||||
class MediasResponse(BaseModel):
|
||||
name: str
|
||||
height: int
|
||||
width: int
|
||||
ctime: float
|
||||
mtime: float
|
||||
|
||||
|
||||
class GenInfoResponse(BaseModel):
|
||||
prompt: str = ""
|
||||
negative_prompt: str = ""
|
||||
|
||||
|
||||
class ServerConfigResponse(BaseModel):
|
||||
plugins: List[PluginInfo]
|
||||
enableFileManager: bool
|
||||
enableAutoSaving: bool
|
||||
enableControlnet: bool
|
||||
controlnetMethod: Optional[str]
|
||||
disableModelSwitch: bool
|
||||
isDesktop: bool
|
||||
samplers: List[str]
|
||||
|
||||
|
||||
class SwitchModelRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
AdjustMaskOperate = Literal["expand", "shrink"]
|
||||
|
||||
|
||||
class AdjustMaskRequest(BaseModel):
|
||||
mask: str = Field(..., description="base64 encoded mask. 255 means area to do inpaint")
|
||||
operate: AdjustMaskOperate = Field(..., description="expand or shrink")
|
||||
kernel_size: int = Field(5, description="Kernel size for expanding mask")
|
||||
2
iopaint/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*_result.png
|
||||
result/
|
||||
0
iopaint/tests/__init__.py
Normal file
BIN
iopaint/tests/anime_test.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
iopaint/tests/bunny.jpeg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
iopaint/tests/cat.png
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
iopaint/tests/icc_profile_test.jpg
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
iopaint/tests/icc_profile_test.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
iopaint/tests/image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
iopaint/tests/mask.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
iopaint/tests/overture-creations-5sI6fQgYIuo.png
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
iopaint/tests/overture-creations-5sI6fQgYIuo_mask.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
iopaint/tests/overture-creations-5sI6fQgYIuo_mask_blur.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
iopaint/tests/png_parameter_test.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
15
iopaint/tests/test_adjust_mask.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import cv2
|
||||
from iopaint.helper import adjust_mask
|
||||
from iopaint.tests.utils import current_dir, save_dir
|
||||
|
||||
mask_p = current_dir / "overture-creations-5sI6fQgYIuo_mask.png"
|
||||
|
||||
|
||||
def test_adjust_mask():
|
||||
mask = cv2.imread(str(mask_p), cv2.IMREAD_GRAYSCALE)
|
||||
res_mask = adjust_mask(mask, 0, "expand")
|
||||
cv2.imwrite(str(save_dir / "adjust_mask_original.png"), res_mask)
|
||||
res_mask = adjust_mask(mask, 40, "expand")
|
||||
cv2.imwrite(str(save_dir / "adjust_mask_expand.png"), res_mask)
|
||||
res_mask = adjust_mask(mask, 20, "shrink")
|
||||
cv2.imwrite(str(save_dir / "adjust_mask_shrink.png"), res_mask)
|
||||
117
iopaint/tests/test_controlnet.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import os
|
||||
|
||||
from iopaint.const import SD_CONTROLNET_CHOICES
|
||||
from iopaint.tests.utils import current_dir, check_device, get_config, assert_equal
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy, SDSampler
|
||||
|
||||
|
||||
model_name = "runwayml/stable-diffusion-inpainting"
|
||||
|
||||
|
||||
def convert_controlnet_method_name(name):
|
||||
return name.replace("/", "--")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("controlnet_method", [SD_CONTROLNET_CHOICES[0]])
|
||||
def test_runway_sd_1_5(device, controlnet_method):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name=model_name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=True,
|
||||
enable_controlnet=True,
|
||||
controlnet_method=controlnet_method,
|
||||
)
|
||||
|
||||
cfg = get_config(
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
enable_controlnet=True,
|
||||
controlnet_conditioning_scale=0.5,
|
||||
controlnet_method=controlnet_method,
|
||||
)
|
||||
name = f"device_{device}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sd_controlnet_{convert_controlnet_method_name(controlnet_method)}_{name}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
def test_controlnet_switch(device):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name=model_name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
cpu_offload=True,
|
||||
enable_controlnet=True,
|
||||
controlnet_method="lllyasviel/control_v11p_sd15_canny",
|
||||
)
|
||||
cfg = get_config(
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
enable_controlnet=True,
|
||||
controlnet_method="lllyasviel/control_v11f1p_sd15_depth",
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"controlnet_switch_canny_to_depth_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"local_file", ["sd-v1-5-inpainting.ckpt", "v1-5-pruned-emaonly.safetensors"]
|
||||
)
|
||||
def test_local_file_path(device, local_file):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
controlnet_kwargs = dict(
|
||||
enable_controlnet=True,
|
||||
controlnet_method=SD_CONTROLNET_CHOICES[0],
|
||||
)
|
||||
|
||||
model = ModelManager(
|
||||
name=local_file,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
cpu_offload=True,
|
||||
**controlnet_kwargs,
|
||||
)
|
||||
cfg = get_config(
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
**controlnet_kwargs,
|
||||
)
|
||||
|
||||
name = f"device_{device}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"{controlnet_kwargs['controlnet_method']}_local_model_{name}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
41
iopaint/tests/test_instruct_pix2pix.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy
|
||||
from iopaint.tests.utils import get_config, check_device, assert_equal, current_dir
|
||||
|
||||
model_name = "timbrooks/instruct-pix2pix"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("disable_nsfw", [True, False])
|
||||
@pytest.mark.parametrize("cpu_offload", [False, True])
|
||||
def test_instruct_pix2pix(device, disable_nsfw, cpu_offload):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name=model_name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=disable_nsfw,
|
||||
sd_cpu_textencoder=False,
|
||||
cpu_offload=cpu_offload,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="What if it were snowing?",
|
||||
p2p_steps=sd_steps,
|
||||
sd_scale=1.1,
|
||||
)
|
||||
|
||||
name = f"device_{device}_disnsfw_{disable_nsfw}_cpu_offload_{cpu_offload}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"instruct_pix2pix_{name}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=1.3,
|
||||
)
|
||||
19
iopaint/tests/test_load_img.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from iopaint.helper import load_img
|
||||
from iopaint.tests.utils import current_dir
|
||||
|
||||
png_img_p = current_dir / "image.png"
|
||||
jpg_img_p = current_dir / "bunny.jpeg"
|
||||
|
||||
|
||||
def test_load_png_image():
|
||||
with open(png_img_p, "rb") as f:
|
||||
np_img, alpha_channel = load_img(f.read())
|
||||
assert np_img.shape == (256, 256, 3)
|
||||
assert alpha_channel.shape == (256, 256)
|
||||
|
||||
|
||||
def test_load_jpg_image():
|
||||
with open(jpg_img_p, "rb") as f:
|
||||
np_img, alpha_channel = load_img(f.read())
|
||||
assert np_img.shape == (394, 448, 3)
|
||||
assert alpha_channel is None
|
||||
160
iopaint/tests/test_model.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy, LDMSampler
|
||||
from iopaint.tests.utils import assert_equal, get_config, current_dir, check_device
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"strategy", [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]
|
||||
)
|
||||
def test_lama(device, strategy):
|
||||
check_device(device)
|
||||
model = ModelManager(name="lama", device=device)
|
||||
assert_equal(
|
||||
model,
|
||||
get_config(strategy=strategy),
|
||||
f"lama_{strategy[0].upper() + strategy[1:]}_result.png",
|
||||
)
|
||||
|
||||
fx = 1.3
|
||||
assert_equal(
|
||||
model,
|
||||
get_config(strategy=strategy),
|
||||
f"lama_{strategy[0].upper() + strategy[1:]}_fx_{fx}_result.png",
|
||||
fx=1.3,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"strategy", [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]
|
||||
)
|
||||
@pytest.mark.parametrize("ldm_sampler", [LDMSampler.ddim, LDMSampler.plms])
|
||||
def test_ldm(device, strategy, ldm_sampler):
|
||||
check_device(device)
|
||||
model = ModelManager(name="ldm", device=device)
|
||||
cfg = get_config(strategy=strategy, ldm_sampler=ldm_sampler)
|
||||
assert_equal(
|
||||
model, cfg, f"ldm_{strategy[0].upper() + strategy[1:]}_{ldm_sampler}_result.png"
|
||||
)
|
||||
|
||||
fx = 1.3
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"ldm_{strategy[0].upper() + strategy[1:]}_{ldm_sampler}_fx_{fx}_result.png",
|
||||
fx=fx,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"strategy", [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]
|
||||
)
|
||||
@pytest.mark.parametrize("zits_wireframe", [False, True])
|
||||
def test_zits(device, strategy, zits_wireframe):
|
||||
check_device(device)
|
||||
model = ModelManager(name="zits", device=device)
|
||||
cfg = get_config(strategy=strategy, zits_wireframe=zits_wireframe)
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"zits_{strategy[0].upper() + strategy[1:]}_wireframe_{zits_wireframe}_result.png",
|
||||
)
|
||||
|
||||
fx = 1.3
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"zits_{strategy.capitalize()}_wireframe_{zits_wireframe}_fx_{fx}_result.png",
|
||||
fx=fx,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("no_half", [True, False])
|
||||
def test_mat(device, strategy, no_half):
|
||||
check_device(device)
|
||||
model = ModelManager(name="mat", device=device, no_half=no_half)
|
||||
cfg = get_config(strategy=strategy)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"mat_{strategy.capitalize()}_result.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
def test_fcf(device, strategy):
|
||||
check_device(device)
|
||||
model = ModelManager(name="fcf", device=device)
|
||||
cfg = get_config(strategy=strategy)
|
||||
|
||||
assert_equal(model, cfg, f"fcf_{strategy.capitalize()}_result.png", fx=2, fy=2)
|
||||
assert_equal(model, cfg, f"fcf_{strategy.capitalize()}_result.png", fx=3.8, fy=2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"strategy", [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]
|
||||
)
|
||||
@pytest.mark.parametrize("cv2_flag", ["INPAINT_NS", "INPAINT_TELEA"])
|
||||
@pytest.mark.parametrize("cv2_radius", [3, 15])
|
||||
def test_cv2(strategy, cv2_flag, cv2_radius):
|
||||
model = ModelManager(
|
||||
name="cv2",
|
||||
device=torch.device("cpu"),
|
||||
)
|
||||
cfg = get_config(strategy=strategy, cv2_flag=cv2_flag, cv2_radius=cv2_radius)
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"cv2_{strategy.capitalize()}_{cv2_flag}_{cv2_radius}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"strategy", [HDStrategy.ORIGINAL, HDStrategy.RESIZE, HDStrategy.CROP]
|
||||
)
|
||||
def test_manga(device, strategy):
|
||||
check_device(device)
|
||||
model = ModelManager(
|
||||
name="manga",
|
||||
device=torch.device(device),
|
||||
)
|
||||
cfg = get_config(strategy=strategy)
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"manga_{strategy.capitalize()}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
def test_mi_gan(device, strategy):
|
||||
check_device(device)
|
||||
model = ModelManager(
|
||||
name="migan",
|
||||
device=torch.device(device),
|
||||
)
|
||||
cfg = get_config(strategy=strategy)
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"migan_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=1.5,
|
||||
fy=1.7
|
||||
)
|
||||
16
iopaint/tests/test_model_md5.py
Normal file
@@ -0,0 +1,16 @@
|
||||
def test_load_model():
|
||||
from iopaint.plugins import InteractiveSeg
|
||||
from iopaint.model_manager import ModelManager
|
||||
|
||||
interactive_seg_model = InteractiveSeg("vit_l", "cpu")
|
||||
|
||||
models = ["lama", "ldm", "zits", "mat", "fcf", "manga", "migan"]
|
||||
for m in models:
|
||||
ModelManager(
|
||||
name=m,
|
||||
device="cpu",
|
||||
no_half=False,
|
||||
disable_nsfw=False,
|
||||
sd_cpu_textencoder=True,
|
||||
cpu_offload=True,
|
||||
)
|
||||
70
iopaint/tests/test_model_switch.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
from iopaint.schema import InpaintRequest
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
|
||||
|
||||
def test_model_switch():
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
enable_controlnet=True,
|
||||
controlnet_method="lllyasviel/control_v11p_sd15_canny",
|
||||
device=torch.device("mps"),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=True,
|
||||
cpu_offload=False,
|
||||
)
|
||||
|
||||
model.switch("lama")
|
||||
|
||||
|
||||
def test_controlnet_switch_onoff(caplog):
|
||||
name = "runwayml/stable-diffusion-inpainting"
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
enable_controlnet=True,
|
||||
controlnet_method="lllyasviel/control_v11p_sd15_canny",
|
||||
device=torch.device("mps"),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=True,
|
||||
cpu_offload=False,
|
||||
)
|
||||
|
||||
model.switch_controlnet_method(
|
||||
InpaintRequest(
|
||||
name=name,
|
||||
enable_controlnet=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert "Disable controlnet" in caplog.text
|
||||
|
||||
|
||||
def test_switch_controlnet_method(caplog):
|
||||
name = "runwayml/stable-diffusion-inpainting"
|
||||
old_method = "lllyasviel/control_v11p_sd15_canny"
|
||||
new_method = "lllyasviel/control_v11p_sd15_openpose"
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
enable_controlnet=True,
|
||||
controlnet_method=old_method,
|
||||
device=torch.device("mps"),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=True,
|
||||
cpu_offload=False,
|
||||
)
|
||||
|
||||
model.switch_controlnet_method(
|
||||
InpaintRequest(
|
||||
name=name,
|
||||
enable_controlnet=True,
|
||||
controlnet_method=new_method,
|
||||
)
|
||||
)
|
||||
|
||||
assert f"Switch Controlnet method from {old_method} to {new_method}" in caplog.text
|
||||
137
iopaint/tests/test_outpainting.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import os
|
||||
|
||||
from iopaint.tests.utils import current_dir, check_device
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy, SDSampler
|
||||
from iopaint.tests.test_model import get_config, assert_equal
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["runwayml/stable-diffusion-inpainting"])
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"rect",
|
||||
[
|
||||
[0, -100, 512, 512 - 128 + 100],
|
||||
[0, 128, 512, 512 - 128 + 100],
|
||||
[128, 0, 512 - 128 + 100, 512],
|
||||
[-100, 0, 512 - 128 + 100, 512],
|
||||
[0, 0, 512, 512 + 200],
|
||||
[0, 0, 512 + 200, 512],
|
||||
[-100, -100, 512 + 200, 512 + 200],
|
||||
],
|
||||
)
|
||||
def test_outpainting(name, device, rect):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
prompt="a dog sitting on a bench in the park",
|
||||
sd_steps=sd_steps,
|
||||
use_extender=True,
|
||||
extender_x=rect[0],
|
||||
extender_y=rect[1],
|
||||
extender_width=rect[2],
|
||||
extender_height=rect[3],
|
||||
sd_guidance_scale=8.0,
|
||||
sd_sampler=SDSampler.dpm_plus_plus_2m,
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"{name.replace('/', '--')}_outpainting_{'_'.join(map(str, rect))}_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["kandinsky-community/kandinsky-2-2-decoder-inpaint"])
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"rect",
|
||||
[
|
||||
[-128, -128, 768, 768],
|
||||
],
|
||||
)
|
||||
def test_kandinsky_outpainting(name, device, rect):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
prompt="a cat",
|
||||
negative_prompt="lowres, text, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, username, watermark, signature",
|
||||
sd_steps=sd_steps,
|
||||
use_extender=True,
|
||||
extender_x=rect[0],
|
||||
extender_y=rect[1],
|
||||
extender_width=rect[2],
|
||||
extender_height=rect[3],
|
||||
sd_guidance_scale=7,
|
||||
sd_sampler=SDSampler.dpm_plus_plus_2m,
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"{name.replace('/', '--')}_outpainting_{'_'.join(map(str, rect))}_device_{device}.png",
|
||||
img_p=current_dir / "cat.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=1,
|
||||
fy=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["Sanster/PowerPaint-V1-stable-diffusion-inpainting"])
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize(
|
||||
"rect",
|
||||
[
|
||||
[-100, -100, 512 + 200, 512 + 200],
|
||||
],
|
||||
)
|
||||
def test_powerpaint_outpainting(name, device, rect):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
prompt="a dog sitting on a bench in the park",
|
||||
sd_steps=sd_steps,
|
||||
use_extender=True,
|
||||
extender_x=rect[0],
|
||||
extender_y=rect[1],
|
||||
extender_width=rect[2],
|
||||
extender_height=rect[3],
|
||||
sd_guidance_scale=8.0,
|
||||
sd_sampler=SDSampler.dpm_plus_plus_2m,
|
||||
powerpaint_task="outpainting",
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"{name.replace('/', '--')}_outpainting_{'_'.join(map(str, rect))}_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
55
iopaint/tests/test_paint_by_example.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import cv2
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy
|
||||
from iopaint.tests.utils import (
|
||||
current_dir,
|
||||
get_config,
|
||||
get_data,
|
||||
save_dir,
|
||||
check_device,
|
||||
)
|
||||
|
||||
model_name = "Fantasy-Studio/Paint-by-Example"
|
||||
|
||||
|
||||
def assert_equal(
|
||||
model,
|
||||
config,
|
||||
save_name: str,
|
||||
fx: float = 1,
|
||||
fy: float = 1,
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
example_p=current_dir / "bunny.jpeg",
|
||||
):
|
||||
img, mask = get_data(fx=fx, fy=fy, img_p=img_p, mask_p=mask_p)
|
||||
|
||||
example_image = cv2.imread(str(example_p))
|
||||
example_image = cv2.cvtColor(example_image, cv2.COLOR_BGRA2RGB)
|
||||
example_image = cv2.resize(
|
||||
example_image, None, fx=fx, fy=fy, interpolation=cv2.INTER_AREA
|
||||
)
|
||||
|
||||
print(f"Input image shape: {img.shape}, example_image: {example_image.shape}")
|
||||
config.paint_by_example_example_image = Image.fromarray(example_image)
|
||||
res = model(img, mask, config)
|
||||
cv2.imwrite(str(save_dir / save_name), res)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
def test_paint_by_example(device):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(name=model_name, device=device, disable_nsfw=True)
|
||||
cfg = get_config(strategy=HDStrategy.ORIGINAL, sd_steps=sd_steps)
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"paint_by_example_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fy=0.9,
|
||||
fx=1.3,
|
||||
)
|
||||
120
iopaint/tests/test_plugins.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from PIL import Image
|
||||
|
||||
from iopaint.helper import encode_pil_to_base64, gen_frontend_mask
|
||||
from iopaint.plugins.anime_seg import AnimeSeg
|
||||
from iopaint.schema import RunPluginRequest
|
||||
from iopaint.tests.utils import check_device, current_dir, save_dir
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
|
||||
import cv2
|
||||
import pytest
|
||||
|
||||
from iopaint.plugins import (
|
||||
RemoveBG,
|
||||
RealESRGANUpscaler,
|
||||
GFPGANPlugin,
|
||||
RestoreFormerPlugin,
|
||||
InteractiveSeg,
|
||||
)
|
||||
|
||||
img_p = current_dir / "bunny.jpeg"
|
||||
img_bytes = open(img_p, "rb").read()
|
||||
bgr_img = cv2.imread(str(img_p))
|
||||
rgb_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
|
||||
rgb_img_base64 = encode_pil_to_base64(Image.fromarray(rgb_img), 100, {})
|
||||
bgr_img_base64 = encode_pil_to_base64(Image.fromarray(bgr_img), 100, {})
|
||||
|
||||
|
||||
def _save(img, name):
|
||||
cv2.imwrite(str(save_dir / name), img)
|
||||
|
||||
|
||||
def test_remove_bg():
|
||||
model = RemoveBG()
|
||||
rgba_np_img = model.gen_image(
|
||||
rgb_img, RunPluginRequest(name=RemoveBG.name, image=rgb_img_base64)
|
||||
)
|
||||
res = cv2.cvtColor(rgba_np_img, cv2.COLOR_RGBA2BGRA)
|
||||
_save(res, "test_remove_bg.png")
|
||||
|
||||
bgr_np_img = model.gen_mask(
|
||||
rgb_img, RunPluginRequest(name=RemoveBG.name, image=rgb_img_base64)
|
||||
)
|
||||
|
||||
res_mask = gen_frontend_mask(bgr_np_img)
|
||||
_save(res_mask, "test_remove_bg_frontend_mask.png")
|
||||
|
||||
assert len(bgr_np_img.shape) == 2
|
||||
_save(bgr_np_img, "test_remove_bg_mask.jpeg")
|
||||
|
||||
|
||||
def test_anime_seg():
|
||||
model = AnimeSeg()
|
||||
img = cv2.imread(str(current_dir / "anime_test.png"))
|
||||
img_base64 = encode_pil_to_base64(Image.fromarray(img), 100, {})
|
||||
res = model.gen_image(img, RunPluginRequest(name=AnimeSeg.name, image=img_base64))
|
||||
assert len(res.shape) == 3
|
||||
assert res.shape[-1] == 4
|
||||
_save(res, "test_anime_seg.png")
|
||||
|
||||
res = model.gen_mask(img, RunPluginRequest(name=AnimeSeg.name, image=img_base64))
|
||||
assert len(res.shape) == 2
|
||||
_save(res, "test_anime_seg_mask.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu", "mps"])
|
||||
def test_upscale(device):
|
||||
check_device(device)
|
||||
model = RealESRGANUpscaler("realesr-general-x4v3", device)
|
||||
res = model.gen_image(
|
||||
rgb_img,
|
||||
RunPluginRequest(name=RealESRGANUpscaler.name, image=rgb_img_base64, scale=2),
|
||||
)
|
||||
_save(res, f"test_upscale_x2_{device}.png")
|
||||
|
||||
res = model.gen_image(
|
||||
rgb_img,
|
||||
RunPluginRequest(name=RealESRGANUpscaler.name, image=rgb_img_base64, scale=4),
|
||||
)
|
||||
_save(res, f"test_upscale_x4_{device}.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu", "mps"])
|
||||
def test_gfpgan(device):
|
||||
check_device(device)
|
||||
model = GFPGANPlugin(device)
|
||||
res = model.gen_image(
|
||||
rgb_img, RunPluginRequest(name=GFPGANPlugin.name, image=rgb_img_base64)
|
||||
)
|
||||
_save(res, f"test_gfpgan_{device}.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu", "mps"])
|
||||
def test_restoreformer(device):
|
||||
check_device(device)
|
||||
model = RestoreFormerPlugin(device)
|
||||
res = model.gen_image(
|
||||
rgb_img, RunPluginRequest(name=RestoreFormerPlugin.name, image=rgb_img_base64)
|
||||
)
|
||||
_save(res, f"test_restoreformer_{device}.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "cpu", "mps"])
|
||||
def test_segment_anything(device):
|
||||
check_device(device)
|
||||
model = InteractiveSeg("vit_l", device)
|
||||
new_mask = model.gen_mask(
|
||||
rgb_img,
|
||||
RunPluginRequest(
|
||||
name=InteractiveSeg.name,
|
||||
image=rgb_img_base64,
|
||||
clicks=([[448 // 2, 394 // 2, 1]]),
|
||||
),
|
||||
)
|
||||
|
||||
save_name = f"test_segment_anything_{device}.png"
|
||||
_save(new_mask, save_name)
|
||||
59
iopaint/tests/test_save_exif.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import io
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from iopaint.helper import pil_to_bytes, load_img
|
||||
|
||||
current_dir = Path(__file__).parent.absolute().resolve()
|
||||
|
||||
|
||||
def print_exif(exif):
|
||||
for k, v in exif.items():
|
||||
print(f"{k}: {v}")
|
||||
|
||||
|
||||
def extra_info(img_p: Path):
|
||||
ext = img_p.suffix.strip(".")
|
||||
img_bytes = img_p.read_bytes()
|
||||
np_img, _, infos = load_img(img_bytes, False, True)
|
||||
res_pil_bytes = pil_to_bytes(Image.fromarray(np_img), ext=ext, infos=infos)
|
||||
res_img = Image.open(io.BytesIO(res_pil_bytes))
|
||||
return infos, res_img.info, res_pil_bytes
|
||||
|
||||
|
||||
def assert_keys(keys: List[str], infos, res_infos):
|
||||
for k in keys:
|
||||
assert k in infos
|
||||
assert k in res_infos
|
||||
assert infos[k] == res_infos[k]
|
||||
|
||||
|
||||
def run_test(file_path, keys):
|
||||
infos, res_infos, res_pil_bytes = extra_info(file_path)
|
||||
assert_keys(keys, infos, res_infos)
|
||||
with tempfile.NamedTemporaryFile("wb", suffix=file_path.suffix) as temp_file:
|
||||
temp_file.write(res_pil_bytes)
|
||||
temp_file.flush()
|
||||
infos, res_infos, res_pil_bytes = extra_info(Path(temp_file.name))
|
||||
assert_keys(keys, infos, res_infos)
|
||||
|
||||
|
||||
def test_png_icc_profile_png():
|
||||
run_test(current_dir / "icc_profile_test.png", ["icc_profile", "exif"])
|
||||
|
||||
|
||||
def test_png_icc_profile_jpeg():
|
||||
run_test(current_dir / "icc_profile_test.jpg", ["icc_profile", "exif"])
|
||||
|
||||
|
||||
def test_jpeg():
|
||||
jpg_img_p = current_dir / "bunny.jpeg"
|
||||
run_test(jpg_img_p, ["dpi", "exif"])
|
||||
|
||||
|
||||
def test_png_parameter():
|
||||
jpg_img_p = current_dir / "png_parameter_test.png"
|
||||
run_test(jpg_img_p, ["parameters"])
|
||||
237
iopaint/tests/test_sd_model.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import os
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.tests.utils import check_device, get_config, assert_equal
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy, SDSampler, FREEUConfig
|
||||
|
||||
current_dir = Path(__file__).parent.absolute().resolve()
|
||||
save_dir = current_dir / "result"
|
||||
save_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps"])
|
||||
def test_runway_sd_1_5_all_samplers(device):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
|
||||
all_samplers = [member.value for member in SDSampler.__members__.values()]
|
||||
print(all_samplers)
|
||||
for sampler in all_samplers:
|
||||
print(f"Testing sampler {sampler}")
|
||||
if (
|
||||
sampler
|
||||
in [SDSampler.dpm2_karras, SDSampler.dpm2_a_karras, SDSampler.lms_karras]
|
||||
and device == "mps"
|
||||
):
|
||||
# diffusers 0.25.0 still has bug on these sampler on mps, wait main branch released to fix it
|
||||
logger.warning(
|
||||
"skip dpm2_karras on mps, diffusers does not support it on mps. TypeError: Cannot convert a MPS Tensor to float64 dtype as the MPS framework doesn't support float64. Please use float32 instead."
|
||||
)
|
||||
continue
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_sampler=sampler,
|
||||
)
|
||||
|
||||
name = f"device_{device}_{sampler}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_sd_{name}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.lcm])
|
||||
def test_runway_sd_lcm_lora(device, sampler):
|
||||
check_device(device)
|
||||
|
||||
sd_steps = 5
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="face of a fox, sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_guidance_scale=2,
|
||||
sd_lcm_lora=True,
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_sd_1_5_lcm_lora_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
def test_runway_sd_freeu(device, sampler):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="face of a fox, sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_guidance_scale=7.5,
|
||||
sd_freeu=True,
|
||||
sd_freeu_config=FREEUConfig(),
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_sd_1_5_freeu_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
def test_runway_sd_sd_strength(device, strategy, sampler):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=strategy,
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_strength=0.8,
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_sd_strength_0.8_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
def test_runway_norm_sd_model(device, strategy, sampler):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-v1-5",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=strategy, prompt="face of a fox, sitting on a bench", sd_steps=sd_steps
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_{device}_norm_sd_model_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.dpm_plus_plus_2m])
|
||||
def test_runway_sd_1_5_cpu_offload(device, strategy, sampler):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name="runwayml/stable-diffusion-inpainting",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
cpu_offload=True,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=strategy, prompt="a fox sitting on a bench", sd_steps=sd_steps
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
name = f"device_{device}_{sampler}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"runway_sd_{strategy.capitalize()}_{name}_cpu_offload.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps", "cpu"])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"sd-v1-5-inpainting.ckpt",
|
||||
"sd-v1-5-inpainting.safetensors",
|
||||
"v1-5-pruned-emaonly.safetensors",
|
||||
],
|
||||
)
|
||||
def test_local_file_path(device, sampler, name):
|
||||
sd_steps = check_device(device)
|
||||
model = ModelManager(
|
||||
name=name,
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
cpu_offload=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="a fox sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
name = f"device_{device}_{sampler}_{name}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sd_local_model_{name}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
)
|
||||
140
iopaint/tests/test_sdxl.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
|
||||
from iopaint.tests.utils import check_device, current_dir
|
||||
|
||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.model_manager import ModelManager
|
||||
from iopaint.schema import HDStrategy, SDSampler, FREEUConfig
|
||||
from iopaint.tests.test_model import get_config, assert_equal
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
def test_sdxl(device, strategy, sampler):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name="diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=strategy,
|
||||
prompt="face of a fox, sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_strength=1.0,
|
||||
sd_guidance_scale=7.0,
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sdxl_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=2,
|
||||
fy=2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps"])
|
||||
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
|
||||
@pytest.mark.parametrize("sampler", [SDSampler.ddim])
|
||||
def test_sdxl_lcm_lora_and_freeu(device, strategy, sampler):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name="diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
cfg = get_config(
|
||||
strategy=strategy,
|
||||
prompt="face of a fox, sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_strength=1.0,
|
||||
sd_guidance_scale=2.0,
|
||||
sd_lcm_lora=True,
|
||||
)
|
||||
cfg.sd_sampler = sampler
|
||||
|
||||
name = f"device_{device}_{sampler}"
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sdxl_{name}_lcm_lora.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=2,
|
||||
fy=2,
|
||||
)
|
||||
|
||||
cfg = get_config(
|
||||
strategy=strategy,
|
||||
prompt="face of a fox, sitting on a bench",
|
||||
sd_steps=sd_steps,
|
||||
sd_guidance_scale=7.5,
|
||||
sd_freeu=True,
|
||||
sd_freeu_config=FREEUConfig(),
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sdxl_{name}_freeu_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=2,
|
||||
fy=2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["cuda", "mps"])
|
||||
@pytest.mark.parametrize(
|
||||
"rect",
|
||||
[
|
||||
[-128, -128, 1024, 1024],
|
||||
],
|
||||
)
|
||||
def test_sdxl_outpainting(device, rect):
|
||||
sd_steps = check_device(device)
|
||||
|
||||
model = ModelManager(
|
||||
name="diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
||||
device=torch.device(device),
|
||||
disable_nsfw=True,
|
||||
sd_cpu_textencoder=False,
|
||||
)
|
||||
|
||||
cfg = get_config(
|
||||
strategy=HDStrategy.ORIGINAL,
|
||||
prompt="a dog sitting on a bench in the park",
|
||||
sd_steps=sd_steps,
|
||||
use_extender=True,
|
||||
extender_x=rect[0],
|
||||
extender_y=rect[1],
|
||||
extender_width=rect[2],
|
||||
extender_height=rect[3],
|
||||
sd_strength=1.0,
|
||||
sd_guidance_scale=8.0,
|
||||
sd_sampler=SDSampler.ddim,
|
||||
)
|
||||
|
||||
assert_equal(
|
||||
model,
|
||||
cfg,
|
||||
f"sdxl_outpainting_dog_ddim_{'_'.join(map(str, rect))}_device_{device}.png",
|
||||
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
|
||||
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
|
||||
fx=1.5,
|
||||
fy=1.5,
|
||||
)
|
||||
77
iopaint/tests/utils.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from pathlib import Path
|
||||
import cv2
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from iopaint.helper import encode_pil_to_base64
|
||||
from iopaint.schema import LDMSampler, HDStrategy, InpaintRequest, SDSampler
|
||||
from PIL import Image
|
||||
|
||||
current_dir = Path(__file__).parent.absolute().resolve()
|
||||
save_dir = current_dir / "result"
|
||||
save_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
|
||||
def check_device(device: str) -> int:
|
||||
if device == "cuda" and not torch.cuda.is_available():
|
||||
pytest.skip("CUDA is not available, skip test on cuda")
|
||||
if device == "mps" and not torch.backends.mps.is_available():
|
||||
pytest.skip("mps is not available, skip test on mps")
|
||||
steps = 1 if device == "cpu" else 20
|
||||
return steps
|
||||
|
||||
|
||||
def assert_equal(
|
||||
model,
|
||||
config: InpaintRequest,
|
||||
gt_name,
|
||||
fx: float = 1,
|
||||
fy: float = 1,
|
||||
img_p=current_dir / "image.png",
|
||||
mask_p=current_dir / "mask.png",
|
||||
):
|
||||
img, mask = get_data(fx=fx, fy=fy, img_p=img_p, mask_p=mask_p)
|
||||
print(f"Input image shape: {img.shape}")
|
||||
res = model(img, mask, config)
|
||||
ok = cv2.imwrite(
|
||||
str(save_dir / gt_name),
|
||||
res,
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), 100, int(cv2.IMWRITE_PNG_COMPRESSION), 0],
|
||||
)
|
||||
assert ok, save_dir / gt_name
|
||||
|
||||
"""
|
||||
Note that JPEG is lossy compression, so even if it is the highest quality 100,
|
||||
when the saved images is reloaded, a difference occurs with the original pixel value.
|
||||
If you want to save the original images as it is, save it as PNG or BMP.
|
||||
"""
|
||||
# gt = cv2.imread(str(current_dir / gt_name), cv2.IMREAD_UNCHANGED)
|
||||
# assert np.array_equal(res, gt)
|
||||
|
||||
|
||||
def get_data(
|
||||
fx: float = 1,
|
||||
fy: float = 1.0,
|
||||
img_p=current_dir / "image.png",
|
||||
mask_p=current_dir / "mask.png",
|
||||
):
|
||||
img = cv2.imread(str(img_p))
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
|
||||
mask = cv2.imread(str(mask_p), cv2.IMREAD_GRAYSCALE)
|
||||
img = cv2.resize(img, None, fx=fx, fy=fy, interpolation=cv2.INTER_AREA)
|
||||
mask = cv2.resize(mask, None, fx=fx, fy=fy, interpolation=cv2.INTER_NEAREST)
|
||||
return img, mask
|
||||
|
||||
|
||||
def get_config(**kwargs):
|
||||
data = dict(
|
||||
sd_sampler=kwargs.get("sd_sampler", SDSampler.uni_pc),
|
||||
ldm_steps=1,
|
||||
ldm_sampler=LDMSampler.plms,
|
||||
hd_strategy=kwargs.get("strategy", HDStrategy.ORIGINAL),
|
||||
hd_strategy_crop_margin=32,
|
||||
hd_strategy_crop_trigger_size=200,
|
||||
hd_strategy_resize_limit=200,
|
||||
)
|
||||
data.update(**kwargs)
|
||||
return InpaintRequest(image="", mask="", **data)
|
||||
238
iopaint/web_config.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import gradio as gr
|
||||
from loguru import logger
|
||||
|
||||
from iopaint.const import *
|
||||
|
||||
_config_file = None
|
||||
|
||||
|
||||
def save_config(
|
||||
host,
|
||||
port,
|
||||
model,
|
||||
sd_local_model_path,
|
||||
enable_controlnet,
|
||||
controlnet_method,
|
||||
device,
|
||||
gui,
|
||||
no_gui_auto_close,
|
||||
no_half,
|
||||
cpu_offload,
|
||||
disable_nsfw,
|
||||
sd_cpu_textencoder,
|
||||
local_files_only,
|
||||
model_dir,
|
||||
input,
|
||||
output_dir,
|
||||
quality,
|
||||
enable_interactive_seg,
|
||||
interactive_seg_model,
|
||||
interactive_seg_device,
|
||||
enable_remove_bg,
|
||||
enable_anime_seg,
|
||||
enable_realesrgan,
|
||||
realesrgan_device,
|
||||
realesrgan_model,
|
||||
enable_gfpgan,
|
||||
gfpgan_device,
|
||||
enable_restoreformer,
|
||||
restoreformer_device,
|
||||
enable_gif,
|
||||
):
|
||||
config = InpaintRequest(**locals())
|
||||
print(config)
|
||||
if config.input and not os.path.exists(config.input):
|
||||
return "[Error] Input file or directory does not exist"
|
||||
|
||||
current_time = datetime.now().strftime("%H:%M:%S")
|
||||
msg = f"[{current_time}] Successful save config to: {os.path.abspath(_config_file)}"
|
||||
logger.info(msg)
|
||||
try:
|
||||
with open(_config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config.dict(), f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return f"Save failed: {str(e)}"
|
||||
return msg
|
||||
|
||||
|
||||
def close_server(*args):
|
||||
# TODO: make close both browser and server works
|
||||
import os, signal
|
||||
|
||||
pid = os.getpid()
|
||||
os.kill(pid, signal.SIGUSR1)
|
||||
|
||||
|
||||
def main(config_file: str):
|
||||
global _config_file
|
||||
_config_file = config_file
|
||||
|
||||
init_config = load_config(config_file)
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
save_btn = gr.Button(value="Save configurations")
|
||||
message = gr.HTML()
|
||||
|
||||
with gr.Tabs():
|
||||
with gr.Tab("Common"):
|
||||
with gr.Row():
|
||||
host = gr.Textbox(init_config.host, label="Host")
|
||||
port = gr.Number(init_config.port, label="Port", precision=0)
|
||||
|
||||
model = gr.Radio(
|
||||
AVAILABLE_MODELS, label="Model", value=init_config.model
|
||||
)
|
||||
device = gr.Radio(
|
||||
AVAILABLE_DEVICES, label="Device", value=init_config.device
|
||||
)
|
||||
quality = gr.Slider(
|
||||
value=95,
|
||||
label=f"Image Quality ({QUALITY_HELP})",
|
||||
minimum=75,
|
||||
maximum=100,
|
||||
step=1,
|
||||
)
|
||||
|
||||
with gr.Column():
|
||||
gui = gr.Checkbox(init_config.gui, label=f"{GUI_HELP}")
|
||||
|
||||
with gr.Column():
|
||||
model_dir = gr.Textbox(
|
||||
init_config.model_dir, label=f"{MODEL_DIR_HELP}"
|
||||
)
|
||||
input = gr.Textbox(
|
||||
init_config.input,
|
||||
label=f"Input file or directory. {INPUT_HELP}",
|
||||
)
|
||||
output_dir = gr.Textbox(
|
||||
init_config.output_dir,
|
||||
label=f"Output directory. {OUTPUT_DIR_HELP}",
|
||||
)
|
||||
|
||||
with gr.Tab("Plugins"):
|
||||
enable_interactive_seg = gr.Checkbox(
|
||||
init_config.enable_interactive_seg, label=INTERACTIVE_SEG_HELP
|
||||
)
|
||||
interactive_seg_model = gr.Radio(
|
||||
AVAILABLE_INTERACTIVE_SEG_MODELS,
|
||||
label=f"Segment Anything models. {INTERACTIVE_SEG_MODEL_HELP}",
|
||||
value=init_config.interactive_seg_model,
|
||||
)
|
||||
interactive_seg_device = gr.Radio(
|
||||
AVAILABLE_INTERACTIVE_SEG_DEVICES,
|
||||
label="Segment Anything Device",
|
||||
value=init_config.interactive_seg_device,
|
||||
)
|
||||
with gr.Row():
|
||||
enable_remove_bg = gr.Checkbox(
|
||||
init_config.enable_remove_bg, label=REMOVE_BG_HELP
|
||||
)
|
||||
with gr.Row():
|
||||
enable_anime_seg = gr.Checkbox(
|
||||
init_config.enable_anime_seg, label=ANIMESEG_HELP
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
enable_realesrgan = gr.Checkbox(
|
||||
init_config.enable_realesrgan, label=REALESRGAN_HELP
|
||||
)
|
||||
realesrgan_device = gr.Radio(
|
||||
REALESRGAN_AVAILABLE_DEVICES,
|
||||
label="RealESRGAN Device",
|
||||
value=init_config.realesrgan_device,
|
||||
)
|
||||
realesrgan_model = gr.Radio(
|
||||
RealESRGANModelNameList,
|
||||
label="RealESRGAN model",
|
||||
value=init_config.realesrgan_model,
|
||||
)
|
||||
with gr.Row():
|
||||
enable_gfpgan = gr.Checkbox(
|
||||
init_config.enable_gfpgan, label=GFPGAN_HELP
|
||||
)
|
||||
gfpgan_device = gr.Radio(
|
||||
GFPGAN_AVAILABLE_DEVICES,
|
||||
label="GFPGAN Device",
|
||||
value=init_config.gfpgan_device,
|
||||
)
|
||||
with gr.Row():
|
||||
enable_restoreformer = gr.Checkbox(
|
||||
init_config.enable_restoreformer, label=RESTOREFORMER_HELP
|
||||
)
|
||||
restoreformer_device = gr.Radio(
|
||||
RESTOREFORMER_AVAILABLE_DEVICES,
|
||||
label="RestoreFormer Device",
|
||||
value=init_config.restoreformer_device,
|
||||
)
|
||||
enable_gif = gr.Checkbox(init_config.enable_gif, label=GIF_HELP)
|
||||
|
||||
with gr.Tab("Diffusion Model"):
|
||||
sd_local_model_path = gr.Textbox(
|
||||
init_config.sd_local_model_path, label=f"{SD_LOCAL_MODEL_HELP}"
|
||||
)
|
||||
enable_controlnet = gr.Checkbox(
|
||||
init_config.enable_controlnet, label=f"{SD_CONTROLNET_HELP}"
|
||||
)
|
||||
controlnet_method = gr.Radio(
|
||||
SD_CONTROLNET_CHOICES,
|
||||
label="ControlNet method",
|
||||
value=init_config.controlnet_method,
|
||||
)
|
||||
no_half = gr.Checkbox(init_config.no_half, label=f"{NO_HALF_HELP}")
|
||||
cpu_offload = gr.Checkbox(
|
||||
init_config.cpu_offload, label=f"{CPU_OFFLOAD_HELP}"
|
||||
)
|
||||
sd_cpu_textencoder = gr.Checkbox(
|
||||
init_config.sd_cpu_textencoder, label=f"{CPU_TEXTENCODER_HELP}"
|
||||
)
|
||||
disable_nsfw = gr.Checkbox(
|
||||
init_config.disable_nsfw, label=f"{DISABLE_NSFW_HELP}"
|
||||
)
|
||||
local_files_only = gr.Checkbox(
|
||||
init_config.local_files_only, label=f"{LOCAL_FILES_ONLY_HELP}"
|
||||
)
|
||||
|
||||
save_btn.click(
|
||||
save_config,
|
||||
[
|
||||
host,
|
||||
port,
|
||||
model,
|
||||
sd_local_model_path,
|
||||
enable_controlnet,
|
||||
controlnet_method,
|
||||
device,
|
||||
gui,
|
||||
no_gui_auto_close,
|
||||
no_half,
|
||||
cpu_offload,
|
||||
disable_nsfw,
|
||||
sd_cpu_textencoder,
|
||||
local_files_only,
|
||||
model_dir,
|
||||
input,
|
||||
output_dir,
|
||||
quality,
|
||||
enable_interactive_seg,
|
||||
interactive_seg_model,
|
||||
interactive_seg_device,
|
||||
enable_remove_bg,
|
||||
enable_anime_seg,
|
||||
enable_realesrgan,
|
||||
realesrgan_device,
|
||||
realesrgan_model,
|
||||
enable_gfpgan,
|
||||
gfpgan_device,
|
||||
enable_restoreformer,
|
||||
restoreformer_device,
|
||||
enable_gif,
|
||||
],
|
||||
message,
|
||||
)
|
||||
demo.launch(inbrowser=True, show_api=False)
|
||||