first commit

This commit is contained in:
Tran Xen
2023-07-26 17:42:02 +02:00
commit 056f7a72a2
83 changed files with 6080 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
import importlib
from scripts.faceswaplab_api import faceswaplab_api
from scripts.faceswaplab_settings import faceswaplab_settings
from scripts.faceswaplab_ui import faceswaplab_tab, faceswaplab_unit_ui
from scripts.faceswaplab_utils.models_utils import get_current_model, get_face_checkpoints
from scripts import (faceswaplab_globals)
from scripts.faceswaplab_swapping import swapper
from scripts.faceswaplab_utils import faceswaplab_logging, imgutils
from scripts.faceswaplab_utils import models_utils
from scripts.faceswaplab_postprocessing import upscaling
import numpy as np
#Reload all the modules when using "apply and restart"
#This is mainly done for development purposes
importlib.reload(swapper)
importlib.reload(faceswaplab_logging)
importlib.reload(faceswaplab_globals)
importlib.reload(imgutils)
importlib.reload(upscaling)
importlib.reload(faceswaplab_settings)
importlib.reload(models_utils)
importlib.reload(faceswaplab_unit_ui)
importlib.reload(faceswaplab_api)
import os
from dataclasses import fields
from pprint import pformat
from typing import List, Optional, Tuple
import dill as pickle
import gradio as gr
import modules.scripts as scripts
from modules import script_callbacks, scripts
from insightface.app.common import Face
from modules import scripts, shared
from modules.images import save_image, image_grid
from modules.processing import (Processed, StableDiffusionProcessing,
StableDiffusionProcessingImg2Img)
from modules.shared import opts
from PIL import Image
from scripts.faceswaplab_utils.imgutils import (pil_to_cv2,check_against_nsfw)
from scripts.faceswaplab_utils.faceswaplab_logging import logger, save_img_debug
from scripts.faceswaplab_globals import VERSION_FLAG
from scripts.faceswaplab_postprocessing.postprocessing_options import PostProcessingOptions
from scripts.faceswaplab_postprocessing.postprocessing import enhance_image
from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
EXTENSION_PATH=os.path.join("extensions","sd-webui-faceswaplab")
# Register the tab, done here to prevent it from being added twice
script_callbacks.on_ui_tabs(faceswaplab_tab.on_ui_tabs)
try:
import modules.script_callbacks as script_callbacks
script_callbacks.on_app_started(faceswaplab_api.faceswaplab_api)
except:
pass
class FaceSwapScript(scripts.Script):
def __init__(self) -> None:
logger.info(f"FaceSwapLab {VERSION_FLAG}")
super().__init__()
@property
def units_count(self) :
return opts.data.get("faceswaplab_units_count", 3)
@property
def upscaled_swapper_in_generated(self) :
return opts.data.get("faceswaplab_upscaled_swapper", False)
@property
def upscaled_swapper_in_source(self) :
return opts.data.get("faceswaplab_upscaled_swapper_in_source", False)
@property
def enabled(self) -> bool :
"""Return True if any unit is enabled and the state is not interupted"""
return any([u.enable for u in self.units]) and not shared.state.interrupted
@property
def keep_original_images(self) :
return opts.data.get("faceswaplab_keep_original", False)
@property
def swap_in_generated_units(self) :
return [u for u in self.units if u.swap_in_generated and u.enable]
@property
def swap_in_source_units(self) :
return [u for u in self.units if u.swap_in_source and u.enable]
def title(self):
return f"faceswaplab"
def show(self, is_img2img):
return scripts.AlwaysVisible
def ui(self, is_img2img):
with gr.Accordion(f"FaceSwapLab {VERSION_FLAG}", open=False):
components = []
for i in range(1, self.units_count + 1):
components += faceswaplab_unit_ui.faceswap_unit_ui(is_img2img, i)
upscaler = faceswaplab_tab.upscaler_ui()
# If the order is modified, the before_process should be changed accordingly.
return components + upscaler
# def make_script_first(self,p: StableDiffusionProcessing) :
# FIXME : not really useful, will only impact postprocessing (kept for further testing)
# runner : scripts.ScriptRunner = p.scripts
# alwayson = runner.alwayson_scripts
# alwayson.pop(alwayson.index(self))
# alwayson.insert(0, self)
# print("Running in ", alwayson.index(self), "position")
# logger.info("Running scripts : %s", pformat(runner.alwayson_scripts))
def read_config(self, p : StableDiffusionProcessing, *components) :
# The order of processing for the components is important
# The method first process faceswap units then postprocessing units
# self.make_first_script(p)
self.units: List[FaceSwapUnitSettings] = []
#Parse and convert units flat components into FaceSwapUnitSettings
for i in range(0, self.units_count):
self.units += [FaceSwapUnitSettings.get_unit_configuration(i, components)]
for i, u in enumerate(self.units):
logger.debug("%s, %s", pformat(i), pformat(u))
#Parse the postprocessing options
#We must first find where to start from (after face swapping units)
len_conf: int = len(fields(FaceSwapUnitSettings))
shift: int = self.units_count * len_conf
self.postprocess_options = PostProcessingOptions(
*components[shift : shift + len(fields(PostProcessingOptions))]
)
logger.debug("%s", pformat(self.postprocess_options))
if self.enabled :
p.do_not_save_samples = not self.keep_original_images
def process(self, p: StableDiffusionProcessing, *components):
self.read_config(p, *components)
#If is instance of img2img, we check if face swapping in source is required.
if isinstance(p, StableDiffusionProcessingImg2Img):
if self.enabled and len(self.swap_in_source_units) > 0:
init_images : List[Tuple[Optional[Image.Image], Optional[str]]] = [(img,None) for img in p.init_images]
new_inits = swapper.process_images_units(get_current_model(), self.swap_in_source_units,images=init_images, upscaled_swapper=self.upscaled_swapper_in_source,force_blend=True)
logger.info(f"processed init images: {len(init_images)}")
if new_inits is not None :
p.init_images = [img[0] for img in new_inits]
def postprocess(self, p : StableDiffusionProcessing, processed: Processed, *args):
if self.enabled :
# Get the original images without the grid
orig_images : List[Image.Image] = processed.images[processed.index_of_first_image:]
orig_infotexts : List[str] = processed.infotexts[processed.index_of_first_image:]
keep_original = self.keep_original_images
# These are were images and infos of swapped images will be stored
images = []
infotexts = []
if (len(self.swap_in_generated_units))>0 :
for i,(img,info) in enumerate(zip(orig_images, orig_infotexts)):
batch_index = i%p.batch_size
swapped_images = swapper.process_images_units(get_current_model(), self.swap_in_generated_units, images=[(img,info)], upscaled_swapper=self.upscaled_swapper_in_generated)
if swapped_images is None :
continue
logger.info(f"{len(swapped_images)} images swapped")
for swp_img, new_info in swapped_images :
img = swp_img # Will only swap the last image in the batch in next units (FIXME : hard to fix properly but not really critical)
if swp_img is not None :
save_img_debug(swp_img,"Before apply mask")
swp_img = imgutils.apply_mask(swp_img, p, batch_index)
save_img_debug(swp_img,"After apply mask")
try :
if self.postprocess_options is not None:
swp_img = enhance_image(swp_img, self.postprocess_options)
except Exception as e:
logger.error("Failed to upscale : %s", e)
logger.info("Add swp image to processed")
images.append(swp_img)
infotexts.append(new_info)
if p.outpath_samples and opts.samples_save :
save_image(swp_img, p.outpath_samples, "", p.all_seeds[batch_index], p.all_prompts[batch_index], opts.samples_format,info=new_info, p=p, suffix="-swapped")
else :
logger.error("swp image is None")
else :
keep_original=True
# Generate grid :
if opts.return_grid and len(images) > 1:
# FIXME :Use sd method, not that if blended is not active, the result will be a bit messy.
grid = imgutils.create_square_image(images)
text = processed.infotexts[0]
infotexts.insert(0, text)
if opts.enable_pnginfo:
grid.info["parameters"] = text
images.insert(0, grid)
if keep_original:
# If we want to keep original images, we add all existing (including grid this time)
images += processed.images
infotexts += processed.infotexts
processed.images = images
processed.infotexts = infotexts
@@ -0,0 +1,70 @@
from PIL import Image
import numpy as np
from fastapi import FastAPI, Body
from fastapi.exceptions import HTTPException
from modules.api.models import *
from modules.api import api
from scripts.faceswaplab_api.faceswaplab_api_types import FaceSwapUnit, FaceSwapRequest, FaceSwapResponse
from scripts.faceswaplab_globals import VERSION_FLAG
import gradio as gr
from typing import List, Optional
from scripts.faceswaplab_swapping import swapper
from scripts.faceswaplab_utils.faceswaplab_logging import save_img_debug
from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
from scripts.faceswaplab_utils.imgutils import (pil_to_cv2,check_against_nsfw, base64_to_pil)
from scripts.faceswaplab_utils.models_utils import get_current_model
from modules.shared import opts
def encode_to_base64(image):
if type(image) is str:
return image
elif type(image) is Image.Image:
return api.encode_pil_to_base64(image)
elif type(image) is np.ndarray:
return encode_np_to_base64(image)
else:
return ""
def encode_np_to_base64(image):
pil = Image.fromarray(image)
return api.encode_pil_to_base64(pil)
def faceswaplab_api(_: gr.Blocks, app: FastAPI):
@app.get("/faceswaplab/version", tags=["faceswaplab"], description="Get faceswaplab version")
async def version():
return {"version": VERSION_FLAG}
# use post as we consider the method non idempotent (which is debatable)
@app.post("/faceswaplab/swap_face", tags=["faceswaplab"], description="Swap a face in an image using units")
async def swap_face(request : FaceSwapRequest) -> FaceSwapResponse:
units : List[FaceSwapUnitSettings]= []
src_image : Optional[Image.Image] = base64_to_pil(request.image)
response = FaceSwapResponse(images = [], infos=[])
if src_image is not None :
for u in request.units:
units.append(
FaceSwapUnitSettings(source_img=base64_to_pil(u.source_img),
source_face = u.source_face,
_batch_files = u.get_batch_images(),
blend_faces= u.blend_faces,
enable = True,
same_gender = u.same_gender,
check_similarity=u.check_similarity,
_compute_similarity=u.compute_similarity,
min_ref_sim= u.min_ref_sim,
min_sim= u.min_sim,
_faces_index = ",".join([str(i) for i in (u.faces_index)]),
swap_in_generated=True,
swap_in_source=False
)
)
swapped_images = swapper.process_images_units(get_current_model(), images=[(src_image,None)], units=units, upscaled_swapper=opts.data.get("faceswaplab_upscaled_swapper", False))
for img, info in swapped_images:
response.images.append(encode_to_base64(img))
response.infos.append(info)
return response
@@ -0,0 +1,73 @@
from scripts.faceswaplab_swapping import swapper
import numpy as np
from typing import List, Tuple
import dill as pickle
import gradio as gr
from insightface.app.common import Face
from PIL import Image
from scripts.faceswaplab_utils.imgutils import (pil_to_cv2,check_against_nsfw, base64_to_pil)
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from pydantic import BaseModel, Field
from scripts.faceswaplab_postprocessing.postprocessing_options import InpaintingWhen
class FaceSwapUnit(BaseModel) :
# The image given in reference
source_img: str = Field(description='base64 reference image', examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."], default=None)
# The checkpoint file
source_face : str = Field(description='face checkpoint (from models/faceswaplab/faces)',examples=["my_face.pkl"], default=None)
# base64 batch source images
batch_images: Tuple[str] = Field(description='list of base64 batch source images',examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD....", "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."], default=None)
# Will blend faces if True
blend_faces: bool = Field(description='Will blend faces if True', default=True)
# Use same gender filtering
same_gender: bool = Field(description='Use same gender filtering', default=True)
# If True, discard images with low similarity
check_similarity : bool = Field(description='If True, discard images with low similarity', default=False)
# if True will compute similarity and add it to the image info
compute_similarity : bool = Field(description='If True will compute similarity and add it to the image info', default=False)
# Minimum similarity against the used face (reference, batch or checkpoint)
min_sim: float = Field(description='Minimum similarity against the used face (reference, batch or checkpoint)', default=0.0)
# Minimum similarity against the reference (reference or checkpoint if checkpoint is given)
min_ref_sim: float = Field(description='Minimum similarity against the reference (reference or checkpoint if checkpoint is given)', default=0.0)
# The face index to use for swapping
faces_index: Tuple[int] = Field(description='The face index to use for swapping, list of face numbers starting from 0', default=(0,))
def get_batch_images(self) -> List[Image.Image] :
images = []
if self.batch_images :
for img in self.batch_images :
images.append(base64_to_pil(img))
return images
class PostProcessingOptions (BaseModel):
face_restorer_name: str = Field(description='face restorer name', default=None)
restorer_visibility: float = Field(description='face restorer visibility', default=1, le=1, ge=0)
codeformer_weight: float = Field(description='face restorer codeformer weight', default=1, le=1, ge=0)
upscaler_name: str = Field(description='upscaler name', default=None)
scale: float = Field(description='upscaling scale', default=1, le=10, ge=0)
upscale_visibility: float = Field(description='upscaler visibility', default=1, le=1, ge=0)
inpainting_denoising_strengh : float = Field(description='Inpainting denoising strenght', default=0, lt=1, ge=0)
inpainting_prompt : str = Field(description='Inpainting denoising strenght',examples=["Portrait of a [gender]"], default="Portrait of a [gender]")
inpainting_negative_prompt : str = Field(description='Inpainting denoising strenght',examples=["Deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation"], default="")
inpainting_steps : int = Field(description='Inpainting steps',examples=["Portrait of a [gender]"], ge=1, le=150, default=20)
inpainting_sampler : str = Field(description='Inpainting sampler',examples=["Euler"], default="Euler")
inpainting_when : InpaintingWhen = Field(description='When inpainting happens', examples=[e.value for e in InpaintingWhen.__members__.values()], default=InpaintingWhen.NEVER)
class FaceSwapRequest(BaseModel) :
image : str = Field(description='base64 reference image', examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."], default=None)
units : List[FaceSwapUnit]
postprocessing : PostProcessingOptions
class FaceSwapResponse(BaseModel) :
images : List[str] = Field(description='base64 swapped image',default=None)
infos : List[str]
+11
View File
@@ -0,0 +1,11 @@
from scripts.faceswaplab_utils.faceswaplab_logging import logger
import os
MODELS_DIR = os.path.abspath(os.path.join("models","faceswaplab"))
ANALYZER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "analysers"))
FACE_PARSER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "parser"))
VERSION_FLAG = "v1.1.0"
EXTENSION_PATH=os.path.join("extensions","sd-webui-faceswaplab")
NSFW_SCORE = 0.7
@@ -0,0 +1,76 @@
from modules.face_restoration import FaceRestoration
from modules.upscaler import UpscalerData
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from PIL import Image
import numpy as np
from modules import shared
from scripts.faceswaplab_utils import imgutils
from modules import shared, processing, codeformer_model
from modules.processing import (StableDiffusionProcessingImg2Img)
from scripts.faceswaplab_postprocessing.postprocessing_options import PostProcessingOptions, InpaintingWhen
from modules import sd_models
from scripts.faceswaplab_swapping import swapper
def img2img_diffusion(img : Image.Image, pp: PostProcessingOptions) -> Image.Image :
if pp.inpainting_denoising_strengh == 0 :
return img
try :
logger.info(
f"""Inpainting face
Sampler : {pp.inpainting_sampler}
inpainting_denoising_strength : {pp.inpainting_denoising_strengh}
inpainting_steps : {pp.inpainting_steps}
"""
)
if not isinstance(pp.inpainting_sampler, str) :
pass
logger.info("send faces to image to image")
img = img.copy()
faces = swapper.get_faces(imgutils.pil_to_cv2(img))
if faces:
for face in faces:
bbox =face.bbox.astype(int)
mask = imgutils.create_mask(img, bbox)
prompt = pp.inpainting_prompt.replace("[gender]", "man" if face["gender"] == 1 else "woman")
negative_prompt = pp.inpainting_negative_prompt.replace("[gender]", "man" if face["gender"] == 1 else "woman")
logger.info("Denoising prompt : %s", prompt)
logger.info("Denoising strenght : %s", pp.inpainting_denoising_strengh)
i2i_kwargs = {"sampler_name" :pp.inpainting_sampler,
"do_not_save_samples":True,
"steps" :pp.inpainting_steps,
"width" : img.width,
"inpainting_fill":1,
"inpaint_full_res":True,
"height" : img.height,
"mask": mask,
"prompt" : prompt,
"negative_prompt" :negative_prompt,
"denoising_strength" :pp.inpainting_denoising_strengh}
current_model_checkpoint = shared.opts.sd_model_checkpoint
if pp.inpainting_model and pp.inpainting_model != "Current" :
# Change checkpoint
shared.opts.sd_model_checkpoint = pp.inpainting_model
sd_models.select_checkpoint
sd_models.load_model()
i2i_p = StableDiffusionProcessingImg2Img([img], **i2i_kwargs)
i2i_processed = processing.process_images(i2i_p)
if pp.inpainting_model and pp.inpainting_model != "Current" :
# Restore checkpoint
shared.opts.sd_model_checkpoint = current_model_checkpoint
sd_models.select_checkpoint
sd_models.load_model()
images = i2i_processed.images
if len(images) > 0 :
img = images[0]
return img
except Exception as e :
logger.error("Failed to apply img2img to face : %s", e)
import traceback
traceback.print_exc()
raise e
@@ -0,0 +1,27 @@
from modules.face_restoration import FaceRestoration
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from PIL import Image
from scripts.faceswaplab_postprocessing.postprocessing_options import PostProcessingOptions, InpaintingWhen
from scripts.faceswaplab_postprocessing.i2i_pp import img2img_diffusion
from scripts.faceswaplab_postprocessing.upscaling import upscale_img, restore_face
def enhance_image(image: Image.Image, pp_options: PostProcessingOptions) -> Image.Image:
result_image = image
try :
if pp_options.inpainting_when == InpaintingWhen.BEFORE_UPSCALING.value :
result_image = img2img_diffusion(image, pp_options)
result_image = upscale_img(result_image, pp_options)
if pp_options.inpainting_when == InpaintingWhen.BEFORE_RESTORE_FACE.value :
result_image = img2img_diffusion(image,pp_options)
result_image = restore_face(result_image, pp_options)
if pp_options.inpainting_when == InpaintingWhen.AFTER_ALL.value :
result_image = img2img_diffusion(image,pp_options)
except Exception as e:
logger.error("Failed to upscale %s", e)
return result_image
@@ -0,0 +1,43 @@
from modules.face_restoration import FaceRestoration
from modules.upscaler import UpscalerData
from dataclasses import dataclass
from modules import shared
from enum import Enum
class InpaintingWhen(Enum):
NEVER = "Never"
BEFORE_UPSCALING = "Before Upscaling/all"
BEFORE_RESTORE_FACE = "After Upscaling/Before Restore Face"
AFTER_ALL = "After All"
@dataclass
class PostProcessingOptions:
face_restorer_name: str = ""
restorer_visibility: float = 0.5
codeformer_weight: float = 1
upscaler_name: str = ""
scale: int = 1
upscale_visibility: float = 0.5
inpainting_denoising_strengh : float = 0
inpainting_prompt : str = ""
inpainting_negative_prompt : str = ""
inpainting_steps : int = 20
inpainting_sampler : str = "Euler"
inpainting_when : InpaintingWhen = InpaintingWhen.BEFORE_UPSCALING
inpainting_model : str = "Current"
@property
def upscaler(self) -> UpscalerData:
for upscaler in shared.sd_upscalers:
if upscaler.name == self.upscaler_name:
return upscaler
return None
@property
def face_restorer(self) -> FaceRestoration:
for face_restorer in shared.face_restorers:
if face_restorer.name() == self.face_restorer_name:
return face_restorer
return None
@@ -0,0 +1,42 @@
from scripts.faceswaplab_postprocessing.postprocessing_options import PostProcessingOptions, InpaintingWhen
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from PIL import Image
import numpy as np
from modules import shared, processing, codeformer_model
def upscale_img(image : Image.Image, pp_options :PostProcessingOptions) -> Image.Image :
if pp_options.upscaler is not None and pp_options.upscaler.name != "None":
original_image = image.copy()
logger.info(
"Upscale with %s scale = %s",
pp_options.upscaler.name,
pp_options.scale,
)
result_image = pp_options.upscaler.scaler.upscale(
image, pp_options.scale, pp_options.upscaler.data_path
)
if pp_options.scale == 1:
result_image = Image.blend(
original_image, result_image, pp_options.upscale_visibility
)
return result_image
return image
def restore_face(image : Image.Image, pp_options : PostProcessingOptions) -> Image.Image :
if pp_options.face_restorer is not None:
original_image = image.copy()
logger.info("Restore face with %s", pp_options.face_restorer.name())
numpy_image = np.array(image)
if pp_options.face_restorer_name == "CodeFormer" :
numpy_image = codeformer_model.codeformer.restore(numpy_image, w=pp_options.codeformer_weight)
else :
numpy_image = pp_options.face_restorer.restore(numpy_image)
restored_image = Image.fromarray(numpy_image)
result_image = Image.blend(
original_image, restored_image, pp_options.restorer_visibility
)
return result_image
return image
@@ -0,0 +1,53 @@
from scripts.faceswaplab_utils.models_utils import get_models
from modules import script_callbacks, shared
import gradio as gr
def on_ui_settings():
section = ('faceswaplab', "FaceSwapLab")
models = get_models()
shared.opts.add_option("faceswaplab_model", shared.OptionInfo(
models[0] if len(models) > 0 else "None", "FaceSwapLab FaceSwap Model", gr.Dropdown, {"interactive": True, "choices" : models}, section=section))
shared.opts.add_option("faceswaplab_keep_original", shared.OptionInfo(
False, "keep original image before swapping", gr.Checkbox, {"interactive": True}, section=section))
shared.opts.add_option("faceswaplab_units_count", shared.OptionInfo(
3, "Max faces units (requires restart)", gr.Slider, {"minimum": 1, "maximum": 10, "step": 1}, section=section))
shared.opts.add_option("faceswaplab_detection_threshold", shared.OptionInfo(
0.5, "Detection threshold ", gr.Slider, {"minimum": 0.1, "maximum": 0.99, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_pp_default_face_restorer", shared.OptionInfo(
None, "UI Default post processing face restorer (requires restart)", gr.Dropdown, {"interactive": True, "choices" : ["None"] + [x.name() for x in shared.face_restorers]}, section=section))
shared.opts.add_option("faceswaplab_pp_default_face_restorer_visibility", shared.OptionInfo(
1, "UI Default post processing face restorer visibility (requires restart)", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_pp_default_face_restorer_weight", shared.OptionInfo(
1, "UI Default post processing face restorer weight (requires restart)", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_pp_default_upscaler", shared.OptionInfo(
None, "UI Default post processing upscaler (requires restart)", gr.Dropdown, {"interactive": True, "choices" : [upscaler.name for upscaler in shared.sd_upscalers]}, section=section))
shared.opts.add_option("faceswaplab_pp_default_upscaler_visibility", shared.OptionInfo(
1, "UI Default post processing upscaler visibility(requires restart)", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper", shared.OptionInfo(
False, "Upscaled swapper. Applied only to the swapped faces. Apply transformations before merging with the original image.", gr.Checkbox, {"interactive": True}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_upscaler", shared.OptionInfo(
None, "Upscaled swapper upscaler (Recommanded : LDSR but slow)", gr.Dropdown, {"interactive": True, "choices" : [upscaler.name for upscaler in shared.sd_upscalers]}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_sharpen", shared.OptionInfo(
False, "Upscaled swapper sharpen", gr.Checkbox, {"interactive": True}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_fixcolor", shared.OptionInfo(
False, "Upscaled swapper color correction", gr.Checkbox, {"interactive": True}, section=section))
shared.opts.add_option("faceswaplab_upscaled_improved_mask", shared.OptionInfo(
True, "Use improved segmented mask (use pastenet to mask only the face)", gr.Checkbox, {"interactive": True}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_face_restorer", shared.OptionInfo(
None, "Upscaled swapper face restorer", gr.Dropdown, {"interactive": True, "choices" : ["None"] + [x.name() for x in shared.face_restorers]}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_face_restorer_visibility", shared.OptionInfo(
1, "Upscaled swapper face restorer visibility", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_face_restorer_weight", shared.OptionInfo(
1, "Upscaled swapper face restorer weight (codeformer)", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.001}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_fthresh", shared.OptionInfo(
10, "Upscaled swapper fthresh (diff sensitivity) 10 = default behaviour. Low impact.", gr.Slider, {"minimum": 5, "maximum": 250, "step": 1}, section=section))
shared.opts.add_option("faceswaplab_upscaled_swapper_erosion", shared.OptionInfo(
1, "Upscaled swapper mask erosion factor, 1 = default behaviour. The larger it is, the more blur is applied around the face. Too large and the facial change is no longer visible.", gr.Slider, {"minimum": 0, "maximum": 10, "step": 0.001}, section=section))
script_callbacks.on_ui_settings(on_ui_settings)
+85
View File
@@ -0,0 +1,85 @@
import cv2
import numpy as np
import torch
from torchvision.transforms.functional import normalize
from scripts.faceswaplab_swapping.parsing import init_parsing_model
from functools import lru_cache
from typing import Union, List
from torch import device as torch_device
@lru_cache
def get_parsing_model(device: torch_device) -> torch.nn.Module:
"""
Returns an instance of the parsing model.
The returned model is cached for faster subsequent access.
Args:
device: The torch device to use for computations.
Returns:
The parsing model.
"""
return init_parsing_model(device=device)
def convert_image_to_tensor(images: Union[np.ndarray, List[np.ndarray]], convert_bgr_to_rgb: bool = True, use_float32: bool = True) -> Union[torch.Tensor, List[torch.Tensor]]:
"""
Converts an image or a list of images to PyTorch tensor.
Args:
images: An image or a list of images in numpy.ndarray format.
convert_bgr_to_rgb: A boolean flag indicating if the conversion from BGR to RGB should be performed.
use_float32: A boolean flag indicating if the tensor should be converted to float32.
Returns:
PyTorch tensor or a list of PyTorch tensors.
"""
def _convert_single_image_to_tensor(image: np.ndarray, convert_bgr_to_rgb: bool, use_float32: bool) -> torch.Tensor:
if image.shape[2] == 3 and convert_bgr_to_rgb:
if image.dtype == 'float64':
image = image.astype('float32')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image_tensor = torch.from_numpy(image.transpose(2, 0, 1))
if use_float32:
image_tensor = image_tensor.float()
return image_tensor
if isinstance(images, list):
return [_convert_single_image_to_tensor(image, convert_bgr_to_rgb, use_float32) for image in images]
else:
return _convert_single_image_to_tensor(images, convert_bgr_to_rgb, use_float32)
def generate_face_mask(face_image: np.ndarray, device: torch.device) -> np.ndarray:
"""
Generates a face mask given a face image.
Args:
face_image: The face image in numpy.ndarray format.
device: The torch device to use for computations.
Returns:
The face mask as a numpy.ndarray.
"""
# Resize the face image for the model
resized_face_image = cv2.resize(face_image, (512, 512), interpolation=cv2.INTER_LINEAR)
# Preprocess the image
face_input = convert_image_to_tensor((resized_face_image.astype('float32') / 255.0), convert_bgr_to_rgb=True, use_float32=True)
normalize(face_input, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
assert isinstance(face_input,torch.Tensor)
face_input = torch.unsqueeze(face_input, 0).to(device)
# Pass the image through the model
with torch.no_grad():
model_output = get_parsing_model(device)(face_input)[0]
model_output = model_output.argmax(dim=1).squeeze().cpu().numpy()
# Generate the mask from the model output
parse_mask = np.zeros(model_output.shape)
MASK_COLOR_MAP = [0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 255, 0, 0, 0]
for idx, color in enumerate(MASK_COLOR_MAP):
parse_mask[model_output == idx] = color
# Resize the mask to match the original image
face_mask = cv2.resize(parse_mask, (face_image.shape[1], face_image.shape[0]))
return face_mask
@@ -0,0 +1,81 @@
"""
Code from codeformer https://github.com/sczhou/CodeFormer
S-Lab License 1.0
Copyright 2022 S-Lab
Redistribution and use for non-commercial purpose in source and
binary forms, with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
In the event that redistribution and/or use for commercial purpose in
source or binary forms, with or without modification is required,
please contact the contributor(s) of the work.
"""
import torch
import cv2
import os
import torch
from torch.hub import download_url_to_file, get_dir
from .parsenet import ParseNet
from urllib.parse import urlparse
from scripts.faceswaplab_globals import FACE_PARSER_DIR
ROOT_DIR = FACE_PARSER_DIR
def load_file_from_url(url, model_dir=None, progress=True, file_name=None):
"""Ref:https://github.com/1adrianb/face-alignment/blob/master/face_alignment/utils.py
"""
if model_dir is None:
hub_dir = get_dir()
model_dir = os.path.join(hub_dir, 'checkpoints')
os.makedirs(os.path.join(ROOT_DIR, model_dir), exist_ok=True)
parts = urlparse(url)
filename = os.path.basename(parts.path)
if file_name is not None:
filename = file_name
cached_file = os.path.abspath(os.path.join(ROOT_DIR, model_dir, filename))
if not os.path.exists(cached_file):
print(f'Downloading: "{url}" to {cached_file}\n')
download_url_to_file(url, cached_file, hash_prefix=None, progress=progress)
return cached_file
def init_parsing_model(device='cuda'):
model = ParseNet(in_size=512, out_size=512, parsing_ch=19)
model_url = 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth'
model_path = load_file_from_url(url=model_url, model_dir='weights/facelib', progress=True, file_name=None)
load_net = torch.load(model_path, map_location=lambda storage, loc: storage)
model.load_state_dict(load_net, strict=True)
model.eval()
model = model.to(device)
return model
@@ -0,0 +1,680 @@
"""
Code from codeformer https://github.com/sczhou/CodeFormer
S-Lab License 1.0
Copyright 2022 S-Lab
Redistribution and use for non-commercial purpose in source and
binary forms, with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
In the event that redistribution and/or use for commercial purpose in
source or binary forms, with or without modification is required,
please contact the contributor(s) of the work.
Modified from https://github.com/chaofengc/PSFRGAN
PSFR-GAN (c) by Chaofeng Chen
PSFR-GAN is licensed under a
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
You should have received a copy of the license along with this
work. If not, see <http://creativecommons.org/licenses/by-nc-sa/4.0/>.
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
"""
import numpy as np
import torch.nn as nn
from torch.nn import functional as F
class NormLayer(nn.Module):
"""Normalization Layers.
Args:
channels: input channels, for batch norm and instance norm.
input_size: input shape without batch size, for layer norm.
"""
def __init__(self, channels, normalize_shape=None, norm_type='bn'):
super(NormLayer, self).__init__()
norm_type = norm_type.lower()
self.norm_type = norm_type
if norm_type == 'bn':
self.norm = nn.BatchNorm2d(channels, affine=True)
elif norm_type == 'in':
self.norm = nn.InstanceNorm2d(channels, affine=False)
elif norm_type == 'gn':
self.norm = nn.GroupNorm(32, channels, affine=True)
elif norm_type == 'pixel':
self.norm = lambda x: F.normalize(x, p=2, dim=1)
elif norm_type == 'layer':
self.norm = nn.LayerNorm(normalize_shape)
elif norm_type == 'none':
self.norm = lambda x: x * 1.0
else:
assert 1 == 0, f'Norm type {norm_type} not support.'
def forward(self, x, ref=None):
if self.norm_type == 'spade':
return self.norm(x, ref)
else:
return self.norm(x)
class ReluLayer(nn.Module):
"""Relu Layer.
Args:
relu type: type of relu layer, candidates are
- ReLU
- LeakyReLU: default relu slope 0.2
- PRelu
- SELU
- none: direct pass
"""
def __init__(self, channels, relu_type='relu'):
super(ReluLayer, self).__init__()
relu_type = relu_type.lower()
if relu_type == 'relu':
self.func = nn.ReLU(True)
elif relu_type == 'leakyrelu':
self.func = nn.LeakyReLU(0.2, inplace=True)
elif relu_type == 'prelu':
self.func = nn.PReLU(channels)
elif relu_type == 'selu':
self.func = nn.SELU(True)
elif relu_type == 'none':
self.func = lambda x: x * 1.0
else:
assert 1 == 0, f'Relu type {relu_type} not support.'
def forward(self, x):
return self.func(x)
class ConvLayer(nn.Module):
def __init__(self,
in_channels,
out_channels,
kernel_size=3,
scale='none',
norm_type='none',
relu_type='none',
use_pad=True,
bias=True):
super(ConvLayer, self).__init__()
self.use_pad = use_pad
self.norm_type = norm_type
if norm_type in ['bn']:
bias = False
stride = 2 if scale == 'down' else 1
self.scale_func = lambda x: x
if scale == 'up':
self.scale_func = lambda x: nn.functional.interpolate(x, scale_factor=2, mode='nearest')
self.reflection_pad = nn.ReflectionPad2d(int(np.ceil((kernel_size - 1.) / 2)))
self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride, bias=bias)
self.relu = ReluLayer(out_channels, relu_type)
self.norm = NormLayer(out_channels, norm_type=norm_type)
def forward(self, x):
out = self.scale_func(x)
if self.use_pad:
out = self.reflection_pad(out)
out = self.conv2d(out)
out = self.norm(out)
out = self.relu(out)
return out
class ResidualBlock(nn.Module):
"""
Residual block recommended in: http://torch.ch/blog/2016/02/04/resnets.html
"""
def __init__(self, c_in, c_out, relu_type='prelu', norm_type='bn', scale='none'):
super(ResidualBlock, self).__init__()
if scale == 'none' and c_in == c_out:
self.shortcut_func = lambda x: x
else:
self.shortcut_func = ConvLayer(c_in, c_out, 3, scale)
scale_config_dict = {'down': ['none', 'down'], 'up': ['up', 'none'], 'none': ['none', 'none']}
scale_conf = scale_config_dict[scale]
self.conv1 = ConvLayer(c_in, c_out, 3, scale_conf[0], norm_type=norm_type, relu_type=relu_type)
self.conv2 = ConvLayer(c_out, c_out, 3, scale_conf[1], norm_type=norm_type, relu_type='none')
def forward(self, x):
identity = self.shortcut_func(x)
res = self.conv1(x)
res = self.conv2(res)
return identity + res
class ParseNet(nn.Module):
def __init__(self,
in_size=128,
out_size=128,
min_feat_size=32,
base_ch=64,
parsing_ch=19,
res_depth=10,
relu_type='LeakyReLU',
norm_type='bn',
ch_range=[32, 256]):
super().__init__()
self.res_depth = res_depth
act_args = {'norm_type': norm_type, 'relu_type': relu_type}
min_ch, max_ch = ch_range
ch_clip = lambda x: max(min_ch, min(x, max_ch)) # noqa: E731
min_feat_size = min(in_size, min_feat_size)
down_steps = int(np.log2(in_size // min_feat_size))
up_steps = int(np.log2(out_size // min_feat_size))
# =============== define encoder-body-decoder ====================
self.encoder = []
self.encoder.append(ConvLayer(3, base_ch, 3, 1))
head_ch = base_ch
for i in range(down_steps):
cin, cout = ch_clip(head_ch), ch_clip(head_ch * 2)
self.encoder.append(ResidualBlock(cin, cout, scale='down', **act_args))
head_ch = head_ch * 2
self.body = []
for i in range(res_depth):
self.body.append(ResidualBlock(ch_clip(head_ch), ch_clip(head_ch), **act_args))
self.decoder = []
for i in range(up_steps):
cin, cout = ch_clip(head_ch), ch_clip(head_ch // 2)
self.decoder.append(ResidualBlock(cin, cout, scale='up', **act_args))
head_ch = head_ch // 2
self.encoder = nn.Sequential(*self.encoder)
self.body = nn.Sequential(*self.body)
self.decoder = nn.Sequential(*self.decoder)
self.out_img_conv = ConvLayer(ch_clip(head_ch), 3)
self.out_mask_conv = ConvLayer(ch_clip(head_ch), parsing_ch)
def forward(self, x):
feat = self.encoder(x)
x = feat + self.body(feat)
x = self.decoder(x)
out_img = self.out_img_conv(x)
out_mask = self.out_mask_conv(x)
return out_mask, out_img
+428
View File
@@ -0,0 +1,428 @@
import copy
import os
from dataclasses import dataclass
from typing import Dict, List, Set, Tuple, Optional, Union
import cv2
import insightface
import numpy as np
from insightface.app.common import Face
from PIL import Image
from sklearn.metrics.pairwise import cosine_similarity
from scripts.faceswaplab_swapping import upscaled_inswapper
from scripts.faceswaplab_utils.imgutils import cv2_to_pil, pil_to_cv2, check_against_nsfw
from scripts.faceswaplab_utils.faceswaplab_logging import logger, save_img_debug
from scripts import faceswaplab_globals
from modules.shared import opts
from functools import lru_cache
from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
providers = ["CPUExecutionProvider"]
def cosine_similarity_face(face1, face2) -> float:
"""
Calculates the cosine similarity between two face embeddings.
Args:
face1 (Face): The first face object containing an embedding.
face2 (Face): The second face object containing an embedding.
Returns:
float: The cosine similarity between the face embeddings.
Note:
The cosine similarity ranges from 0 to 1, where 1 indicates identical embeddings and 0 indicates completely
dissimilar embeddings. In this implementation, the similarity is clamped to a minimum value of 0 to ensure a
non-negative similarity score.
"""
# Reshape the face embeddings to have a shape of (1, -1)
vec1 = face1.embedding.reshape(1, -1)
vec2 = face2.embedding.reshape(1, -1)
# Calculate the cosine similarity between the reshaped embeddings
similarity = cosine_similarity(vec1, vec2)
# Return the maximum of 0 and the calculated similarity as the final similarity score
return max(0, similarity[0, 0])
def compare_faces(img1: Image.Image, img2: Image.Image) -> float:
"""
Compares the similarity between two faces extracted from images using cosine similarity.
Args:
img1: The first image containing a face.
img2: The second image containing a face.
Returns:
A float value representing the similarity between the two faces (0 to 1).
Returns -1 if one or both of the images do not contain any faces.
"""
# Extract faces from the images
face1 = get_or_default(get_faces(pil_to_cv2(img1)), 0, None)
face2 = get_or_default(get_faces(pil_to_cv2(img2)), 0, None)
# Check if both faces are detected
if face1 is not None and face2 is not None:
# Calculate the cosine similarity between the faces
return cosine_similarity_face(face1, face2)
# Return -1 if one or both of the images do not contain any faces
return -1
class FaceModelException(Exception):
"""Exception raised when an error is encountered in the face model."""
def __init__(self, message: str) -> None:
"""
Args:
message: A string containing the error description.
"""
self.message = message
super().__init__(self.message)
@lru_cache(maxsize=1)
def getAnalysisModel():
"""
Retrieves the analysis model for face analysis.
Returns:
insightface.app.FaceAnalysis: The analysis model for face analysis.
"""
try :
if not os.path.exists(faceswaplab_globals.ANALYZER_DIR):
os.makedirs(faceswaplab_globals.ANALYZER_DIR)
logger.info("Load analysis model, will take some time.")
# Initialize the analysis model with the specified name and providers
return insightface.app.FaceAnalysis(
name="buffalo_l", providers=providers, root=faceswaplab_globals.ANALYZER_DIR
)
except Exception as e :
logger.error("Loading of swapping model failed, please check the requirements (On Windows, download and install Visual Studio. During the install, make sure to include the Python and C++ packages.)")
raise FaceModelException("Loading of swapping model failed")
@lru_cache(maxsize=1)
def getFaceSwapModel(model_path: str):
"""
Retrieves the face swap model and initializes it if necessary.
Args:
model_path (str): Path to the face swap model.
Returns:
insightface.model_zoo.FaceModel: The face swap model.
"""
try :
# Initializes the face swap model using the specified model path.
return upscaled_inswapper.UpscaledINSwapper(insightface.model_zoo.get_model(model_path, providers=providers))
except Exception as e :
logger.error("Loading of swapping model failed, please check the requirements (On Windows, download and install Visual Studio. During the install, make sure to include the Python and C++ packages.)")
def get_faces(img_data: np.ndarray, det_size=(640, 640), det_thresh : Optional[int]=None, sort_by_face_size = False) -> List[Face]:
"""
Detects and retrieves faces from an image using an analysis model.
Args:
img_data (np.ndarray): The image data as a NumPy array.
det_size (tuple): The desired detection size (width, height). Defaults to (640, 640).
sort_by_face_size (bool) : Will sort the faces by their size from larger to smaller face
Returns:
list: A list of detected faces, sorted by their x-coordinate of the bounding box.
"""
if det_thresh is None :
det_thresh = opts.data.get("faceswaplab_detection_threshold", 0.5)
# Create a deep copy of the analysis model (otherwise det_size is attached to the analysis model and can't be changed)
face_analyser = copy.deepcopy(getAnalysisModel())
# Prepare the analysis model for face detection with the specified detection size
face_analyser.prepare(ctx_id=0, det_thresh=det_thresh, det_size=det_size)
# Get the detected faces from the image using the analysis model
face = face_analyser.get(img_data)
# If no faces are detected and the detection size is larger than 320x320,
# recursively call the function with a smaller detection size
if len(face) == 0 and det_size[0] > 320 and det_size[1] > 320:
det_size_half = (det_size[0] // 2, det_size[1] // 2)
return get_faces(img_data, det_size=det_size_half, det_thresh=det_thresh)
try:
if sort_by_face_size :
return sorted(face, reverse=True, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
# Sort the detected faces based on their x-coordinate of the bounding box
return sorted(face, key=lambda x: x.bbox[0])
except Exception as e:
return []
@dataclass
class ImageResult:
"""
Represents the result of an image swap operation
"""
image: Image.Image
"""
The image object with the swapped face
"""
similarity: Dict[int, float]
"""
A dictionary mapping face indices to their similarity scores.
The similarity scores are represented as floating-point values between 0 and 1.
"""
ref_similarity: Dict[int, float]
"""
A dictionary mapping face indices to their similarity scores compared to a reference image.
The similarity scores are represented as floating-point values between 0 and 1.
"""
def get_or_default(l, index, default):
"""
Retrieve the value at the specified index from the given list.
If the index is out of bounds, return the default value instead.
Args:
l (list): The input list.
index (int): The index to retrieve the value from.
default: The default value to return if the index is out of bounds.
Returns:
The value at the specified index if it exists, otherwise the default value.
"""
return l[index] if index < len(l) else default
def get_faces_from_img_files(files):
"""
Extracts faces from a list of image files.
Args:
files (list): A list of file objects representing image files.
Returns:
list: A list of detected faces.
"""
faces = []
if len(files) > 0:
for file in files:
img = Image.open(file.name) # Open the image file
face = get_or_default(get_faces(pil_to_cv2(img)), 0, None) # Extract faces from the image
if face is not None:
faces.append(face) # Add the detected face to the list of faces
return faces
def blend_faces(faces: List[Face]) -> Face:
"""
Blends the embeddings of multiple faces into a single face.
Args:
faces (List[Face]): List of Face objects.
Returns:
Face: The blended Face object with the averaged embedding.
Returns None if the input list is empty.
Raises:
ValueError: If the embeddings have different shapes.
"""
embeddings = [face.embedding for face in faces]
if len(embeddings) > 0:
embedding_shape = embeddings[0].shape
# Check if all embeddings have the same shape
for embedding in embeddings:
if embedding.shape != embedding_shape:
raise ValueError("embedding shape mismatch")
# Compute the mean of all embeddings
blended_embedding = np.mean(embeddings, axis=0)
# Create a new Face object using the properties of the first face in the list
# Assign the blended embedding to the blended Face object
blended = Face(embedding=blended_embedding, gender=faces[0].gender, age=faces[0].age)
assert not np.array_equal(blended.embedding,faces[0].embedding) if len(faces) > 1 else True, "If len(faces)>0, the blended embedding should not be the same than the first image"
return blended
# Return None if the input list is empty
return None
def swap_face(
reference_face: np.ndarray,
source_face: np.ndarray,
target_img: Image.Image,
model: str,
faces_index: Set[int] = {0},
same_gender=True,
upscaled_swapper = False,
compute_similarity = True,
sort_by_face_size = False
) -> ImageResult:
"""
Swaps faces in the target image with the source face.
Args:
reference_face (np.ndarray): The reference face used for similarity comparison.
source_face (np.ndarray): The source face to be swapped.
target_img (Image.Image): The target image to swap faces in.
model (str): Path to the face swap model.
faces_index (Set[int], optional): Set of indices specifying which faces to swap. Defaults to {0}.
same_gender (bool, optional): If True, only swap faces with the same gender as the source face. Defaults to True.
Returns:
ImageResult: An object containing the swapped image and similarity scores.
"""
return_result = ImageResult(target_img, {}, {})
try :
target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
gender = source_face["gender"]
logger.info("Source Gender %s", gender)
if source_face is not None:
result = target_img
model_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), model)
face_swapper = getFaceSwapModel(model_path)
target_faces = get_faces(target_img, sort_by_face_size=sort_by_face_size)
logger.info("Target faces count : %s", len(target_faces))
if same_gender:
target_faces = [x for x in target_faces if x["gender"] == gender]
logger.info("Target Gender Matches count %s", len(target_faces))
for i, swapped_face in enumerate(target_faces):
logger.info(f"swap face {i}")
if i in faces_index:
result = face_swapper.get(result, swapped_face, source_face, upscale = upscaled_swapper)
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
return_result.image = result_image
if compute_similarity :
try:
result_faces = get_faces(
cv2.cvtColor(np.array(result_image), cv2.COLOR_RGB2BGR), sort_by_face_size=sort_by_face_size
)
if same_gender:
result_faces = [x for x in result_faces if x["gender"] == gender]
for i, swapped_face in enumerate(result_faces):
logger.info(f"compare face {i}")
if i in faces_index and i < len(target_faces):
return_result.similarity[i] = cosine_similarity_face(
source_face, swapped_face
)
return_result.ref_similarity[i] = cosine_similarity_face(
reference_face, swapped_face
)
logger.info(f"similarity {return_result.similarity}")
logger.info(f"ref similarity {return_result.ref_similarity}")
except Exception as e:
logger.error("Similarity processing failed %s", e)
raise e
except Exception as e :
logger.error("Conversion failed %s", e)
raise e
return return_result
def process_image_unit(model, unit : FaceSwapUnitSettings, image: Image.Image, info = None, upscaled_swapper = False, force_blend = False) -> List:
"""Process one image and return a List of (image, info) (one if blended, many if not).
Args:
unit : the current unit
image : the image where to apply swapping
info : The info
Returns:
List of tuple of (image, info) where image is the image where swapping has been applied and info is the image info with similarity infos.
"""
results = []
if unit.enable :
if check_against_nsfw(image) :
return [(image, info)]
if not unit.blend_faces and not force_blend :
src_faces = unit.faces
logger.info(f"will generate {len(src_faces)} images")
else :
logger.info("blend all faces together")
src_faces = [unit.blended_faces]
assert(not np.array_equal(unit.reference_face.embedding,src_faces[0].embedding) if len(unit.faces)>1 else True), "Reference face cannot be the same as blended"
for i,src_face in enumerate(src_faces):
logger.info(f"Process face {i}")
if unit.reference_face is not None :
reference_face = unit.reference_face
else :
logger.info("Use source face as reference face")
reference_face = src_face
save_img_debug(image, "Before swap")
result: ImageResult = swap_face(
reference_face,
src_face,
image,
faces_index=unit.faces_index,
model=model,
same_gender=unit.same_gender,
upscaled_swapper=upscaled_swapper,
compute_similarity=unit.compute_similarity,
sort_by_face_size=unit.sort_by_size
)
save_img_debug(result.image, "After swap")
if result.image is None :
logger.error("Result image is None")
if (not unit.check_similarity) or result.similarity and all([result.similarity.values()!=0]+[x >= unit.min_sim for x in result.similarity.values()]) and all([result.ref_similarity.values()!=0]+[x >= unit.min_ref_sim for x in result.ref_similarity.values()]):
results.append((result.image, f"{info}, similarity = {result.similarity}, ref_similarity = {result.ref_similarity}"))
else:
logger.warning(
f"skip, similarity to low, sim = {result.similarity} (target {unit.min_sim}) ref sim = {result.ref_similarity} (target = {unit.min_ref_sim})"
)
logger.debug("process_image_unit : Unit produced %s results", len(results))
return results
def process_images_units(model, units : List[FaceSwapUnitSettings], images: List[Tuple[Optional[Image.Image], Optional[str]]], upscaled_swapper = False, force_blend = False) -> Union[List,None]:
if len(units) == 0 :
logger.info("Finished processing image, return %s images", len(images))
return None
logger.debug("%s more units", len(units))
processed_images = []
for i,(image, info) in enumerate(images) :
logger.debug("Processing image %s", i)
swapped = process_image_unit(model,units[0],image, info, upscaled_swapper, force_blend)
logger.debug("Image %s -> %s images", i, len(swapped))
nexts = process_images_units(model,units[1:],swapped, upscaled_swapper,force_blend)
if nexts :
processed_images.extend(nexts)
else :
processed_images.extend(swapped)
return processed_images
@@ -0,0 +1,188 @@
import cv2
import numpy as np
import onnx
import onnxruntime
from insightface.model_zoo.inswapper import INSwapper
from insightface.utils import face_align
from modules import codeformer_model, processing, scripts, shared
from modules.face_restoration import FaceRestoration
from modules.shared import cmd_opts, opts, state
from modules.upscaler import UpscalerData
from onnx import numpy_helper
from PIL import Image
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from scripts.faceswaplab_postprocessing import upscaling
from scripts.faceswaplab_postprocessing.postprocessing_options import \
PostProcessingOptions
from scripts.faceswaplab_swapping.facemask import generate_face_mask
from scripts.faceswaplab_utils.imgutils import cv2_to_pil, pil_to_cv2
def get_upscaler() -> UpscalerData:
for upscaler in shared.sd_upscalers:
if upscaler.name == opts.data.get("faceswaplab_upscaled_swapper_upscaler", "LDSR"):
return upscaler
return None
def merge_images_with_mask(image1, image2, mask):
if image1.shape != image2.shape or image1.shape[:2] != mask.shape:
raise ValueError("Img should have the same shape")
mask = mask.astype(np.uint8)
masked_region = cv2.bitwise_and(image2, image2, mask=mask)
inverse_mask = cv2.bitwise_not(mask)
empty_region = cv2.bitwise_and(image1, image1, mask=inverse_mask)
merged_image = cv2.add(empty_region, masked_region)
return merged_image
def erode_mask(mask, kernel_size=3, iterations=1):
kernel = np.ones((kernel_size, kernel_size), np.uint8)
eroded_mask = cv2.erode(mask, kernel, iterations=iterations)
return eroded_mask
def apply_gaussian_blur(mask, kernel_size=(5, 5), sigma_x=0):
blurred_mask = cv2.GaussianBlur(mask, kernel_size, sigma_x)
return blurred_mask
def dilate_mask(mask, kernel_size=5, iterations=1):
kernel = np.ones((kernel_size, kernel_size), np.uint8)
dilated_mask = cv2.dilate(mask, kernel, iterations=iterations)
return dilated_mask
def get_face_mask(aimg,bgr_fake):
mask1 = generate_face_mask(aimg, device = shared.device)
mask2 = generate_face_mask(bgr_fake, device = shared.device)
mask = dilate_mask(cv2.bitwise_or(mask1,mask2))
return mask
class UpscaledINSwapper():
def __init__(self, inswapper : INSwapper):
self.__dict__.update(inswapper.__dict__)
def forward(self, img, latent):
img = (img - self.input_mean) / self.input_std
pred = self.session.run(self.output_names, {self.input_names[0]: img, self.input_names[1]: latent})[0]
return pred
def super_resolution(self,img, k = 2) :
pil_img = cv2_to_pil(img)
options = PostProcessingOptions(
upscaler_name=opts.data.get('faceswaplab_upscaled_swapper_upscaler', 'LDSR'),
upscale_visibility=1,
scale=k,
face_restorer_name=opts.data.get('faceswaplab_upscaled_swapper_face_restorer', ""),
codeformer_weight= opts.data.get('faceswaplab_upscaled_swapper_face_restorer_weight', 1),
restorer_visibility=opts.data.get('faceswaplab_upscaled_swapper_face_restorer_visibility', 1))
upscaled = upscaling.upscale_img(pil_img, options)
upscaled = upscaling.restore_face(upscaled, options)
return pil_to_cv2(upscaled)
def get(self, img, target_face, source_face, paste_back=True, upscale = True):
aimg, M = face_align.norm_crop2(img, target_face.kps, self.input_size[0])
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
latent = source_face.normed_embedding.reshape((1,-1))
latent = np.dot(latent, self.emap)
latent /= np.linalg.norm(latent)
pred = self.session.run(self.output_names, {self.input_names[0]: blob, self.input_names[1]: latent})[0]
#print(latent.shape, latent.dtype, pred.shape)
img_fake = pred.transpose((0,2,3,1))[0]
bgr_fake = np.clip(255 * img_fake, 0, 255).astype(np.uint8)[:,:,::-1]
try :
if not paste_back:
return bgr_fake, M
else:
target_img = img
def compute_diff(bgr_fake,aimg) :
fake_diff = bgr_fake.astype(np.float32) - aimg.astype(np.float32)
fake_diff = np.abs(fake_diff).mean(axis=2)
fake_diff[:2,:] = 0
fake_diff[-2:,:] = 0
fake_diff[:,:2] = 0
fake_diff[:,-2:] = 0
return fake_diff
if upscale :
print("*"*80)
print(f"Upscaled inswapper using {opts.data.get('faceswaplab_upscaled_swapper_upscaler', 'LDSR')}")
print("*"*80)
k = 4
aimg, M = face_align.norm_crop2(img, target_face.kps, self.input_size[0]*k)
# upscale and restore face :
bgr_fake = self.super_resolution(bgr_fake, k)
if opts.data.get("faceswaplab_upscaled_improved_mask", True) :
mask = get_face_mask(aimg,bgr_fake)
bgr_fake = merge_images_with_mask(aimg, bgr_fake,mask)
# compute fake_diff before sharpen and color correction (better result)
fake_diff = compute_diff(bgr_fake, aimg)
if opts.data.get("faceswaplab_upscaled_swapper_sharpen", True) :
print("sharpen")
# Add sharpness
blurred = cv2.GaussianBlur(bgr_fake, (0, 0), 3)
bgr_fake = cv2.addWeighted(bgr_fake, 1.5, blurred, -0.5, 0)
# Apply color corrections
if opts.data.get("faceswaplab_upscaled_swapper_fixcolor", True) :
print("color correction")
correction = processing.setup_color_correction(cv2_to_pil(aimg))
bgr_fake_pil = processing.apply_color_correction(correction, cv2_to_pil(bgr_fake))
bgr_fake = pil_to_cv2(bgr_fake_pil)
else :
fake_diff = compute_diff(bgr_fake, aimg)
IM = cv2.invertAffineTransform(M)
img_white = np.full((aimg.shape[0],aimg.shape[1]), 255, dtype=np.float32)
bgr_fake = cv2.warpAffine(bgr_fake, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white = cv2.warpAffine(img_white, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
fake_diff = cv2.warpAffine(fake_diff, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white[img_white>20] = 255
fthresh = opts.data.get('faceswaplab_upscaled_swapper_fthresh', 10)
print("fthresh", fthresh)
fake_diff[fake_diff<fthresh] = 0
fake_diff[fake_diff>=fthresh] = 255
img_mask = img_white
mask_h_inds, mask_w_inds = np.where(img_mask==255)
mask_h = np.max(mask_h_inds) - np.min(mask_h_inds)
mask_w = np.max(mask_w_inds) - np.min(mask_w_inds)
mask_size = int(np.sqrt(mask_h*mask_w))
erosion_factor = opts.data.get('faceswaplab_upscaled_swapper_erosion', 1)
k = max(int(mask_size//10*erosion_factor), int(10*erosion_factor))
kernel = np.ones((k,k),np.uint8)
img_mask = cv2.erode(img_mask,kernel,iterations = 1)
kernel = np.ones((2,2),np.uint8)
fake_diff = cv2.dilate(fake_diff,kernel,iterations = 1)
k = max(int(mask_size//20*erosion_factor), int(5*erosion_factor))
kernel_size = (k, k)
blur_size = tuple(2*i+1 for i in kernel_size)
img_mask = cv2.GaussianBlur(img_mask, blur_size, 0)
k = int(5*erosion_factor)
kernel_size = (k, k)
blur_size = tuple(2*i+1 for i in kernel_size)
fake_diff = cv2.GaussianBlur(fake_diff, blur_size, 0)
img_mask /= 255
fake_diff /= 255
img_mask = np.reshape(img_mask, [img_mask.shape[0],img_mask.shape[1],1])
fake_merged = img_mask * bgr_fake + (1-img_mask) * target_img.astype(np.float32)
fake_merged = fake_merged.astype(np.uint8)
return fake_merged
except Exception as e :
import traceback
traceback.print_exc()
raise e
+326
View File
@@ -0,0 +1,326 @@
import os
import tempfile
from pprint import pformat, pprint
import dill as pickle
import gradio as gr
import modules.scripts as scripts
import numpy as np
import onnx
import pandas as pd
from scripts.faceswaplab_ui.faceswaplab_unit_ui import faceswap_unit_ui
from scripts.faceswaplab_ui.faceswaplab_upscaler_ui import upscaler_ui
from insightface.app.common import Face
from modules import script_callbacks, scripts
from PIL import Image
from modules.shared import opts
from scripts.faceswaplab_utils import imgutils
from scripts.faceswaplab_utils.imgutils import pil_to_cv2
from scripts.faceswaplab_utils.models_utils import get_models
from scripts.faceswaplab_utils.faceswaplab_logging import logger
import scripts.faceswaplab_swapping.swapper as swapper
from scripts.faceswaplab_postprocessing.postprocessing_options import PostProcessingOptions
from scripts.faceswaplab_postprocessing.postprocessing import enhance_image
from dataclasses import fields
from typing import List
from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
from scripts.faceswaplab_utils.models_utils import get_current_model
def compare(img1, img2):
if img1 is not None and img2 is not None:
return swapper.compare_faces(img1, img2)
return "You need 2 images to compare"
def extract_faces(files, extract_path, face_restorer_name, face_restorer_visibility, codeformer_weight,upscaler_name,upscaler_scale, upscaler_visibility,inpainting_denoising_strengh, inpainting_prompt, inpainting_negative_prompt, inpainting_steps, inpainting_sampler,inpainting_when):
if not extract_path :
tempfile.mkdtemp()
if files is not None:
images = []
for file in files :
img = Image.open(file.name).convert("RGB")
faces = swapper.get_faces(pil_to_cv2(img))
if faces:
face_images = []
for face in faces:
bbox = face.bbox.astype(int)
x_min, y_min, x_max, y_max = bbox
face_image = img.crop((x_min, y_min, x_max, y_max))
if face_restorer_name or face_restorer_visibility:
scale = 1 if face_image.width > 512 else 512//face_image.width
face_image = enhance_image(face_image, PostProcessingOptions(face_restorer_name=face_restorer_name,
restorer_visibility=face_restorer_visibility,
codeformer_weight= codeformer_weight,
upscaler_name=upscaler_name,
upscale_visibility=upscaler_visibility,
scale=scale,
inpainting_denoising_strengh=inpainting_denoising_strengh,
inpainting_prompt=inpainting_prompt,
inpainting_steps=inpainting_steps,
inpainting_negative_prompt=inpainting_negative_prompt,
inpainting_when=inpainting_when,
inpainting_sampler=inpainting_sampler))
path = tempfile.NamedTemporaryFile(delete=False,suffix=".png",dir=extract_path).name
face_image.save(path)
face_images.append(path)
images+= face_images
return images
return None
def analyse_faces(image, det_threshold = 0.5) :
try :
faces = swapper.get_faces(imgutils.pil_to_cv2(image), det_thresh=det_threshold)
result = ""
for i,face in enumerate(faces) :
result+= f"\nFace {i} \n" + "="*40 +"\n"
result+= pformat(face) + "\n"
result+= "="*40
return result
except Exception as e :
logger.error("Analysis Failed : %s", e)
return "Analysis Failed"
def build_face_checkpoint_and_save(batch_files, name):
"""
Builds a face checkpoint, swaps faces, and saves the result to a file.
Args:
batch_files (list): List of image file paths.
name (str): Name of the face checkpoint
Returns:
PIL.Image.Image or None: Resulting swapped face image if successful, otherwise None.
"""
batch_files = batch_files or []
print("Build", name, [x.name for x in batch_files])
faces = swapper.get_faces_from_img_files(batch_files)
blended_face = swapper.blend_faces(faces)
preview_path = os.path.join(
scripts.basedir(), "extensions", "sd-webui-faceswaplab", "references"
)
faces_path = os.path.join(scripts.basedir(), "models", "faceswaplab","faces")
if not os.path.exists(faces_path):
os.makedirs(faces_path)
target_img = None
if blended_face:
if blended_face["gender"] == 0:
target_img = Image.open(os.path.join(preview_path, "woman.png"))
else:
target_img = Image.open(os.path.join(preview_path, "man.png"))
if name == "":
name = "default_name"
pprint(blended_face)
result = swapper.swap_face(blended_face, blended_face, target_img, get_models()[0])
result_image = enhance_image(result.image, PostProcessingOptions(face_restorer_name="CodeFormer", restorer_visibility=1))
file_path = os.path.join(faces_path, f"{name}.pkl")
file_number = 1
while os.path.exists(file_path):
file_path = os.path.join(faces_path, f"{name}_{file_number}.pkl")
file_number += 1
result_image.save(file_path+".png")
with open(file_path, "wb") as file:
pickle.dump({"embedding" :blended_face.embedding, "gender" :blended_face.gender, "age" :blended_face.age},file)
try :
with open(file_path, "rb") as file:
data = Face(pickle.load(file))
print(data)
except Exception as e :
print(e)
return result_image
print("No face found")
return target_img
def explore_onnx_faceswap_model(model_path):
data = {
'Node Name': [],
'Op Type': [],
'Inputs': [],
'Outputs': [],
'Attributes': []
}
if model_path:
model = onnx.load(model_path)
for node in model.graph.node:
data['Node Name'].append(pformat(node.name))
data['Op Type'].append(pformat(node.op_type))
data['Inputs'].append(pformat(node.input))
data['Outputs'].append(pformat(node.output))
attributes = []
for attr in node.attribute:
attr_name = attr.name
attr_value = attr.t
attributes.append("{} = {}".format(pformat(attr_name), pformat(attr_value)))
data['Attributes'].append(attributes)
df = pd.DataFrame(data)
return df
def batch_process(files, save_path, *components):
try :
if save_path is not None:
os.makedirs(save_path, exist_ok=True)
units_count = opts.data.get("faceswaplab_units_count", 3)
units: List[FaceSwapUnitSettings] = []
#Parse and convert units flat components into FaceSwapUnitSettings
for i in range(0, units_count):
units += [FaceSwapUnitSettings.get_unit_configuration(i, components)]
for i, u in enumerate(units):
logger.debug("%s, %s", pformat(i), pformat(u))
#Parse the postprocessing options
#We must first find where to start from (after face swapping units)
len_conf: int = len(fields(FaceSwapUnitSettings))
shift: int = units_count * len_conf
postprocess_options = PostProcessingOptions(
*components[shift : shift + len(fields(PostProcessingOptions))]
)
logger.debug("%s", pformat(postprocess_options))
units = [u for u in units if u.enable]
if files is not None:
images = []
for file in files :
current_images = []
src_image = Image.open(file.name).convert("RGB")
swapped_images = swapper.process_images_units(get_current_model(), images=[(src_image,None)], units=units, upscaled_swapper=opts.data.get("faceswaplab_upscaled_swapper", False))
if len(swapped_images) > 0:
current_images+= [img for img,info in swapped_images]
logger.info("%s images generated", len(current_images))
for i, img in enumerate(current_images) :
current_images[i] = enhance_image(img,postprocess_options)
for img in current_images :
path = tempfile.NamedTemporaryFile(delete=False,suffix=".png",dir=save_path).name
img.save(path)
images += current_images
return images
except Exception as e:
logger.error("Batch Process error : %s",e)
import traceback
traceback.print_exc()
return None
def tools_ui():
models = get_models()
with gr.Tab("Tools"):
with gr.Tab("Build"):
gr.Markdown(
"""Build a face based on a batch list of images. Will blend the resulting face and store the checkpoint in the faceswaplab/faces directory.""")
with gr.Row():
batch_files = gr.components.File(
type="file",
file_count="multiple",
label="Batch Sources Images",
optional=True,
elem_id="faceswaplab_build_batch_files"
)
preview = gr.components.Image(type="pil", label="Preview", interactive=False, elem_id="faceswaplab_build_preview_face")
name = gr.Textbox(
value="Face",
placeholder="Name of the character",
label="Name of the character",
elem_id="faceswaplab_build_character_name"
)
generate_checkpoint_btn = gr.Button("Save",elem_id="faceswaplab_build_save_btn")
with gr.Tab("Compare"):
gr.Markdown(
"""Give a similarity score between two images (only first face is compared).""")
with gr.Row():
img1 = gr.components.Image(type="pil",
label="Face 1",
elem_id="faceswaplab_compare_face1"
)
img2 = gr.components.Image(type="pil",
label="Face 2",
elem_id="faceswaplab_compare_face2"
)
compare_btn = gr.Button("Compare",elem_id="faceswaplab_compare_btn")
compare_result_text = gr.Textbox(
interactive=False, label="Similarity", value="0", elem_id="faceswaplab_compare_result"
)
with gr.Tab("Extract"):
gr.Markdown(
"""Extract all faces from a batch of images. Will apply enhancement in the tools enhancement tab.""")
with gr.Row():
extracted_source_files = gr.components.File(
type="file",
file_count="multiple",
label="Batch Sources Images",
optional=True,
elem_id="faceswaplab_extract_batch_images"
)
extracted_faces = gr.Gallery(
label="Extracted faces", show_label=False,
elem_id="faceswaplab_extract_results"
).style(columns=[2], rows=[2])
extract_save_path = gr.Textbox(label="Destination Directory", value="", elem_id="faceswaplab_extract_destination")
extract_btn = gr.Button("Extract", elem_id="faceswaplab_extract_btn")
with gr.Tab("Explore Model"):
model = gr.Dropdown(
choices=models,
label="Model not found, please download one and reload automatic 1111",
elem_id="faceswaplab_explore_model"
)
explore_btn = gr.Button("Explore", elem_id="faceswaplab_explore_btn")
explore_result_text = gr.Dataframe(
interactive=False, label="Explored",
elem_id="faceswaplab_explore_result"
)
with gr.Tab("Analyse Face"):
img_to_analyse = gr.components.Image(type="pil", label="Face", elem_id="faceswaplab_analyse_face")
analyse_det_threshold = gr.Slider(0.1, 1, 0.5, step=0.01, label="Detection threshold", elem_id="faceswaplab_analyse_det_threshold")
analyse_btn = gr.Button("Analyse", elem_id="faceswaplab_analyse_btn")
analyse_results = gr.Textbox(label="Results", interactive=False, value="", elem_id="faceswaplab_analyse_results")
with gr.Tab("Batch Process"):
with gr.Tab("Source Images"):
gr.Markdown(
"""Batch process images. Will apply enhancement in the tools enhancement tab.""")
with gr.Row():
batch_source_files = gr.components.File(
type="file",
file_count="multiple",
label="Batch Sources Images",
optional=True,
elem_id="faceswaplab_batch_images"
)
batch_results = gr.Gallery(
label="Batch result", show_label=False,
elem_id="faceswaplab_batch_results"
).style(columns=[2], rows=[2])
batch_save_path = gr.Textbox(label="Destination Directory", value="outputs/faceswap/", elem_id="faceswaplab_batch_destination")
batch_save_btn= gr.Button("Process & Save", elem_id="faceswaplab_extract_btn")
unit_components = []
for i in range(1,opts.data.get("faceswaplab_units_count", 3)+1):
unit_components += faceswap_unit_ui(False, i, id_prefix="faceswaplab_tab")
upscale_options = upscaler_ui()
explore_btn.click(explore_onnx_faceswap_model, inputs=[model], outputs=[explore_result_text])
compare_btn.click(compare, inputs=[img1, img2], outputs=[compare_result_text])
generate_checkpoint_btn.click(build_face_checkpoint_and_save, inputs=[batch_files, name], outputs=[preview])
extract_btn.click(extract_faces, inputs=[extracted_source_files, extract_save_path]+upscale_options, outputs=[extracted_faces])
analyse_btn.click(analyse_faces, inputs=[img_to_analyse,analyse_det_threshold], outputs=[analyse_results])
batch_save_btn.click(batch_process, inputs=[batch_source_files, batch_save_path]+unit_components+upscale_options, outputs=[batch_results])
def on_ui_tabs() :
with gr.Blocks(analytics_enabled=False) as ui_faceswap:
tools_ui()
return [(ui_faceswap, "FaceSwapLab", "faceswaplab_tab")]
@@ -0,0 +1,152 @@
from scripts.faceswaplab_swapping import swapper
import numpy as np
import base64
import io
from dataclasses import dataclass, fields
from typing import List, Union
import dill as pickle
import gradio as gr
from insightface.app.common import Face
from PIL import Image
from scripts.faceswaplab_utils.imgutils import (pil_to_cv2,check_against_nsfw)
from scripts.faceswaplab_utils.faceswaplab_logging import logger
@dataclass
class FaceSwapUnitSettings:
# ORDER of parameters is IMPORTANT. It should match the result of faceswap_unit_ui
# The image given in reference
source_img: Union[Image.Image, str]
# The checkpoint file
source_face : str
# The batch source images
_batch_files: Union[gr.components.File,List[Image.Image]]
# Will blend faces if True
blend_faces: bool
# Enable this unit
enable: bool
# Use same gender filtering
same_gender: bool
# Sort faces by their size (from larger to smaller)
sort_by_size : bool
# If True, discard images with low similarity
check_similarity : bool
# if True will compute similarity and add it to the image info
_compute_similarity :bool
# Minimum similarity against the used face (reference, batch or checkpoint)
min_sim: float
# Minimum similarity against the reference (reference or checkpoint if checkpoint is given)
min_ref_sim: float
# The face index to use for swapping
_faces_index: str
# The face index to get image from source
reference_face_index : int
# Swap in the source image in img2img (before processing)
swap_in_source: bool
# Swap in the generated image in img2img (always on for txt2img)
swap_in_generated: bool
@staticmethod
def get_unit_configuration(unit: int, components):
fields_count = len(fields(FaceSwapUnitSettings))
return FaceSwapUnitSettings(
*components[unit * fields_count : unit * fields_count + fields_count]
)
@property
def faces_index(self):
"""
Convert _faces_index from str to int
"""
faces_index = {
int(x) for x in self._faces_index.strip(",").split(",") if x.isnumeric()
}
if len(faces_index) == 0:
return {0}
logger.debug("FACES INDEX : %s", faces_index)
return faces_index
@property
def compute_similarity(self) :
return self._compute_similarity or self.check_similarity
@property
def batch_files(self):
"""
Return empty array instead of None for batch files
"""
return self._batch_files or []
@property
def reference_face(self) :
"""
Extract reference face (only once and store it for the rest of processing).
Reference face is the checkpoint or the source image or the first image in the batch in that order.
"""
if not hasattr(self,"_reference_face") :
if self.source_face and self.source_face != "None" :
with open(self.source_face, "rb") as file:
try :
logger.info(f"loading pickle {file.name}")
face = Face(pickle.load(file))
self._reference_face = face
except Exception as e :
logger.error("Failed to load checkpoint : %s", e)
elif self.source_img is not None :
if isinstance(self.source_img, str): # source_img is a base64 string
if 'base64,' in self.source_img: # check if the base64 string has a data URL scheme
base64_data = self.source_img.split('base64,')[-1]
img_bytes = base64.b64decode(base64_data)
else:
# if no data URL scheme, just decode
img_bytes = base64.b64decode(self.source_img)
self.source_img = Image.open(io.BytesIO(img_bytes))
source_img = pil_to_cv2(self.source_img)
self._reference_face = swapper.get_or_default(swapper.get_faces(source_img), self.reference_face_index, None)
if self._reference_face is None :
logger.error("Face not found in reference image")
else :
self._reference_face = None
if self._reference_face is None :
logger.error("You need at least one reference face")
return self._reference_face
@property
def faces(self) :
"""_summary_
Extract all faces (including reference face) to provide an array of faces
Only processed once.
"""
if self.batch_files is not None and not hasattr(self,"_faces") :
self._faces = [self.reference_face] if self.reference_face is not None else []
for file in self.batch_files :
if isinstance(file, Image.Image) :
img = file
else :
img = Image.open(file.name)
face = swapper.get_or_default(swapper.get_faces(pil_to_cv2(img)), 0, None)
if face is not None :
self._faces.append(face)
return self._faces
@property
def blended_faces(self):
"""
Blend the faces using the mean of all embeddings
"""
if not hasattr(self,"_blended_faces") :
self._blended_faces = swapper.blend_faces(self.faces)
assert(all([not np.array_equal(self._blended_faces.embedding, face.embedding) for face in self.faces]) if len(self.faces) > 1 else True), "Blended faces cannot be the same as one of the face if len(face)>0"
assert(not np.array_equal(self._blended_faces.embedding,self.reference_face.embedding) if len(self.faces) > 1 else True), "Blended faces cannot be the same as reference face if len(face)>0"
return self._blended_faces
@@ -0,0 +1,111 @@
from scripts.faceswaplab_utils.models_utils import get_face_checkpoints
import gradio as gr
def faceswap_unit_ui(is_img2img, unit_num=1, id_prefix="faceswaplab"):
with gr.Tab(f"Face {unit_num}"):
with gr.Column():
gr.Markdown(
"""Reference is an image. First face will be extracted.
First face of batches sources will be extracted and used as input (or blended if blend is activated).""")
with gr.Row():
img = gr.components.Image(type="pil", label="Reference", elem_id=f"{id_prefix}_face{unit_num}_reference_image")
batch_files = gr.components.File(
type="file",
file_count="multiple",
label="Batch Sources Images",
optional=True,
elem_id=f"{id_prefix}_face{unit_num}_batch_source_face_files"
)
gr.Markdown(
"""Face checkpoint built with the checkpoint builder in tools. Will overwrite reference image.""")
with gr.Row() :
face = gr.Dropdown(
choices=get_face_checkpoints(),
label="Face Checkpoint (precedence over reference face)",
elem_id=f"{id_prefix}_face{unit_num}_face_checkpoint"
)
refresh = gr.Button(value='', variant='tool', elem_id=f"{id_prefix}_face{unit_num}_refresh_checkpoints")
def refresh_fn(selected):
return gr.Dropdown.update(value=selected, choices=get_face_checkpoints())
refresh.click(fn=refresh_fn,inputs=face, outputs=face)
with gr.Row():
enable = gr.Checkbox(False, placeholder="enable", label="Enable", elem_id=f"{id_prefix}_face{unit_num}_enable")
blend_faces = gr.Checkbox(
True, placeholder="Blend Faces", label="Blend Faces ((Source|Checkpoint)+References = 1)",
elem_id=f"{id_prefix}_face{unit_num}_blend_faces",
interactive=True
)
gr.Markdown("""Discard images with low similarity or no faces :""")
with gr.Row():
check_similarity = gr.Checkbox(False, placeholder="discard", label="Check similarity",
elem_id=f"{id_prefix}_face{unit_num}_check_similarity")
compute_similarity = gr.Checkbox(False, label="Compute similarity",
elem_id=f"{id_prefix}_face{unit_num}_compute_similarity")
min_sim = gr.Slider(0, 1, 0, step=0.01, label="Min similarity",
elem_id=f"{id_prefix}_face{unit_num}_min_similarity")
min_ref_sim = gr.Slider(
0, 1, 0, step=0.01, label="Min reference similarity",
elem_id=f"{id_prefix}_face{unit_num}_min_ref_similarity"
)
gr.Markdown("""Select the face to be swapped, you can sort by size or use the same gender as the desired face:""")
with gr.Row():
same_gender = gr.Checkbox(
False, placeholder="Same Gender", label="Same Gender",
elem_id=f"{id_prefix}_face{unit_num}_same_gender"
)
sort_by_size = gr.Checkbox(
False, placeholder="Sort by size", label="Sort by size (larger>smaller)",
elem_id=f"{id_prefix}_face{unit_num}_sort_by_size"
)
target_faces_index = gr.Textbox(
value="0",
placeholder="Which face to swap (comma separated), start from 0 (by gender if same_gender is enabled)",
label="Target face : Comma separated face number(s)",
elem_id=f"{id_prefix}_face{unit_num}_target_faces_index"
)
gr.Markdown("""The following will only affect reference face image (and is not affected by sort by size) :""")
reference_faces_index = gr.Number(
value=0,
precision=0,
minimum=0,
placeholder="Which face to get from reference image start from 0",
label="Reference source face : start from 0",
elem_id=f"{id_prefix}_face{unit_num}_reference_face_index"
)
gr.Markdown("""Configure swapping. Swapping can occure before img2img, after or both :""", visible=is_img2img)
swap_in_source = gr.Checkbox(
False,
placeholder="Swap face in source image",
label="Swap in source image (blended face)",
visible=is_img2img,
elem_id=f"{id_prefix}_face{unit_num}_swap_in_source"
)
swap_in_generated = gr.Checkbox(
True,
placeholder="Swap face in generated image",
label="Swap in generated image",
visible=is_img2img,
elem_id=f"{id_prefix}_face{unit_num}_swap_in_generated"
)
# If changed, you need to change FaceSwapUnitSettings accordingly
# ORDER of parameters is IMPORTANT. It should match the result of FaceSwapUnitSettings
return [
img,
face,
batch_files,
blend_faces,
enable,
same_gender,
sort_by_size,
check_similarity,
compute_similarity,
min_sim,
min_ref_sim,
target_faces_index,
reference_faces_index,
swap_in_source,
swap_in_generated,
]
@@ -0,0 +1,80 @@
import gradio as gr
import modules
from modules import shared, sd_models
from modules.shared import cmd_opts, opts, state
import scripts.faceswaplab_postprocessing.upscaling as upscaling
from scripts.faceswaplab_utils.faceswaplab_logging import logger
def upscaler_ui():
with gr.Tab(f"Post-Processing"):
gr.Markdown(
"""Upscaling is performed on the whole image. Upscaling happens before face restoration.""")
with gr.Row():
face_restorer_name = gr.Radio(
label="Restore Face",
choices=["None"] + [x.name() for x in shared.face_restorers],
value=lambda : opts.data.get("faceswaplab_pp_default_face_restorer", shared.face_restorers[0].name()),
type="value",
elem_id="faceswaplab_pp_face_restorer"
)
with gr.Column():
face_restorer_visibility = gr.Slider(
0, 1, value=lambda:opts.data.get("faceswaplab_pp_default_face_restorer_visibility", 1), step=0.001, label="Restore visibility",
elem_id="faceswaplab_pp_face_restorer_visibility"
)
codeformer_weight = gr.Slider(
0, 1, value=lambda:opts.data.get("faceswaplab_pp_default_face_restorer_weight", 1), step=0.001, label="codeformer weight",
elem_id="faceswaplab_pp_face_restorer_weight"
)
upscaler_name = gr.Dropdown(
choices=[upscaler.name for upscaler in shared.sd_upscalers],
value= lambda:opts.data.get("faceswaplab_pp_default_upscaler","None"),
label="Upscaler",
elem_id="faceswaplab_pp_upscaler"
)
upscaler_scale = gr.Slider(1, 8, 1, step=0.1, label="Upscaler scale", elem_id="faceswaplab_pp_upscaler_scale")
upscaler_visibility = gr.Slider(
0, 1, value=lambda:opts.data.get("faceswaplab_pp_default_upscaler_visibility", 1), step=0.1, label="Upscaler visibility (if scale = 1)",
elem_id="faceswaplab_pp_upscaler_visibility"
)
with gr.Accordion(f"Post Inpainting", open=True):
gr.Markdown(
"""Inpainting sends image to inpainting with a mask on face (once for each faces).""")
inpainting_when = gr.Dropdown(
elem_id="faceswaplab_pp_inpainting_when", choices = [e.value for e in upscaling.InpaintingWhen.__members__.values()],value=[upscaling.InpaintingWhen.BEFORE_RESTORE_FACE.value], label="Enable/When")
inpainting_denoising_strength = gr.Slider(
0, 1, 0, step=0.01, elem_id="faceswaplab_pp_inpainting_denoising_strength", label="Denoising strenght (will send face to img2img after processing)"
)
inpainting_denoising_prompt = gr.Textbox("Portrait of a [gender]",elem_id="faceswaplab_pp_inpainting_denoising_prompt", label="Inpainting prompt use [gender] instead of men or woman")
inpainting_denoising_negative_prompt = gr.Textbox("", elem_id="faceswaplab_pp_inpainting_denoising_neg_prompt", label="Inpainting negative prompt use [gender] instead of men or woman")
with gr.Row():
samplers_names = [s.name for s in modules.sd_samplers.all_samplers]
inpainting_sampler = gr.Dropdown(
choices=samplers_names,
value=[samplers_names[0]],
label="Inpainting Sampler",
elem_id="faceswaplab_pp_inpainting_sampler"
)
inpainting_denoising_steps = gr.Slider(
1, 150, 20, step=1, label="Inpainting steps",
elem_id="faceswaplab_pp_inpainting_steps"
)
inpaiting_model = gr.Dropdown(choices=["Current"]+sd_models.checkpoint_tiles(), default="Current", label="sd model (experimental)", elem_id="faceswaplab_pp_inpainting_sd_model")
return [
face_restorer_name,
face_restorer_visibility,
codeformer_weight,
upscaler_name,
upscaler_scale,
upscaler_visibility,
inpainting_denoising_strength,
inpainting_denoising_prompt,
inpainting_denoising_negative_prompt,
inpainting_denoising_steps,
inpainting_sampler,
inpainting_when,
inpaiting_model
]
@@ -0,0 +1,53 @@
import logging
import copy
import sys
from modules import shared
from PIL import Image
class ColoredFormatter(logging.Formatter):
COLORS = {
"DEBUG": "\033[0;36m", # CYAN
"INFO": "\033[0;32m", # GREEN
"WARNING": "\033[0;33m", # YELLOW
"ERROR": "\033[0;31m", # RED
"CRITICAL": "\033[0;37;41m", # WHITE ON RED
"RESET": "\033[0m", # RESET COLOR
}
def format(self, record):
colored_record = copy.copy(record)
levelname = colored_record.levelname
seq = self.COLORS.get(levelname, self.COLORS["RESET"])
colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
return super().format(colored_record)
# Create a new logger
logger = logging.getLogger("FaceSwapLab")
logger.propagate = False
# Add handler if we don't have one.
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(
ColoredFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
logger.addHandler(handler)
# Configure logger
loglevel_string = getattr(shared.cmd_opts, "faceswaplab_loglevel", "INFO")
loglevel = getattr(logging, loglevel_string.upper(), "INFO")
logger.setLevel(loglevel)
import tempfile
if logger.getEffectiveLevel() <= logging.DEBUG :
DEBUG_DIR = tempfile.mkdtemp()
def save_img_debug(img: Image.Image, message: str, *opts):
if logger.getEffectiveLevel() <= logging.DEBUG:
with tempfile.NamedTemporaryFile(dir=DEBUG_DIR, delete=False, suffix=".png") as temp_file:
img_path = temp_file.name
img.save(img_path)
message_with_link = f"{message}\nImage: file://{img_path}"
logger.debug(message_with_link, *opts)
+185
View File
@@ -0,0 +1,185 @@
import io
from typing import Optional
from PIL import Image, ImageChops, ImageOps,ImageFilter
import cv2
import numpy as np
from math import isqrt, ceil
import torch
from ifnude import detect
from scripts.faceswaplab_globals import NSFW_SCORE
from modules import processing
import base64
def check_against_nsfw(img):
shapes = []
chunks = detect(img)
for chunk in chunks:
shapes.append(chunk["score"] > NSFW_SCORE)
return any(shapes)
def pil_to_cv2(pil_img):
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
def cv2_to_pil(cv2_img):
return Image.fromarray(cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB))
def torch_to_pil(images):
"""
Convert a numpy image or a batch of images to a PIL image.
"""
images = images.cpu().permute(0, 2, 3, 1).numpy()
if images.ndim == 3:
images = images[None, ...]
images = (images * 255).round().astype("uint8")
pil_images = [Image.fromarray(image) for image in images]
return pil_images
def pil_to_torch(pil_images):
"""
Convert a PIL image or a list of PIL images to a torch tensor or a batch of torch tensors.
"""
if isinstance(pil_images, list):
numpy_images = [np.array(image) for image in pil_images]
torch_images = torch.from_numpy(np.stack(numpy_images)).permute(0, 3, 1, 2)
return torch_images
numpy_image = np.array(pil_images)
torch_image = torch.from_numpy(numpy_image).permute(2, 0, 1)
return torch_image
from collections import Counter
def create_square_image(image_list):
"""
Creates a square image by combining multiple images in a grid pattern.
Args:
image_list (list): List of PIL Image objects to be combined.
Returns:
PIL Image object: The resulting square image.
None: If the image_list is empty or contains only one image.
"""
# Count the occurrences of each image size in the image_list
size_counter = Counter(image.size for image in image_list)
# Get the most common image size (size with the highest count)
common_size = size_counter.most_common(1)[0][0]
# Filter the image_list to include only images with the common size
image_list = [image for image in image_list if image.size == common_size]
# Get the dimensions (width and height) of the common size
size = common_size
# If there are more than one image in the image_list
if len(image_list) > 1:
num_images = len(image_list)
# Calculate the number of rows and columns for the grid
rows = isqrt(num_images)
cols = ceil(num_images / rows)
# Calculate the size of the square image
square_size = (cols * size[0], rows * size[1])
# Create a new RGB image with the square size
square_image = Image.new("RGB", square_size)
# Paste each image onto the square image at the appropriate position
for i, image in enumerate(image_list):
row = i // cols
col = i % cols
square_image.paste(image, (col * size[0], row * size[1]))
# Return the resulting square image
return square_image
# Return None if there are no images or only one image in the image_list
return None
def create_mask(image, box_coords):
width, height = image.size
mask = Image.new("L", (width, height), 255)
x1, y1, x2, y2 = box_coords
for x in range(width):
for y in range(height):
if x1 <= x <= x2 and y1 <= y <= y2:
mask.putpixel((x, y), 255)
else:
mask.putpixel((x, y), 0)
return mask
def apply_mask(img : Image.Image,p : processing.StableDiffusionProcessing, batch_index : int) -> Image.Image :
"""
Apply mask overlay and color correction to an image if enabled
Args:
img: PIL Image objects.
p : The processing object
batch_index : the batch index
Returns:
PIL Image object
"""
if isinstance(p, processing.StableDiffusionProcessingImg2Img) :
if p.inpaint_full_res :
overlays = p.overlay_images
if overlays is None or batch_index >= len(overlays):
return img
overlay : Image.Image = overlays[batch_index]
overlay = overlay.resize((img.size), resample= Image.Resampling.LANCZOS)
img = img.copy()
img.paste(overlay, (0, 0), overlay)
return img
img = processing.apply_overlay(img, p.paste_to, batch_index, p.overlay_images)
if p.color_corrections is not None and batch_index < len(p.color_corrections):
img = processing.apply_color_correction(p.color_corrections[batch_index], img)
return img
def prepare_mask(
mask: Image.Image, p: processing.StableDiffusionProcessing
) -> Image.Image:
"""
Prepare an image mask for the inpainting process. (This comes from controlnet)
This function takes as input a PIL Image object and an instance of the
StableDiffusionProcessing class, and performs the following steps to prepare the mask:
1. Convert the mask to grayscale (mode "L").
2. If the 'inpainting_mask_invert' attribute of the processing instance is True,
invert the mask colors.
3. If the 'mask_blur' attribute of the processing instance is greater than 0,
apply a Gaussian blur to the mask with a radius equal to 'mask_blur'.
Args:
mask (Image.Image): The input mask as a PIL Image object.
p (processing.StableDiffusionProcessing): An instance of the StableDiffusionProcessing class
containing the processing parameters.
Returns:
mask (Image.Image): The prepared mask as a PIL Image object.
"""
mask = mask.convert("L")
#FIXME : Properly fix blur
# if getattr(p, "mask_blur", 0) > 0:
# mask = mask.filter(ImageFilter.GaussianBlur(p.mask_blur))
return mask
def base64_to_pil(base64str : Optional[str]) -> Optional[Image.Image] :
if base64str is None :
return None
if 'base64,' in base64str: # check if the base64 string has a data URL scheme
base64_data = base64str.split('base64,')[-1]
img_bytes = base64.b64decode(base64_data)
else:
# if no data URL scheme, just decode
img_bytes = base64.b64decode(base64str)
return Image.open(io.BytesIO(img_bytes))
+55
View File
@@ -0,0 +1,55 @@
import glob
import os
import modules.scripts as scripts
from modules import scripts
from scripts.faceswaplab_globals import EXTENSION_PATH
from modules.shared import opts
from scripts.faceswaplab_utils.faceswaplab_logging import logger
def get_models():
"""
Retrieve a list of swap model files.
This function searches for model files in the specified directories and returns a list of file paths.
The supported file extensions are ".onnx".
Returns:
A list of file paths of the model files.
"""
models_path = os.path.join(scripts.basedir(), EXTENSION_PATH, "models", "*")
models = glob.glob(models_path)
# Add an additional models directory and find files in it
models_path = os.path.join(scripts.basedir(), "models", "faceswaplab", "*")
models += glob.glob(models_path)
# Filter the list to include only files with the supported extensions
models = [x for x in models if x.endswith(".onnx")]
return models
def get_current_model() -> str :
model = opts.data.get("faceswaplab_model", None)
if model is None :
models = get_models()
model = models[0] if len(models) else None
logger.info("Try to use model : %s", model)
if not os.path.isfile(model):
logger.error("The model %s cannot be found or loaded", model)
raise FileNotFoundError("No faceswap model found. Please add it to the faceswaplab directory.")
return model
def get_face_checkpoints():
"""
Retrieve a list of face checkpoint paths.
This function searches for face files with the extension ".pkl" in the specified directory and returns a list
containing the paths of those files.
Returns:
list: A list of face paths, including the string "None" as the first element.
"""
faces_path = os.path.join(scripts.basedir(), "models", "faceswaplab", "faces", "*.pkl")
faces = glob.glob(faces_path)
return ["None"] + faces