21 Commits

Author SHA1 Message Date
Tran Xen 38f46d3f01 add seed selection 2023-08-24 23:56:03 +02:00
Tran Xen 2e718ee4ae fix default settings by not marking non managed fields as do_not_save 2023-08-24 19:20:31 +02:00
Tran Xen d0c56ae6ef remove override_settings in inpainting i2i_pp.py as they are not supported in some cases, i.e. sdnext for return_mask_composite and cause bugs 2023-08-24 15:47:24 +02:00
Tran Xen 54adcc0fc6 fix install 2023-08-24 09:54:57 +02:00
Tran Xen b11037173b change model link 2023-08-23 18:00:08 +02:00
Tran Xen 6ba60f6332 speed up ui, BREAKING : see changelog 2023-08-23 14:16:03 +02:00
Tran Xen 6d69f69509 speed up ui, BREAKING : see changelog 2023-08-23 14:14:21 +02:00
Tran Xen 7830eb1da9 change model url 2023-08-21 22:21:55 +02:00
Tran Xen aba00ba381 remove javascript, use https://github.com/w-e-w/sdwebui-close-confirmation-dialogue.git instead 2023-08-17 23:46:33 +02:00
Tran Xen 04a0a5c46c fix bug in improved mask 2023-08-17 10:41:04 +02:00
Tran Xen 612cc43752 Merge pull request #46 from glucauze/v1.2.2
V1.2.2 : Install speed fix and nsfw filter option
2023-08-16 23:58:22 +02:00
Tran Xen 5b19333968 update doc 2023-08-16 23:33:18 +02:00
Tran Xen 50db415069 update doc 2023-08-16 18:38:15 +02:00
Tran Xen 61fc9269ed makes GPU requirements default if CPU --use-cpu options is not used, remove faceswaplab_gpu 2023-08-16 18:25:09 +02:00
Tran Xen 0499581305 fix preview in build 2023-08-16 17:07:48 +02:00
Tran Xen afcfc7d255 wip, add nsfw option due to perf, still some mypy warnings 2023-08-16 01:11:02 +02:00
Tran Xen f9fc0bbff1 fix install wip 2023-08-15 14:58:10 +02:00
Tran Xen 054f693815 speed up install and config 2023-08-15 01:17:26 +02:00
Tran Xen 17db6ea2b4 add error reporting in dataclass_from_flat_list 2023-08-14 22:22:48 +02:00
Tran Xen bfb4578c8b Merge pull request #37 from glucauze/v1.2.1
enforce package install, less efficient but more robust
2023-08-07 12:22:05 +02:00
Tran Xen 0a9d992544 enforce package install, less efficient but more robust 2023-08-07 12:10:20 +02:00
39 changed files with 539 additions and 386 deletions
+5 -5
View File
@@ -1,8 +1,4 @@
repos:
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
@@ -10,4 +6,8 @@ repos:
- id: check-case-conflict
- id: check-docstring-first
- id: detect-private-key
- id: fix-byte-order-marker
- id: fix-byte-order-marker
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
+26
View File
@@ -1,3 +1,29 @@
# 1.2.5
Allow seed selection in inpainting.
# 1.2.4
Fix default settings by marking only managed field as do_not_save.
See the discussion here : https://github.com/glucauze/sd-webui-faceswaplab/issues/62
# 1.2.3
Speed up ui : change the way default settings are manage by not storing them in ui-config.json
Migration : YOU NEED TO recreate ui-config.json (delete) or at least remove any faceswaplab reference to be able to use default settings again.
See this for explainations : https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/6109
# 1.2.2
+ Add NSFW filter option in settings (1 == disable)
+ Improve install speed
+ Install gpu requirements by default if --use-cpu is not used
+ Fix improved mask + color correction
+ Remove javascript, use https://github.com/w-e-w/sdwebui-close-confirmation-dialogue.git instead to prevent gradio from closing.
# 1.2.1 :
Add GPU support option : see https://github.com/glucauze/sd-webui-faceswaplab/pull/24
+40 -3
View File
@@ -1,6 +1,12 @@
# FaceSwapLab for a1111/Vlad
Please read the documentation here : https://glucauze.github.io/sd-webui-faceswaplab/
V1.2.3 : Breaking change for settings, please read changelog.
Please read the documentation here : https://glucauze.github.io/sd-webui-faceswaplab/
You can also read the [doc discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions/categories/guide-doc)
See [CHANGELOG.md](CHANGELOG.md) for changes in last versions.
FaceSwapLab is an extension for Stable Diffusion that simplifies face-swapping. It has evolved from sd-webui-faceswap and some part of sd-webui-roop. However, a substantial amount of the code has been rewritten to improve performance and to better manage masks.
@@ -14,7 +20,7 @@ While FaceSwapLab is still under development, it has reached a good level of sta
In short:
+ **Ethical Guideline:** This extension should not be forked to create a public, easy way to bypass NSFW filtering. If you modify it for this purpose, keep it private, or you'll be banned.
+ **Ethical Guideline:** NSFW is now configurable due to performance issue. Please don't use this to do harm.
+ **License:** This software is distributed under the terms of the GNU Affero General Public License (AGPL), version 3 or later.
+ **Model License:** This software uses InsightFace's pre-trained models, which are available for non-commercial research purposes only.
@@ -24,6 +30,35 @@ More on this here : https://glucauze.github.io/sd-webui-faceswaplab/
+ Older versions of gradio don't work well with the extension. See this bug : https://github.com/glucauze/sd-webui-faceswaplab/issues/5
## Quick Start
### Simple
1. Put a face in the reference.
2. Select a face number.
3. Select "Enable."
4. Select "CodeFormer" in **Global Post-Processing** tab.
Once you're happy with some results but want to improve, the next steps are to:
+ Use advanced settings in face units (which are not as complex as they might seem, it's basically fine tuning post-processing for each faces).
+ Use pre/post inpainting to tweak the image a bit for more natural results.
### Better
1. Put a face in the reference.
2. Select a face number.
3. Select "Enable."
4. In **Post-Processing** accordeon:
+ Select "CodeFormer"
+ Select "LDSR" or a faster model "003_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR-L_x4_GAN" in upscaler. See [here for a list of upscalers](https://github.com/glucauze/sd-webui-faceswaplab/discussions/29).
+ Use sharpen, color_correction and improved mask
5. Disable "CodeFormer" in **Global Post-Processing** tab (otherwise it will be applied twice)
Don't hesitate to share config in the [discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions).
### Features
+ **Face Unit Concept**: Similar to controlNet, the program introduces the concept of a face unit. You can configure up to 10 units (3 units are the default setting) in the program settings (sd).
@@ -32,6 +67,8 @@ More on this here : https://glucauze.github.io/sd-webui-faceswaplab/
+ **Batch Processing**
+ **GPU**
+ **Inpainting Fixes** : supports “only masked” and mask inpainting.
+ **Performance Improvements**: The overall performance of the software has been enhanced.
@@ -62,7 +99,7 @@ More on this here : https://glucauze.github.io/sd-webui-faceswaplab/
+ **Upscaled Inswapper**: The program now includes an upscaled inswapper option, which improves results by incorporating upsampling, sharpness adjustment, and color correction before face is merged to the original image.
+ **API with typing support** :
+ **API with typing support**
## Installation
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
autoflake --in-place --remove-unused-variables -r --remove-all-unused-imports .
mypy --install-types
mypy --non-interactive --install-types
pre-commit run --all-files
+1
View File
@@ -48,6 +48,7 @@ class InpaintingOptions(BaseModel):
inpainting_model: str = Field(
description="Inpainting model", examples=["Current"], default="Current"
)
inpainting_seed: int = Field(description="Inpainting Seed", ge=-1, default=-1)
class InswappperOptions(BaseModel):
Binary file not shown.
+17
View File
@@ -5,6 +5,8 @@ permalink: /doc/
toc: true
---
You can also read the [doc discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions/categories/guide-doc)
## TLDR: I Just Want Good Results:
1. Put a face in the reference.
@@ -17,6 +19,21 @@ Once you're happy with some results but want to improve, the next steps are to:
+ Use advanced settings in face units (which are not as complex as they might seem, it's basically fine tuning post-processing for each faces).
+ Use pre/post inpainting to tweak the image a bit for more natural results.
### Getting better results
1. Put a face in the reference.
2. Select a face number.
3. Select "Enable."
4. In **Post-Processing** accordeon:
+ Select "CodeFormer"
+ Select "LDSR" or a faster model "003_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR-L_x4_GAN" in upscaler. See [here for a list of upscalers](https://github.com/glucauze/sd-webui-faceswaplab/discussions/29).
+ Use sharpen, color_correction and improved mask
5. Disable "CodeFormer" in **Global Post-Processing** tab (otherwise it will be applied twice)
Don't hesitate to share config in the [discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions).
## Main Interface
Here is the interface for FaceSwap Lab. It is available in the form of an accordion in both img2img and txt2img.
+1 -3
View File
@@ -20,7 +20,7 @@ While FaceSwapLab is still under development, it has reached a good level of sta
In short:
+ **Ethical Guideline:** This extension should not be forked to create a public, easy way to circumvent NSFW filtering.
+ **Ethical Guideline:** This extension is **not intended to facilitate the creation of not safe for work (NSFW) or non-consensual deepfake content**. Its purpose is to bring consistency to image creation, making it easier to repair existing images, or bring characters back to life.
+ **License:** This software is distributed under the terms of the GNU Affero General Public License (AGPL), version 3 or later.
+ **Model License:** This software uses InsightFace's pre-trained models, which are available for non-commercial research purposes only.
@@ -28,8 +28,6 @@ In short:
This extension is **not intended to facilitate the creation of not safe for work (NSFW) or non-consensual deepfake content**. Its purpose is to bring consistency to image creation, making it easier to repair existing images, or bring characters back to life.
While the code for this extension is licensed under the AGPL in compliance with models and other source materials, it's important to stress that **we strongly discourage any attempts to fork this project to create an uncensored version**. Any modifications to the code to enable the production of such content would be contrary to the ethical guidelines we advocate for.
We will comply with European regulations regarding this type of software. As required by law, the code may include both visible and invisible watermarks. If your local laws prohibit the use of this extension, you should not use it.
From an ethical perspective, the main goal of this extension is to generate consistent images by swapping faces. It's important to note that we've done our best to integrate censorship features. However, when users can access the source code, they might bypass these censorship measures. That's why we urge users to use this extension responsibly and avoid any malicious use. We emphasize the importance of respecting people's privacy and consent when swapping faces in images. We discourage any activities that could harm others, invade their privacy, or negatively affect their well-being.
+18 -7
View File
@@ -2,20 +2,26 @@ import launch
import os
import sys
import pkg_resources
from modules import shared
from packaging.version import parse
def check_install() -> None:
use_gpu = getattr(
shared.cmd_opts, "faceswaplab_gpu", False
) or shared.opts.data.get("faceswaplab_use_gpu", False)
use_gpu = True
try:
from modules import shared
use_gpu = not getattr(shared.cmd_opts, "use-cpu", False)
except:
# On some platform previous lines may failed (modules.shared not initialized), just ignore and use GPU requirements
pass
if use_gpu and sys.platform != "darwin":
print("Faceswaplab : Use GPU requirements")
req_file = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "requirements-gpu.txt"
)
else:
print("Faceswaplab : Use CPU requirements")
req_file = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "requirements.txt"
)
@@ -36,7 +42,9 @@ def check_install() -> None:
required_version = parse(package.split(">=")[1])
return installed_version >= required_version
else:
return True
if package_name == "opencv-python":
return launch.is_installed(package_name) or launch.is_installed("cv2")
return launch.is_installed(package_name)
print("Checking faceswaplab requirements")
with open(req_file) as file:
@@ -54,9 +62,12 @@ def check_install() -> None:
except Exception as e:
print(e)
print(
f"Warning: Failed to install {package}, faceswaplab will not work."
f"Warning: Failed to install {package}, faceswaplab may not work. Try to restart server or install dependencies manually."
)
raise e
check_install()
import timeit
check_time = timeit.timeit(check_install, number=1)
print(check_time)
-4
View File
@@ -1,4 +0,0 @@
window.onbeforeunload = function() {
// Prevent the stable diffusion window from being closed by mistake
return "Are you sure ?";
};
+1
View File
@@ -0,0 +1 @@
[{"analyzerName":"intellisense-members-lstm-pylance","languageName":"python","identity":{"modelId":"E61945A9A512ED5E1A3EE3F1A2365B88F8FE","outputId":"E4E9EADA96734F01970E616FAB2FAC19","modifiedTimeUtc":"2020-08-11T14:06:50.811Z"},"filePath":"E61945A9A512ED5E1A3EE3F1A2365B88F8FE_E4E9EADA96734F01970E616FAB2FAC19","lastAccessTimeUtc":"2023-08-14T21:58:14.988Z"}]
-5
View File
@@ -8,8 +8,3 @@ def preload(parser: ArgumentParser) -> None:
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
parser.add_argument(
"--faceswaplab_gpu",
action="store_true",
help="Enable GPU if set, disable if not set",
)
+1
View File
@@ -3,6 +3,7 @@ dill
ifnude
insightface==0.7.3
onnx>=1.14.0
protobuf>=3.20.2
opencv-python
pandas
pydantic
+1
View File
@@ -1,3 +1,4 @@
protobuf>=3.20.2
cython
dill
ifnude
+3 -45
View File
@@ -1,51 +1,15 @@
import os
from tqdm import tqdm
import traceback
import urllib.request
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from scripts.faceswaplab_globals import *
from packaging import version
import pkg_resources
import hashlib
from scripts.faceswaplab_utils.models_utils import check_model
ALREADY_DONE = False
def check_install() -> None:
# Very ugly hack :( due to sdnext optimization not calling install.py every time if git log has not changed
import importlib.util
import sys
import os
current_dir = os.path.dirname(os.path.realpath(__file__))
check_install_path = os.path.join(current_dir, "..", "install.py")
spec = importlib.util.spec_from_file_location("check_install", check_install_path)
check_install = importlib.util.module_from_spec(spec)
sys.modules["check_install"] = check_install
spec.loader.exec_module(check_install)
check_install.check_install() # type: ignore
#### End of ugly hack :( !
def is_sha1_matching(file_path: str, expected_sha1: str) -> bool:
sha1_hash = hashlib.sha1(usedforsecurity=False)
try:
with open(file_path, "rb") as file:
for byte_block in iter(lambda: file.read(4096), b""):
sha1_hash.update(byte_block)
if sha1_hash.hexdigest() == expected_sha1:
return True
else:
return False
except Exception as e:
logger.error(
"Failed to check model hash, check the model is valid or has been downloaded adequately : %e",
e,
)
traceback.print_exc()
return False
def check_configuration() -> None:
global ALREADY_DONE
@@ -56,7 +20,7 @@ def check_configuration() -> None:
models_dir = MODELS_DIR
faces_dir = FACES_DIR
model_url = "https://huggingface.co/henryruhs/roop/resolve/main/inswapper_128.onnx"
model_url = "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx"
model_name = os.path.basename(model_url)
model_path = os.path.join(models_dir, model_name)
@@ -83,13 +47,7 @@ def check_configuration() -> None:
if not os.path.exists(model_path):
download(model_url, model_path)
if not is_sha1_matching(model_path, EXPECTED_INSWAPPER_SHA1):
logger.error(
"Suspicious sha1 for model %s, check the model is valid or has been downloaded adequately. Should be %s",
model_path,
EXPECTED_INSWAPPER_SHA1,
)
check_model()
gradio_version = pkg_resources.get_distribution("gradio").version
+16 -12
View File
@@ -1,4 +1,5 @@
from scripts.configure import check_configuration
from scripts.faceswaplab_utils.sd_utils import get_sd_option
check_configuration()
@@ -12,7 +13,7 @@ from scripts.faceswaplab_settings import faceswaplab_settings
from scripts.faceswaplab_swapping import swapper
from scripts.faceswaplab_ui import faceswaplab_tab, faceswaplab_unit_ui
from scripts.faceswaplab_utils import faceswaplab_logging, imgutils, models_utils
from scripts.faceswaplab_utils.models_utils import get_current_model
from scripts.faceswaplab_utils.models_utils import get_current_swap_model
from scripts.faceswaplab_utils.typing import *
from scripts.faceswaplab_utils.ui_utils import dataclasses_from_flat_list
from scripts.faceswaplab_utils.faceswaplab_logging import logger, save_img_debug
@@ -76,7 +77,7 @@ class FaceSwapScript(scripts.Script):
@property
def units_count(self) -> int:
return opts.data.get("faceswaplab_units_count", 3)
return get_sd_option("faceswaplab_units_count", 3)
@property
def enabled(self) -> bool:
@@ -85,7 +86,7 @@ class FaceSwapScript(scripts.Script):
@property
def keep_original_images(self) -> bool:
return opts.data.get("faceswaplab_keep_original", False)
return get_sd_option("faceswaplab_keep_original", False)
@property
def swap_in_generated_units(self) -> List[FaceSwapUnitSettings]:
@@ -99,7 +100,7 @@ class FaceSwapScript(scripts.Script):
return f"faceswaplab"
def show(self, is_img2img: bool) -> bool:
return scripts.AlwaysVisible
return scripts.AlwaysVisible # type: ignore
def ui(self, is_img2img: bool) -> List[gr.components.Component]:
with gr.Accordion(f"FaceSwapLab {VERSION_FLAG}", open=False):
@@ -108,7 +109,10 @@ class FaceSwapScript(scripts.Script):
components += faceswaplab_unit_ui.faceswap_unit_ui(is_img2img, i)
post_processing = faceswaplab_tab.postprocessing_ui()
# If the order is modified, the before_process should be changed accordingly.
return components + post_processing
components = components + post_processing
return components
def read_config(
self, p: StableDiffusionProcessing, *components: Tuple[Any, ...]
@@ -147,7 +151,7 @@ class FaceSwapScript(scripts.Script):
(img, None) for img in p.init_images
]
new_inits = swapper.process_images_units(
get_current_model(),
get_current_swap_model(),
self.swap_in_source_units,
images=init_images,
force_blend=True,
@@ -181,7 +185,7 @@ class FaceSwapScript(scripts.Script):
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(),
get_current_swap_model(),
self.swap_in_generated_units,
images=[(img, info)],
)
@@ -213,8 +217,8 @@ class FaceSwapScript(scripts.Script):
swp_img,
p.outpath_samples,
"",
p.all_seeds[batch_index],
p.all_prompts[batch_index],
p.all_seeds[batch_index], # type: ignore
p.all_prompts[batch_index], # type: ignore
opts.samples_format,
info=new_info,
p=p,
@@ -231,7 +235,7 @@ class FaceSwapScript(scripts.Script):
text = processed.infotexts[0]
infotexts.insert(0, text)
if opts.enable_pnginfo:
grid.info["parameters"] = text
grid.info["parameters"] = text # type: ignore
images.insert(0, grid)
if opts.grid_save:
@@ -239,8 +243,8 @@ class FaceSwapScript(scripts.Script):
grid,
p.outpath_grids,
"swapped-grid",
p.all_seeds[0],
p.all_prompts[0],
p.all_seeds[0], # type: ignore
p.all_prompts[0], # type: ignore
opts.grid_format,
info=text,
short_filename=not opts.grid_extended_filename,
+3 -2
View File
@@ -18,9 +18,10 @@ from scripts.faceswaplab_postprocessing.postprocessing_options import (
PostProcessingOptions,
)
from client_api import api_utils
from scripts.faceswaplab_utils.face_checkpoints_utils import (
from scripts.faceswaplab_swapping.face_checkpoints import (
build_face_checkpoint_and_save,
)
from scripts.faceswaplab_utils.typing import PILImage
def encode_to_base64(image: Union[str, Image.Image, np.ndarray]) -> str: # type: ignore
@@ -99,7 +100,7 @@ def faceswaplab_api(_: gr.Blocks, app: FastAPI) -> None:
pp_options = None
units = get_faceswap_units_settings(request.units)
swapped_images = swapper.batch_process(
swapped_images: Optional[List[PILImage]] = swapper.batch_process(
[src_image], None, units=units, postprocess_options=pp_options
)
+9 -3
View File
@@ -1,18 +1,24 @@
import os
from modules import scripts
# Defining the absolute path for the 'faceswaplab' directory inside 'models' directory
MODELS_DIR = os.path.abspath(os.path.join("models", "faceswaplab"))
# Defining the absolute path for the 'analysers' directory inside 'MODELS_DIR'
ANALYZER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "analysers"))
# Defining the absolute path for the 'parser' directory inside 'MODELS_DIR'
FACE_PARSER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "parser"))
# Defining the absolute path for the 'faces' directory inside 'MODELS_DIR'
FACES_DIR = os.path.abspath(os.path.join(MODELS_DIR, "faces"))
# Constructing the path for 'references' directory inside the 'extensions' and 'sd-webui-faceswaplab' directories, based on the base directory of scripts
REFERENCE_PATH = os.path.join(
scripts.basedir(), "extensions", "sd-webui-faceswaplab", "references"
)
VERSION_FLAG: str = "v1.2.1"
# Defining the version flag for the application
VERSION_FLAG: str = "v1.2.5"
# Defining the path for 'sd-webui-faceswaplab' inside the 'extensions' directory
EXTENSION_PATH = os.path.join("extensions", "sd-webui-faceswaplab")
# The NSFW score threshold. If any part of the image has a score greater than this threshold, the image will be considered NSFW.
NSFW_SCORE_THRESHOLD: float = 0.7
# Defining the expected SHA1 hash value for 'INSWAPPER'
EXPECTED_INSWAPPER_SHA1 = "17a64851eaefd55ea597ee41e5c18409754244c5"
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List
from typing import List, Optional
import gradio as gr
from client_api import api_utils
@@ -12,13 +12,14 @@ class InpaintingOptions:
inpainting_steps: int = 20
inpainting_sampler: str = "Euler"
inpainting_model: str = "Current"
inpainting_seed: int = -1
@staticmethod
def from_gradio(components: List[gr.components.Component]) -> "InpaintingOptions":
return InpaintingOptions(*components)
return InpaintingOptions(*components) # type: ignore
@staticmethod
def from_api_dto(dto: api_utils.InpaintingOptions) -> "InpaintingOptions":
def from_api_dto(dto: Optional[api_utils.InpaintingOptions]) -> "InpaintingOptions":
"""
Converts a InpaintingOptions object from an API DTO (Data Transfer Object).
@@ -38,4 +39,5 @@ class InpaintingOptions:
inpainting_steps=dto.inpainting_steps,
inpainting_sampler=dto.inpainting_sampler,
inpainting_model=dto.inpainting_model,
inpainting_seed=dto.inpainting_seed,
)
+12 -9
View File
@@ -63,16 +63,19 @@ inpainting_steps : {options.inpainting_steps}
"prompt": prompt,
"negative_prompt": negative_prompt,
"denoising_strength": options.inpainting_denoising_strengh,
"override_settings": {
"return_mask_composite": False,
"save_images_before_face_restoration": False,
"save_images_before_highres_fix": False,
"save_images_before_color_correction": False,
"save_mask": False,
"save_mask_composite": False,
"samples_save": False,
},
"seed": options.inpainting_seed,
}
# Remove the following as they are not always supported on all platform :
# "override_settings": {
# "return_mask_composite": False,
# "save_images_before_face_restoration": False,
# "save_images_before_highres_fix": False,
# "save_images_before_color_correction": False,
# "save_mask": False,
# "save_mask_composite": False,
# "samples_save": False,
# },
current_model_checkpoint = shared.opts.sd_model_checkpoint
if options.inpainting_model and options.inpainting_model != "Current":
# Change checkpoint
@@ -1,3 +1,4 @@
from typing import Optional
from modules.face_restoration import FaceRestoration
from modules.upscaler import UpscalerData
from dataclasses import dataclass
@@ -27,17 +28,17 @@ class PostProcessingOptions:
inpainting_when: InpaintingWhen = InpaintingWhen.BEFORE_UPSCALING
# (Don't use optional for this or gradio parsing will fail) :
inpainting_options: InpaintingOptions = None
inpainting_options: InpaintingOptions = None # type: ignore
@property
def upscaler(self) -> UpscalerData:
def upscaler(self) -> Optional[UpscalerData]:
for upscaler in shared.sd_upscalers:
if upscaler.name == self.upscaler_name:
return upscaler
return None
@property
def face_restorer(self) -> FaceRestoration:
def face_restorer(self) -> Optional[FaceRestoration]:
for face_restorer in shared.face_restorers:
if face_restorer.name() == self.face_restorer_name:
return face_restorer
@@ -17,7 +17,7 @@ def upscale_img(image: PILImage, pp_options: PostProcessingOptions) -> PILImage:
pp_options.scale,
)
result_image = pp_options.upscaler.scaler.upscale(
image, pp_options.scale, pp_options.upscaler.data_path
image, pp_options.scale, pp_options.upscaler.data_path # type: ignore
)
# FIXME : Could be better (managing images whose dimensions are not multiples of 16)
@@ -1,11 +1,11 @@
from scripts.faceswaplab_utils.models_utils import get_models
from scripts.faceswaplab_utils.models_utils import get_swap_models
from modules import script_callbacks, shared
import gradio as gr
def on_ui_settings() -> None:
section = ("faceswaplab", "FaceSwapLab")
models = get_models()
models = get_swap_models()
shared.opts.add_option(
"faceswaplab_model",
shared.OptionInfo(
@@ -46,6 +46,16 @@ def on_ui_settings() -> None:
section=section,
),
)
shared.opts.add_option(
"faceswaplab_nsfw_threshold",
shared.OptionInfo(
0.7,
"NSFW score threshold. Any image part with a score above this value will be treated as NSFW (use extension responsibly !). 1=Disable filtering",
gr.Slider,
{"minimum": 0, "maximum": 1, "step": 0.01},
section=section,
),
)
shared.opts.add_option(
"faceswaplab_det_size",
@@ -11,7 +11,7 @@ from scripts.faceswaplab_swapping.upcaled_inswapper_options import InswappperOpt
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from scripts.faceswaplab_utils.typing import *
from scripts.faceswaplab_utils import imgutils
from scripts.faceswaplab_utils.models_utils import get_models
from scripts.faceswaplab_utils.models_utils import get_swap_models
import traceback
import dill as pickle # will be removed in future versions
@@ -38,8 +38,11 @@ def sanitize_name(name: str) -> str:
def build_face_checkpoint_and_save(
images: List[PILImage], name: str, overwrite: bool = False, path: str = None
) -> PILImage:
images: List[PILImage],
name: str,
overwrite: bool = False,
path: Optional[str] = None,
) -> Optional[PILImage]:
"""
Builds a face checkpoint using the provided image files, performs face swapping,
and saves the result to a file. If a blended face is successfully obtained and the face swapping
@@ -57,13 +60,17 @@ def build_face_checkpoint_and_save(
name = sanitize_name(name)
images = images or []
logger.info("Build %s with %s images", name, len(images))
faces = swapper.get_faces_from_img_files(images)
blended_face = swapper.blend_faces(faces)
faces: List[Face] = swapper.get_faces_from_img_files(images=images)
if faces is None or len(faces) == 0:
logger.error("No source faces found")
return None
blended_face: Optional[Face] = swapper.blend_faces(faces)
preview_path = os.path.join(
scripts.basedir(), "extensions", "sd-webui-faceswaplab", "references"
)
reference_preview_img: PILImage = None
reference_preview_img: PILImage
if blended_face:
if blended_face["gender"] == 0:
reference_preview_img = Image.open(
@@ -85,41 +92,51 @@ def build_face_checkpoint_and_save(
"Failed to open reference image, cannot create preview : That should not happen unless you deleted the references folder or change the detection threshold."
)
else:
result = swapper.swap_face(
reference_face=blended_face,
result: swapper.ImageResult = swapper.swap_face(
target_faces=[target_face],
source_face=blended_face,
target_img=reference_preview_img,
model=get_models()[0],
swapping_options=InswappperOptions(face_restorer_name="Codeformer"),
model=get_swap_models()[0],
swapping_options=InswappperOptions(
face_restorer_name="CodeFormer",
restorer_visibility=1,
upscaler_name="Lanczos",
codeformer_weight=1,
improved_mask=True,
color_corrections=False,
sharpen=True,
),
)
preview_image = result.image
if path:
file_path = path
else:
file_path = os.path.join(get_checkpoint_path(), f"{name}.safetensors")
if not overwrite:
file_number = 1
while os.path.exists(file_path):
file_path = os.path.join(
get_checkpoint_path(), f"{name}_{file_number}.safetensors"
)
file_number += 1
save_face(filename=file_path, face=blended_face)
preview_image.save(file_path + ".png")
try:
data = load_face(file_path)
logger.debug(data)
except Exception as e:
logger.error("Error loading checkpoint, after creation %s", e)
traceback.print_exc()
if path:
file_path = path
else:
file_path = os.path.join(
get_checkpoint_path(), f"{name}.safetensors"
)
if not overwrite:
file_number = 1
while os.path.exists(file_path):
file_path = os.path.join(
get_checkpoint_path(),
f"{name}_{file_number}.safetensors",
)
file_number += 1
save_face(filename=file_path, face=blended_face)
preview_image.save(file_path + ".png")
try:
data = load_face(file_path)
logger.debug(data)
except Exception as e:
logger.error("Error loading checkpoint, after creation %s", e)
traceback.print_exc()
return preview_image
return preview_image
else:
logger.error("No face found")
return None
return None # type: ignore
except Exception as e:
logger.error("Failed to build checkpoint %s", e)
traceback.print_exc()
@@ -140,7 +157,7 @@ def save_face(face: Face, filename: str) -> None:
raise e
def load_face(name: str) -> Face:
def load_face(name: str) -> Optional[Face]:
if name.startswith("data:application/face;base64,"):
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
api_utils.base64_to_safetensors(name, temp_file.name)
+1 -1
View File
@@ -83,7 +83,7 @@ def generate_face_mask(face_image: np.ndarray, device: torch.device) -> np.ndarr
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)
normalize(face_input, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) # type: ignore
assert isinstance(face_input, torch.Tensor)
face_input = torch.unsqueeze(face_input, 0).to(device)
+42 -35
View File
@@ -26,24 +26,24 @@ from scripts.faceswaplab_utils.imgutils import (
)
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
from scripts.faceswaplab_postprocessing.postprocessing import enhance_image
from scripts.faceswaplab_postprocessing.postprocessing_options import (
PostProcessingOptions,
)
from scripts.faceswaplab_utils.models_utils import get_current_model
from scripts.faceswaplab_utils.models_utils import get_current_swap_model
from scripts.faceswaplab_utils.typing import CV2ImgU8, PILImage, Face
from scripts.faceswaplab_inpainting.i2i_pp import img2img_diffusion
from modules import shared
import onnxruntime
from scripts.faceswaplab_utils.sd_utils import get_sd_option
def use_gpu() -> bool:
return (
getattr(shared.cmd_opts, "faceswaplab_gpu", False)
or opts.data.get("faceswaplab_use_gpu", False)
or get_sd_option("faceswaplab_use_gpu", False)
) and sys.platform != "darwin"
@@ -51,7 +51,7 @@ def use_gpu() -> bool:
def force_install_gpu_providers() -> None:
# Ugly Ugly hack due to SDNEXT :
try:
from scripts.configure import check_install
from scripts.faceswaplab_utils.install_utils import check_install
logger.warning("Try to reinstall gpu dependencies")
check_install()
@@ -101,8 +101,9 @@ def cosine_similarity_face(face1: Face, face2: Face) -> float:
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)
assert face1.normed_embedding is not None and face2.normed_embedding is not None
vec1 = face1.normed_embedding.reshape(1, -1)
vec2 = face2.normed_embedding.reshape(1, -1)
# Calculate the cosine similarity between the reshaped embeddings
similarity = cosine_similarity(vec1, vec2)
@@ -141,7 +142,7 @@ def batch_process(
src_images: List[Union[PILImage, str]], # image or filename
save_path: Optional[str],
units: List[FaceSwapUnitSettings],
postprocess_options: PostProcessingOptions,
postprocess_options: Optional[PostProcessingOptions],
) -> Optional[List[PILImage]]:
"""
Process a batch of images, apply face swapping according to the given settings, and optionally save the resulting images to a specified path.
@@ -166,6 +167,7 @@ def batch_process(
if src_images is not None and len(units) > 0:
result_images = []
for src_image in src_images:
path: str = ""
if isinstance(src_image, str):
if save_path:
path = os.path.join(
@@ -180,9 +182,9 @@ def batch_process(
current_images = []
swapped_images = process_images_units(
get_current_model(), images=[(src_image, None)], units=units
get_current_swap_model(), images=[(src_image, None)], units=units
)
if len(swapped_images) > 0:
if swapped_images and len(swapped_images) > 0:
current_images += [img for img, _ in swapped_images]
logger.info("%s images generated", len(current_images))
@@ -209,7 +211,7 @@ def extract_faces(
images: List[PILImage],
extract_path: Optional[str],
postprocess_options: PostProcessingOptions,
) -> Optional[List[str]]:
) -> Optional[List[PILImage]]:
"""
Extracts faces from a list of image files.
@@ -232,14 +234,14 @@ def extract_faces(
os.makedirs(extract_path, exist_ok=True)
if images:
result_images = []
result_images: list[PILImage] = []
for img in images:
faces = get_faces(pil_to_cv2(img))
if faces:
face_images = []
for face in faces:
bbox = face.bbox.astype(int)
bbox = face.bbox.astype(int) # type: ignore
x_min, y_min, x_max, y_max = bbox
face_image = img.crop((x_min, y_min, x_max, y_max))
@@ -311,7 +313,9 @@ def capture_stdout() -> Generator[StringIO, None, None]:
@lru_cache(maxsize=3)
def getAnalysisModel(
det_size: Tuple[int, int] = (640, 640), det_thresh: float = 0.5
det_size: Tuple[int, int] = (640, 640),
det_thresh: float = 0.5,
use_gpu: bool = False,
) -> insightface.app.FaceAnalysis:
"""
Retrieves the analysis model for face analysis.
@@ -355,7 +359,9 @@ def getAnalysisModel(
@lru_cache(maxsize=1)
def getFaceSwapModel(model_path: str) -> upscaled_inswapper.UpscaledINSwapper:
def getFaceSwapModel(
model_path: str, use_gpu: bool = False
) -> upscaled_inswapper.UpscaledINSwapper:
"""
Retrieves the face swap model and initializes it if necessary.
@@ -370,7 +376,7 @@ def getFaceSwapModel(model_path: str) -> upscaled_inswapper.UpscaledINSwapper:
with tqdm(total=1, desc="Loading swap model", unit="model") as pbar:
with capture_stdout() as captured:
model = upscaled_inswapper.UpscaledINSwapper(
insightface.model_zoo.get_model(model_path, providers=providers)
insightface.model_zoo.get_model(model_path, providers=providers) # type: ignore
)
pbar.update(1)
logger.info("%s", pformat(captured.getvalue()))
@@ -402,14 +408,16 @@ def get_faces(
"""
if det_thresh is None:
det_thresh = opts.data.get("faceswaplab_detection_threshold", 0.5)
det_thresh = get_sd_option("faceswaplab_detection_threshold", 0.5)
auto_det_size = opts.data.get("faceswaplab_auto_det_size", True)
auto_det_size = get_sd_option("faceswaplab_auto_det_size", True)
if not auto_det_size:
x = opts.data.get("faceswaplab_det_size", 640)
x = get_sd_option("faceswaplab_det_size", 640)
det_size = (x, x)
face_analyser = getAnalysisModel(det_size, det_thresh)
face_analyser = getAnalysisModel(
det_size=det_size, det_thresh=det_thresh, use_gpu=not is_cpu_provider()
)
# Get the detected faces from the image using the analysis model
faces = face_analyser.get(img_data)
@@ -433,7 +441,7 @@ def get_faces(
try:
# Sort the detected faces based on their x-coordinate of the bounding box
return sorted(faces, key=lambda x: x.bbox[0])
return sorted(faces, key=lambda x: x.bbox[0]) # type: ignore
except Exception as e:
logger.error("Failed to get faces %s", e)
traceback.print_exc()
@@ -470,7 +478,7 @@ def filter_faces(
filtered_faces = sorted(
all_faces,
reverse=True,
key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]),
key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), # type: ignore
)
if filtering_options.source_gender is not None:
@@ -526,7 +534,7 @@ def get_or_default(l: List[Any], index: int, default: Any) -> Any:
return l[index] if index < len(l) else default
def get_faces_from_img_files(images: List[PILImage]) -> List[Optional[CV2ImgU8]]:
def get_faces_from_img_files(images: List[PILImage]) -> List[Face]:
"""
Extracts faces from a list of image files.
@@ -538,7 +546,7 @@ def get_faces_from_img_files(images: List[PILImage]) -> List[Optional[CV2ImgU8]]
"""
faces = []
faces: List[Face] = []
if len(images) > 0:
for img in images:
@@ -566,7 +574,7 @@ def blend_faces(faces: List[Face]) -> Optional[Face]:
ValueError: If the embeddings have different shapes.
"""
embeddings = [face.embedding for face in faces]
embeddings: list[Any] = [face.embedding for face in faces]
if len(embeddings) > 0:
embedding_shape = embeddings[0].shape
@@ -592,19 +600,16 @@ def blend_faces(faces: List[Face]) -> Optional[Face]:
def swap_face(
reference_face: CV2ImgU8,
source_face: Face,
target_img: PILImage,
target_faces: List[Face],
model: str,
swapping_options: Optional[InswappperOptions],
compute_similarity: bool = True,
) -> ImageResult:
"""
Swaps faces in the target image with the source face.
Args:
reference_face (CV2ImgU8): The reference face used for similarity comparison.
source_face (CV2ImgU8): The source face to be swapped.
target_img (PILImage): The target image to swap faces in.
model (str): Path to the face swap model.
@@ -614,14 +619,16 @@ def swap_face(
"""
return_result = ImageResult(target_img, {}, {})
target_img_cv2: CV2ImgU8 = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
target_img_cv2: CV2ImgU8 = cv2.cvtColor(
np.array(target_img), cv2.COLOR_RGB2BGR
).astype("uint8")
try:
gender = source_face["gender"]
logger.info("Source Gender %s", gender)
if source_face is not None:
result = target_img_cv2
result: CV2ImgU8 = target_img_cv2
model_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), model)
face_swapper = getFaceSwapModel(model_path)
face_swapper = getFaceSwapModel(model_path, use_gpu=not is_cpu_provider())
logger.info("Target faces count : %s", len(target_faces))
for i, swapped_face in enumerate(target_faces):
@@ -679,9 +686,9 @@ def process_image_unit(
model: str,
unit: FaceSwapUnitSettings,
image: PILImage,
info: str = None,
info: Optional[str] = None,
force_blend: bool = False,
) -> List[Tuple[PILImage, str]]:
) -> List[Tuple[PILImage, Optional[str]]]:
"""Process one image and return a List of (image, info) (one if blended, many if not).
Args:
@@ -722,7 +729,9 @@ def process_image_unit(
sort_by_face_size=unit.sort_by_size,
)
target_faces = filter_faces(faces, filtering_options=face_filtering_options)
target_faces: List[Face] = filter_faces(
all_faces=faces, filtering_options=face_filtering_options
)
# Apply pre-inpainting to image
if unit.pre_inpainting.inpainting_denoising_strengh > 0:
@@ -732,13 +741,11 @@ def process_image_unit(
save_img_debug(image, "Before swap")
result: ImageResult = swap_face(
reference_face=reference_face,
source_face=src_face,
target_img=current_image,
target_faces=target_faces,
model=model,
swapping_options=unit.swapping_options,
compute_similarity=unit.compute_similarity,
)
# Apply post-inpainting to image
if unit.post_inpainting.inpainting_denoising_strengh > 0:
@@ -1,20 +1,21 @@
from dataclasses import *
from typing import Optional
from client_api import api_utils
@dataclass
class InswappperOptions:
face_restorer_name: str = None
face_restorer_name: Optional[str] = None
restorer_visibility: float = 1
codeformer_weight: float = 1
upscaler_name: str = None
upscaler_name: Optional[str] = None
improved_mask: bool = False
color_corrections: bool = False
sharpen: bool = False
erosion_factor: float = 1.0
@staticmethod
def from_api_dto(dto: api_utils.InswappperOptions) -> "InswappperOptions":
def from_api_dto(dto: Optional[api_utils.InswappperOptions]) -> "InswappperOptions":
"""
Converts a InpaintingOptions object from an API DTO (Data Transfer Object).
@@ -1,10 +1,9 @@
from typing import Any, Tuple, Union
from typing import Any, Optional, Tuple, Union
import cv2
import numpy as np
from insightface.model_zoo.inswapper import INSwapper
from insightface.utils import face_align
from modules import processing, shared
from modules.shared import opts
from modules.upscaler import UpscalerData
from scripts.faceswaplab_postprocessing import upscaling
@@ -14,13 +13,14 @@ from scripts.faceswaplab_postprocessing.postprocessing_options import (
from scripts.faceswaplab_swapping.facemask import generate_face_mask
from scripts.faceswaplab_swapping.upcaled_inswapper_options import InswappperOptions
from scripts.faceswaplab_utils.imgutils import cv2_to_pil, pil_to_cv2
from scripts.faceswaplab_utils.sd_utils import get_sd_option
from scripts.faceswaplab_utils.typing import CV2ImgU8, Face
from scripts.faceswaplab_utils.faceswaplab_logging import logger
def get_upscaler() -> UpscalerData:
def get_upscaler() -> Optional[UpscalerData]:
for upscaler in shared.sd_upscalers:
if upscaler.name == opts.data.get(
if upscaler.name == get_sd_option(
"faceswaplab_upscaled_swapper_upscaler", "LDSR"
):
return upscaler
@@ -130,8 +130,14 @@ class UpscaledINSwapper(INSwapper):
self.__dict__.update(inswapper.__dict__)
def upscale_and_restore(
self, img: CV2ImgU8, k: int = 2, inswapper_options: InswappperOptions = None
self,
img: CV2ImgU8,
k: int = 2,
inswapper_options: Optional[InswappperOptions] = None,
) -> CV2ImgU8:
if inswapper_options is None:
return img
pil_img = cv2_to_pil(img)
pp_options = PostProcessingOptions(
upscaler_name=inswapper_options.upscaler_name,
@@ -156,7 +162,7 @@ class UpscaledINSwapper(INSwapper):
target_face: Face,
source_face: Face,
paste_back: bool = True,
options: InswappperOptions = None,
options: Optional[InswappperOptions] = None,
) -> Union[CV2ImgU8, Tuple[CV2ImgU8, Any]]:
aimg, M = face_align.norm_crop2(img, target_face.kps, self.input_size[0])
blob = cv2.dnn.blobFromImage(
@@ -166,9 +172,10 @@ class UpscaledINSwapper(INSwapper):
(self.input_mean, self.input_mean, self.input_mean),
swapRB=True,
)
latent = source_face.normed_embedding.reshape((1, -1))
latent = source_face.normed_embedding.reshape((1, -1)) # type: ignore
latent = np.dot(latent, self.emap)
latent /= np.linalg.norm(latent)
assert self.session is not None
pred = self.session.run(
self.output_names, {self.input_names[0]: blob, self.input_names[1]: latent}
)[0]
@@ -209,18 +216,11 @@ class UpscaledINSwapper(INSwapper):
bgr_fake, inswapper_options=options, k=k
)
if options.improved_mask:
if k == 1:
logger.warning(
"Please note that improved mask does not work well without upscaling. Set upscaling to Lanczos at least if you want speed and want to use improved mask."
)
fake_diff: CV2ImgU8 = None # type: ignore
logger.info("improved_mask")
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 not options.improved_mask:
# If improved mask is not used, we should compute before sharpen and color correction (better diff)
fake_diff = compute_diff(bgr_fake, aimg=aimg)
if options.sharpen:
logger.info("sharpen")
@@ -237,6 +237,24 @@ class UpscaledINSwapper(INSwapper):
)
bgr_fake = pil_to_cv2(bgr_fake_pil)
if options.improved_mask:
if k == 1:
logger.warning(
"Please note that improved mask does not work well without upscaling. Set upscaling to Lanczos at least if you want speed and want to use improved mask."
)
logger.info("improved_mask")
mask = get_face_mask(aimg, bgr_fake)
# save_img_debug(cv2_to_pil(bgr_fake), "Before Mask")
bgr_fake = merge_images_with_mask(aimg, bgr_fake, mask)
# save_img_debug(cv2_to_pil(bgr_fake), "After Mask")
fake_diff = compute_diff(bgr_fake, aimg=aimg)
assert (
fake_diff is not None
), "fake diff is None, this should not happen"
logger.info("*" * 80)
else:
@@ -259,6 +277,7 @@ class UpscaledINSwapper(INSwapper):
(target_img.shape[1], target_img.shape[0]),
borderValue=0.0,
)
fake_diff = cv2.warpAffine(
fake_diff,
IM,
@@ -274,7 +293,7 @@ class UpscaledINSwapper(INSwapper):
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 = options.erosion_factor
erosion_factor = options.erosion_factor if options else 1
k = max(int(mask_size // 10 * erosion_factor), int(10 * erosion_factor))
@@ -1,68 +1,74 @@
from typing import List
import gradio as gr
from modules.shared import opts
from modules import sd_models, sd_samplers
from scripts.faceswaplab_utils.sd_utils import get_sd_option
def face_inpainting_ui(
name: str, id_prefix: str = "faceswaplab", description: str = ""
) -> List[gr.components.Component]:
with gr.Accordion(name, open=False):
gr.Markdown(description)
inpainting_denoising_strength = gr.Slider(
0,
def face_inpainting_ui(id_prefix: str = "faceswaplab") -> List[gr.components.Component]:
inpainting_denoising_strength = gr.Slider(
0,
1,
0,
step=0.01,
elem_id=f"{id_prefix}_pp_inpainting_denoising_strength",
label="Denoising strenght",
)
inpainting_denoising_prompt = gr.Textbox(
get_sd_option(
"faceswaplab_pp_default_inpainting_prompt", "Portrait of a [gender]"
),
elem_id=f"{id_prefix}_pp_inpainting_denoising_prompt",
label="Inpainting prompt use [gender] instead of men or woman",
)
inpainting_denoising_negative_prompt = gr.Textbox(
get_sd_option("faceswaplab_pp_default_inpainting_negative_prompt", "blurry"),
elem_id=f"{id_prefix}_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 sd_samplers.all_samplers]
inpainting_sampler = gr.Dropdown(
choices=samplers_names,
value=[samplers_names[0]],
label="Inpainting Sampler",
elem_id=f"{id_prefix}_pp_inpainting_sampler",
)
inpainting_denoising_steps = gr.Slider(
1,
0,
step=0.01,
elem_id=f"{id_prefix}_pp_inpainting_denoising_strength",
label="Denoising strenght",
150,
20,
step=1,
label="Inpainting steps",
elem_id=f"{id_prefix}_pp_inpainting_steps",
)
inpainting_denoising_prompt = gr.Textbox(
opts.data.get(
"faceswaplab_pp_default_inpainting_prompt", "Portrait of a [gender]"
),
elem_id=f"{id_prefix}_pp_inpainting_denoising_prompt",
label="Inpainting prompt use [gender] instead of men or woman",
)
inpainting_denoising_negative_prompt = gr.Textbox(
opts.data.get(
"faceswaplab_pp_default_inpainting_negative_prompt", "blurry"
),
elem_id=f"{id_prefix}_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 sd_samplers.all_samplers]
inpainting_sampler = gr.Dropdown(
choices=samplers_names,
value=[samplers_names[0]],
label="Inpainting Sampler",
elem_id=f"{id_prefix}_pp_inpainting_sampler",
)
inpainting_denoising_steps = gr.Slider(
1,
150,
20,
step=1,
label="Inpainting steps",
elem_id=f"{id_prefix}_pp_inpainting_steps",
)
inpaiting_model = gr.Dropdown(
choices=["Current"] + sd_models.checkpoint_tiles(),
default="Current",
label="sd model (experimental)",
elem_id=f"{id_prefix}_pp_inpainting_sd_model",
)
inpaiting_model = gr.Dropdown(
choices=["Current"] + sd_models.checkpoint_tiles(),
default="Current",
label="sd model (experimental)",
elem_id=f"{id_prefix}_pp_inpainting_sd_model",
)
inpaiting_seed = gr.Number(
label="Inpainting seed",
value=0,
minimum=0,
precision=0,
elem_id=f"{id_prefix}_pp_inpainting_seed",
)
gradio_components: List[gr.components.Component] = [
inpainting_denoising_strength,
inpainting_denoising_prompt,
inpainting_denoising_negative_prompt,
inpainting_denoising_steps,
inpainting_sampler,
inpaiting_model,
]
gradio_components: List[gr.components.Component] = [
inpainting_denoising_strength,
inpainting_denoising_prompt,
inpainting_denoising_negative_prompt,
inpainting_denoising_steps,
inpainting_sampler,
inpaiting_model,
inpaiting_seed,
]
return gradio_components
for component in gradio_components:
setattr(component, "do_not_save_to_config", True)
return gradio_components
@@ -2,8 +2,9 @@ from typing import List
import gradio as gr
import modules
from modules import shared, sd_models
from modules.shared import opts
from scripts.faceswaplab_postprocessing.postprocessing_options import InpaintingWhen
from scripts.faceswaplab_utils.sd_utils import get_sd_option
from scripts.faceswaplab_ui.faceswaplab_inpainting_ui import face_inpainting_ui
def postprocessing_ui() -> List[gr.components.Component]:
@@ -15,18 +16,19 @@ def postprocessing_ui() -> List[gr.components.Component]:
face_restorer_name = gr.Radio(
label="Restore Face",
choices=["None"] + [x.name() for x in shared.face_restorers],
value=lambda: opts.data.get(
value=get_sd_option(
"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(
value=get_sd_option(
"faceswaplab_pp_default_face_restorer_visibility", 1
),
step=0.001,
@@ -36,7 +38,7 @@ def postprocessing_ui() -> List[gr.components.Component]:
codeformer_weight = gr.Slider(
0,
1,
value=lambda: opts.data.get(
value=get_sd_option(
"faceswaplab_pp_default_face_restorer_weight", 1
),
step=0.001,
@@ -45,7 +47,7 @@ def postprocessing_ui() -> List[gr.components.Component]:
)
upscaler_name = gr.Dropdown(
choices=[upscaler.name for upscaler in shared.sd_upscalers],
value=lambda: opts.data.get("faceswaplab_pp_default_upscaler", "None"),
value=get_sd_option("faceswaplab_pp_default_upscaler", "None"),
label="Upscaler",
elem_id="faceswaplab_pp_upscaler",
)
@@ -60,16 +62,15 @@ def postprocessing_ui() -> List[gr.components.Component]:
upscaler_visibility = gr.Slider(
0,
1,
value=lambda: opts.data.get(
"faceswaplab_pp_default_upscaler_visibility", 1
),
value=get_sd_option("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):
with gr.Accordion(label="Global-Inpainting (all faces)", open=False):
gr.Markdown(
"""Inpainting sends image to inpainting with a mask on face (once for each faces)."""
"Inpainting sends image to inpainting with a mask on face (once for each faces)."
)
inpainting_when = gr.Dropdown(
elem_id="faceswaplab_pp_inpainting_when",
@@ -77,53 +78,9 @@ def postprocessing_ui() -> List[gr.components.Component]:
value=[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)",
)
global_inpainting = face_inpainting_ui("faceswaplab_gpp")
inpainting_denoising_prompt = gr.Textbox(
opts.data.get(
"faceswaplab_pp_default_inpainting_prompt", "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(
opts.data.get(
"faceswaplab_pp_default_inpainting_negative_prompt", "blurry"
),
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 [
components = [
face_restorer_name,
face_restorer_visibility,
codeformer_weight,
@@ -131,10 +88,9 @@ def postprocessing_ui() -> List[gr.components.Component]:
upscaler_scale,
upscaler_visibility,
inpainting_when,
inpainting_denoising_strength,
inpainting_denoising_prompt,
inpainting_denoising_negative_prompt,
inpainting_denoising_steps,
inpainting_sampler,
inpaiting_model,
]
] + global_inpainting
# Ask sd to not store in ui-config.json
for component in components:
setattr(component, "do_not_save_to_config", True)
return components
+29 -23
View File
@@ -1,11 +1,12 @@
import traceback
from pprint import pformat
from typing import *
from scripts.faceswaplab_swapping import face_checkpoints
from scripts.faceswaplab_utils.sd_utils import get_sd_option
from scripts.faceswaplab_utils.typing import *
import gradio as gr
import onnx
import pandas as pd
from modules.shared import opts
from PIL import Image
import scripts.faceswaplab_swapping.swapper as swapper
@@ -15,9 +16,9 @@ from scripts.faceswaplab_postprocessing.postprocessing_options import (
from scripts.faceswaplab_ui.faceswaplab_postprocessing_ui import postprocessing_ui
from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
from scripts.faceswaplab_ui.faceswaplab_unit_ui import faceswap_unit_ui
from scripts.faceswaplab_utils import face_checkpoints_utils, imgutils
from scripts.faceswaplab_utils import imgutils
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from scripts.faceswaplab_utils.models_utils import get_models
from scripts.faceswaplab_utils.models_utils import get_swap_models
from scripts.faceswaplab_utils.ui_utils import dataclasses_from_flat_list
@@ -74,7 +75,7 @@ def extract_faces(
[PostProcessingOptions], components
).pop()
images = [
Image.open(file.name) for file in files
Image.open(file.name) for file in files # type: ignore
] # potentially greedy but Image.open is supposed to be lazy
result_images = swapper.extract_faces(
images, extract_path=extract_path, postprocess_options=postprocess_options
@@ -136,7 +137,7 @@ def analyse_faces(image: PILImage, det_threshold: float = 0.5) -> Optional[str]:
def build_face_checkpoint_and_save(
batch_files: gr.File, name: str, overwrite: bool
batch_files: List[gr.File], name: str, overwrite: bool
) -> PILImage:
"""
Builds a face checkpoint using the provided image files, performs face swapping,
@@ -154,17 +155,19 @@ def build_face_checkpoint_and_save(
try:
if not batch_files:
logger.error("No face found")
return None
images = [Image.open(file.name) for file in batch_files]
preview_image = face_checkpoints_utils.build_face_checkpoint_and_save(
images, name, overwrite=overwrite
return None # type: ignore (Optional not really supported by old gradio)
images: list[PILImage] = [Image.open(file.name) for file in batch_files] # type: ignore
preview_image: PILImage | None = (
face_checkpoints.build_face_checkpoint_and_save(
images=images, name=name, overwrite=overwrite
)
)
except Exception as e:
logger.error("Failed to build checkpoint %s", e)
traceback.print_exc()
return None
return preview_image
return None # type: ignore
return preview_image # type: ignore
def explore_onnx_faceswap_model(model_path: str) -> pd.DataFrame:
@@ -197,7 +200,7 @@ def explore_onnx_faceswap_model(model_path: str) -> pd.DataFrame:
logger.error("Failed to explore model %s", e)
traceback.print_exc()
return None
return None # type: ignore
return df
@@ -205,7 +208,7 @@ def batch_process(
files: List[gr.File], save_path: str, *components: Tuple[Any, ...]
) -> List[PILImage]:
try:
units_count = opts.data.get("faceswaplab_units_count", 3)
units_count = get_sd_option("faceswaplab_units_count", 3)
classes: List[Any] = dataclasses_from_flat_list(
[FaceSwapUnitSettings] * units_count + [PostProcessingOptions],
@@ -216,13 +219,16 @@ def batch_process(
]
postprocess_options = classes[-1]
images_paths = [file.name for file in files]
images_paths = [file.name for file in files] # type: ignore
return swapper.batch_process(
images_paths,
save_path=save_path,
units=units,
postprocess_options=postprocess_options,
return (
swapper.batch_process(
images_paths,
save_path=save_path,
units=units,
postprocess_options=postprocess_options,
)
or []
)
except Exception as e:
logger.error("Batch Process error : %s", e)
@@ -232,7 +238,7 @@ def batch_process(
def tools_ui() -> None:
models = get_models()
models = get_swap_models()
with gr.Tab("Tools"):
with gr.Tab("Build"):
gr.Markdown(
@@ -304,7 +310,7 @@ def tools_ui() -> None:
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="",
@@ -360,7 +366,7 @@ def tools_ui() -> None:
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/",
@@ -370,7 +376,7 @@ def tools_ui() -> None:
"Process & Save", elem_id="faceswaplab_extract_btn"
)
unit_components = []
for i in range(1, opts.data.get("faceswaplab_units_count", 3) + 1):
for i in range(1, get_sd_option("faceswaplab_units_count", 3) + 1):
unit_components += faceswap_unit_ui(False, i, id_prefix="faceswaplab_tab")
upscale_options = postprocessing_ui()
@@ -9,7 +9,7 @@ from PIL import Image
from scripts.faceswaplab_swapping.upcaled_inswapper_options import InswappperOptions
from scripts.faceswaplab_utils.imgutils import pil_to_cv2
from scripts.faceswaplab_utils.faceswaplab_logging import logger
from scripts.faceswaplab_utils import face_checkpoints_utils
from scripts.faceswaplab_swapping import face_checkpoints
from scripts.faceswaplab_inpainting.faceswaplab_inpainting import InpaintingOptions
from client_api import api_utils
@@ -124,7 +124,7 @@ class FaceSwapUnitSettings:
if self.source_face and self.source_face != "None":
try:
logger.info(f"loading face {self.source_face}")
face = face_checkpoints_utils.load_face(self.source_face)
face = face_checkpoints.load_face(self.source_face)
self._reference_face = face
except Exception as e:
logger.error("Failed to load checkpoint : %s", e)
@@ -169,7 +169,7 @@ class FaceSwapUnitSettings:
if isinstance(file, Image.Image):
img = file
else:
img = Image.open(file.name)
img = Image.open(file.name) # type: ignore
face = swapper.get_or_default(
swapper.get_faces(pil_to_cv2(img)), 0, None
+27 -30
View File
@@ -1,9 +1,9 @@
from typing import List
from scripts.faceswaplab_ui.faceswaplab_inpainting_ui import face_inpainting_ui
from scripts.faceswaplab_utils.face_checkpoints_utils import get_face_checkpoints
from scripts.faceswaplab_swapping.face_checkpoints import get_face_checkpoints
import gradio as gr
from modules.shared import opts
from modules import shared
from scripts.faceswaplab_utils.sd_utils import get_sd_option
def faceswap_unit_advanced_options(
@@ -17,7 +17,7 @@ def faceswap_unit_advanced_options(
face_restorer_name = gr.Radio(
label="Restore Face",
choices=["None"] + [x.name() for x in shared.face_restorers],
value=lambda: opts.data.get(
value=get_sd_option(
"faceswaplab_default_upscaled_swapper_face_restorer",
"None",
),
@@ -28,7 +28,7 @@ def faceswap_unit_advanced_options(
face_restorer_visibility = gr.Slider(
0,
1,
value=lambda: opts.data.get(
value=get_sd_option(
"faceswaplab_default_upscaled_swapper_face_restorer_visibility",
1.0,
),
@@ -39,7 +39,7 @@ def faceswap_unit_advanced_options(
codeformer_weight = gr.Slider(
0,
1,
value=lambda: opts.data.get(
value=get_sd_option(
"faceswaplab_default_upscaled_swapper_face_restorer_weight", 1.0
),
step=0.001,
@@ -48,33 +48,25 @@ def faceswap_unit_advanced_options(
)
upscaler_name = gr.Dropdown(
choices=[upscaler.name for upscaler in shared.sd_upscalers],
value=lambda: opts.data.get(
"faceswaplab_default_upscaled_swapper_upscaler", ""
),
value=get_sd_option("faceswaplab_default_upscaled_swapper_upscaler", ""),
label="Upscaler",
elem_id=f"{id_prefix}_face{unit_num}_upscaler",
)
improved_mask = gr.Checkbox(
lambda: opts.data.get(
"faceswaplab_default_upscaled_swapper_improved_mask", False
),
get_sd_option("faceswaplab_default_upscaled_swapper_improved_mask", False),
interactive=True,
label="Use improved segmented mask (use pastenet to mask only the face)",
elem_id=f"{id_prefix}_face{unit_num}_improved_mask",
)
color_corrections = gr.Checkbox(
lambda: opts.data.get(
"faceswaplab_default_upscaled_swapper_fixcolor", False
),
get_sd_option("faceswaplab_default_upscaled_swapper_fixcolor", False),
interactive=True,
label="Use color corrections",
elem_id=f"{id_prefix}_face{unit_num}_color_corrections",
)
sharpen_face = gr.Checkbox(
lambda: opts.data.get(
"faceswaplab_default_upscaled_swapper_sharpen", False
),
get_sd_option("faceswaplab_default_upscaled_swapper_sharpen", False),
interactive=True,
label="sharpen face",
elem_id=f"{id_prefix}_face{unit_num}_sharpen_face",
@@ -82,13 +74,13 @@ def faceswap_unit_advanced_options(
erosion_factor = gr.Slider(
0.0,
10.0,
lambda: opts.data.get("faceswaplab_default_upscaled_swapper_erosion", 1.0),
get_sd_option("faceswaplab_default_upscaled_swapper_erosion", 1.0),
step=0.01,
label="Upscaled swapper mask erosion factor, 1 = default behaviour.",
elem_id=f"{id_prefix}_face{unit_num}_erosion_factor",
)
return [
components = [
face_restorer_name,
face_restorer_visibility,
codeformer_weight,
@@ -99,6 +91,11 @@ def faceswap_unit_advanced_options(
erosion_factor,
]
for component in components:
setattr(component, "do_not_save_to_config", True)
return components
def faceswap_unit_ui(
is_img2img: bool, unit_num: int = 1, id_prefix: str = "faceswaplab"
@@ -137,7 +134,7 @@ def faceswap_unit_ui(
elem_id=f"{id_prefix}_face{unit_num}_refresh_checkpoints",
)
def refresh_fn(selected: str) -> None:
def refresh_fn(selected: str):
return gr.Dropdown.update(
value=selected, choices=get_face_checkpoints()
)
@@ -252,19 +249,19 @@ Otherwise, read the [doc](https://glucauze.github.io/sd-webui-faceswaplab/doc/)
elem_id=f"{id_prefix}_face{unit_num}_min_ref_similarity",
)
pre_inpainting = face_inpainting_ui(
name="Pre-Inpainting (Before swapping)",
id_prefix=f"{id_prefix}_face{unit_num}_preinpainting",
description="Pre-inpainting sends face to inpainting before swapping",
)
with gr.Accordion(label="Pre-Inpainting (before swapping)", open=False):
gr.Markdown("Pre-inpainting sends face to inpainting before swapping")
pre_inpainting = face_inpainting_ui(
id_prefix=f"{id_prefix}_face{unit_num}_preinpainting",
)
options = faceswap_unit_advanced_options(is_img2img, unit_num, id_prefix)
post_inpainting = face_inpainting_ui(
name="Post-Inpainting (After swapping)",
id_prefix=f"{id_prefix}_face{unit_num}_postinpainting",
description="Post-inpainting sends face to inpainting after swapping",
)
with gr.Accordion(label="Post-Inpainting (After swapping)", open=False):
gr.Markdown("Pre-inpainting sends face to inpainting before swapping")
post_inpainting = face_inpainting_ui(
id_prefix=f"{id_prefix}_face{unit_num}_postinpainting",
)
gradio_components: List[gr.components.Component] = (
[
+14 -4
View File
@@ -5,11 +5,10 @@ import cv2
import numpy as np
from math import isqrt, ceil
import torch
from ifnude import detect
from scripts.faceswaplab_globals import NSFW_SCORE_THRESHOLD
from modules import processing
import base64
from collections import Counter
from scripts.faceswaplab_utils.sd_utils import get_sd_option
from scripts.faceswaplab_utils.typing import BoxCoords, CV2ImgU8, PILImage
from scripts.faceswaplab_utils.faceswaplab_logging import logger
@@ -25,16 +24,27 @@ def check_against_nsfw(img: PILImage) -> bool:
bool: True if any part of the image is considered NSFW, False otherwise.
"""
NSFW_SCORE_THRESHOLD = get_sd_option("faceswaplab_nsfw_threshold", 0.7)
# For testing purpose :
if NSFW_SCORE_THRESHOLD >= 1:
return False
from ifnude import detect
shapes: List[bool] = []
chunks: List[Dict[str, Union[int, float]]] = detect(img)
for chunk in chunks:
logger.debug(
f"chunck score {chunk['score']}, threshold : {NSFW_SCORE_THRESHOLD}"
)
shapes.append(chunk["score"] > NSFW_SCORE_THRESHOLD)
return any(shapes)
def pil_to_cv2(pil_img: PILImage) -> CV2ImgU8: # type: ignore
def pil_to_cv2(pil_img: PILImage) -> CV2ImgU8:
"""
Convert a PIL Image into an OpenCV image (cv2).
@@ -44,7 +54,7 @@ def pil_to_cv2(pil_img: PILImage) -> CV2ImgU8: # type: ignore
Returns:
CV2ImgU8: The input image converted to OpenCV format (BGR).
"""
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR).astype("uint8")
def cv2_to_pil(cv2_img: CV2ImgU8) -> PILImage: # type: ignore
@@ -0,0 +1,18 @@
from types import ModuleType
def check_install() -> None:
# Very ugly hack :( due to sdnext optimization not calling install.py every time if git log has not changed
import importlib.util
import sys
import os
current_dir = os.path.dirname(os.path.realpath(__file__))
check_install_path = os.path.join(current_dir, "..", "..", "install.py")
spec = importlib.util.spec_from_file_location("check_install", check_install_path)
if spec != None:
check_install: ModuleType = importlib.util.module_from_spec(spec)
sys.modules["check_install"] = check_install
spec.loader.exec_module(check_install) # type: ignore
check_install.check_install() # type: ignore
#### End of ugly hack :( !
+42 -6
View File
@@ -3,12 +3,47 @@ import os
from typing import List
import modules.scripts as scripts
from modules import scripts
from scripts.faceswaplab_globals import EXTENSION_PATH
from scripts.faceswaplab_globals import EXPECTED_INSWAPPER_SHA1, EXTENSION_PATH
from modules.shared import opts
from scripts.faceswaplab_utils.faceswaplab_logging import logger
import traceback
import hashlib
def get_models() -> List[str]:
def is_sha1_matching(file_path: str, expected_sha1: str) -> bool:
sha1_hash = hashlib.sha1(usedforsecurity=False)
try:
with open(file_path, "rb") as file:
for byte_block in iter(lambda: file.read(4096), b""):
sha1_hash.update(byte_block)
if sha1_hash.hexdigest() == expected_sha1:
return True
else:
return False
except Exception as e:
logger.error(
"Failed to check model hash, check the model is valid or has been downloaded adequately : %e",
e,
)
traceback.print_exc()
return False
def check_model() -> bool:
model_path = get_current_swap_model()
if not is_sha1_matching(
file_path=model_path, expected_sha1=EXPECTED_INSWAPPER_SHA1
):
logger.error(
"Suspicious sha1 for model %s, check the model is valid or has been downloaded adequately. Should be %s",
model_path,
EXPECTED_INSWAPPER_SHA1,
)
return False
return True
def get_swap_models() -> List[str]:
"""
Retrieve a list of swap model files.
@@ -31,15 +66,16 @@ def get_models() -> List[str]:
return models
def get_current_model() -> str:
model = opts.data.get("faceswaplab_model", None)
def get_current_swap_model() -> str:
model = opts.data.get("faceswaplab_model", None) # type: ignore
if model is None:
models = get_models()
models = get_swap_models()
model = models[0] if len(models) else None
logger.info("Try to use model : %s", model)
if not os.path.isfile(model):
if not os.path.isfile(model): # type: ignore
logger.error("The model %s cannot be found or loaded", model)
raise FileNotFoundError(
"No faceswap model found. Please add it to the faceswaplab directory."
)
assert model is not None
return model
+7
View File
@@ -0,0 +1,7 @@
from typing import Any
from modules.shared import opts
def get_sd_option(name: str, default: Any) -> Any:
assert opts.data is not None
return opts.data.get(name, default)
+2 -2
View File
@@ -1,10 +1,10 @@
from typing import Tuple
from numpy import uint8
from numpy.typing import NDArray
from insightface.app.common import Face as IFace
from PIL import Image
import numpy as np
PILImage = Image.Image
CV2ImgU8 = NDArray[uint8]
CV2ImgU8 = np.ndarray[int, np.dtype[uint8]]
Face = IFace
BoxCoords = Tuple[int, int, int, int]
+4
View File
@@ -14,6 +14,10 @@ def dataclass_from_flat_list(cls: type, values: Tuple[Any, ...]) -> Any:
init_values[field.name] = field.type(*inner_values)
idx += len(inner_values)
else:
if idx >= len(values):
raise IndexError(
f"Expected more values for dataclass {cls}. Current index: {idx}, values length: {len(values)}"
)
value = values[idx]
init_values[field.name] = value
idx += 1