├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── check.sh
├── client_api
├── __init__.py
├── api_utils.py
├── faceswaplab_api_example.py
├── requirements.txt
├── test.safetensors
└── test_image.png
├── docs
├── .gitignore
├── 404.html
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _includes
│ └── footer.html
├── _layouts
│ └── page.html
├── _sass
│ ├── minima.scss
│ └── minima
│ │ ├── _base.scss
│ │ ├── _layout.scss
│ │ └── _syntax-highlighting.scss
├── assets
│ └── images
│ │ ├── blend_face.png
│ │ ├── checkpoints.png
│ │ ├── checkpoints_use.png
│ │ ├── compare.png
│ │ ├── doc_mi.png
│ │ ├── doc_pp.png
│ │ ├── doc_tab.png
│ │ ├── example1.png
│ │ ├── example2.png
│ │ ├── extract.png
│ │ ├── face_units.png
│ │ ├── gender.png
│ │ ├── install_from_url.png
│ │ ├── inswapper_options.png
│ │ ├── keep_orig.png
│ │ ├── main_interface.png
│ │ ├── multiple_face_src.png
│ │ ├── post-processing.png
│ │ ├── postinpainting.png
│ │ ├── postinpainting_result.png
│ │ ├── settings.png
│ │ ├── similarity.png
│ │ ├── step1.png
│ │ ├── step2.png
│ │ ├── step3a.png
│ │ ├── step3b.png
│ │ ├── step4.png
│ │ ├── tab.png
│ │ ├── testein.png
│ │ ├── upscaled_settings.png
│ │ └── upscalled_swapper.png
├── documentation.markdown
├── examples.markdown
├── faq.markdown
├── features.markdown
├── index.markdown
└── install.markdown
├── install.py
├── models.json
├── mypy.ini
├── preload.py
├── references
├── man.png
└── woman.png
├── requirements-gpu.txt
├── requirements.txt
├── scripts
├── configure.py
├── faceswaplab.py
├── faceswaplab_api
│ └── faceswaplab_api.py
├── faceswaplab_globals.py
├── faceswaplab_inpainting
│ ├── faceswaplab_inpainting.py
│ └── i2i_pp.py
├── faceswaplab_postprocessing
│ ├── postprocessing.py
│ ├── postprocessing_options.py
│ └── upscaling.py
├── faceswaplab_settings
│ └── faceswaplab_settings.py
├── faceswaplab_swapping
│ ├── face_checkpoints.py
│ ├── facemask.py
│ ├── parsing
│ │ ├── __init__.py
│ │ └── parsenet.py
│ ├── swapper.py
│ ├── upcaled_inswapper_options.py
│ └── upscaled_inswapper.py
├── faceswaplab_ui
│ ├── faceswaplab_inpainting_ui.py
│ ├── faceswaplab_postprocessing_ui.py
│ ├── faceswaplab_tab.py
│ ├── faceswaplab_unit_settings.py
│ └── faceswaplab_unit_ui.py
└── faceswaplab_utils
│ ├── faceswaplab_logging.py
│ ├── imgutils.py
│ ├── install_utils.py
│ ├── models_utils.py
│ ├── sd_utils.py
│ ├── typing.py
│ └── ui_utils.py
├── test.sh
└── tests
├── test_api.py
└── test_image.png
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Please remember that the bug report section is not a forum. Before submitting a bug, ensure you have read the FAQ thoroughly. This helps us maintain an efficient bug tracking process. Thank you for your understanding and cooperation. Use the discussion section for anything else.**
11 |
12 | **Describe the bug**
13 | A clear and concise description of what the bug is.
14 |
15 | **To Reproduce**
16 | Steps to reproduce the behavior:
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Screenshots**
26 | If applicable, add screenshots to help explain your problem.
27 |
28 | **Desktop (please complete the following information):**
29 | - OS: [e.g. iOS]
30 | - Browser [e.g. chrome, safari]
31 | - Version [e.g. 22]
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | I have limited time to develop, so before asking for a feature, please read the FAQ section. Some features will not be implemented by choice. Use the discussion section for anything else.
11 |
12 | **Is your feature request related to a problem? Please describe.**
13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
14 |
15 | **Describe the solution you'd like**
16 | A clear and concise description of what you want to happen.
17 |
18 | **Describe alternatives you've considered**
19 | A clear and concise description of any alternative solutions or features you've considered.
20 |
21 | **Additional context**
22 | Add any other context or screenshots about the feature request here.
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python
3 |
4 | ### Python ###
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | models/*.onnx
14 |
15 | .vscode
16 | .mypy_cache
17 |
18 | models/*
19 |
20 | _site/*
21 |
22 | # Distribution / packaging
23 | .Python
24 | build/
25 | develop-eggs/
26 | dist/
27 | downloads/
28 | eggs/
29 | .eggs/
30 | lib/
31 | lib64/
32 | parts/
33 | sdist/
34 | var/
35 | wheels/
36 | share/python-wheels/
37 | *.egg-info/
38 | .installed.cfg
39 | *.egg
40 | MANIFEST
41 |
42 | # PyInstaller
43 | # Usually these files are written by a python script from a template
44 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
45 | *.manifest
46 | *.spec
47 |
48 | # Installer logs
49 | pip-log.txt
50 | pip-delete-this-directory.txt
51 |
52 | # Unit test / coverage reports
53 | htmlcov/
54 | .tox/
55 | .nox/
56 | .coverage
57 | .coverage.*
58 | .cache
59 | nosetests.xml
60 | coverage.xml
61 | *.cover
62 | *.py,cover
63 | .hypothesis/
64 | .pytest_cache/
65 | cover/
66 |
67 | # Translations
68 | *.mo
69 | *.pot
70 |
71 | # Django stuff:
72 | *.log
73 | local_settings.py
74 | db.sqlite3
75 | db.sqlite3-journal
76 |
77 | # Flask stuff:
78 | instance/
79 | .webassets-cache
80 |
81 | # Scrapy stuff:
82 | .scrapy
83 |
84 | # Sphinx documentation
85 | docs/_build/
86 |
87 | # PyBuilder
88 | .pybuilder/
89 | target/
90 |
91 | # Jupyter Notebook
92 | .ipynb_checkpoints
93 |
94 | # IPython
95 | profile_default/
96 | ipython_config.py
97 |
98 | # pyenv
99 | # For a library or package, you might want to ignore these files since the code is
100 | # intended to run in multiple environments; otherwise, check them in:
101 | # .python-version
102 |
103 | # pipenv
104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
107 | # install all needed dependencies.
108 | #Pipfile.lock
109 |
110 | # poetry
111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
112 | # This is especially recommended for binary packages to ensure reproducibility, and is more
113 | # commonly ignored for libraries.
114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
115 | #poetry.lock
116 |
117 | # pdm
118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
119 | #pdm.lock
120 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
121 | # in version control.
122 | # https://pdm.fming.dev/#use-with-ide
123 | .pdm.toml
124 |
125 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126 | __pypackages__/
127 |
128 | # Celery stuff
129 | celerybeat-schedule
130 | celerybeat.pid
131 |
132 | # SageMath parsed files
133 | *.sage.py
134 |
135 | # Environments
136 | .env
137 | .venv
138 | env/
139 | venv/
140 | ENV/
141 | env.bak/
142 | venv.bak/
143 |
144 | # Spyder project settings
145 | .spyderproject
146 | .spyproject
147 |
148 | # Rope project settings
149 | .ropeproject
150 |
151 | # mkdocs documentation
152 | /site
153 |
154 | # mypy
155 | .mypy_cache/
156 | .dmypy.json
157 | dmypy.json
158 |
159 | # Pyre type checker
160 | .pyre/
161 |
162 | # pytype static type analyzer
163 | .pytype/
164 |
165 | # Cython debug symbols
166 | cython_debug/
167 |
168 | # PyCharm
169 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171 | # and can be added to the global gitignore or merged into this file. For a more nuclear
172 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173 | #.idea/
174 |
175 | ### Python Patch ###
176 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
177 | poetry.toml
178 |
179 | # ruff
180 | .ruff_cache/
181 |
182 | # LSP config files
183 | pyrightconfig.json
184 |
185 | ### VisualStudioCode ###
186 | .vscode/*
187 | !.vscode/settings.json
188 | !.vscode/tasks.json
189 | !.vscode/launch.json
190 | !.vscode/extensions.json
191 | !.vscode/*.code-snippets
192 |
193 | # Local History for Visual Studio Code
194 | .history/
195 |
196 | # Built Visual Studio Code Extensions
197 | *.vsix
198 |
199 | ### VisualStudioCode Patch ###
200 | # Ignore all local history of files
201 | .history
202 | .ionide
203 |
204 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
205 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.4.0
4 | hooks:
5 | - id: check-added-large-files
6 | - id: check-case-conflict
7 | - id: check-docstring-first
8 | - id: detect-private-key
9 | - id: fix-byte-order-marker
10 | - repo: https://github.com/psf/black
11 | rev: 23.7.0
12 | hooks:
13 | - id: black
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.2.5
2 |
3 | Allow seed selection in inpainting.
4 |
5 | # 1.2.4
6 |
7 | Fix default settings by marking only managed field as do_not_save.
8 |
9 | See the discussion here : https://github.com/glucauze/sd-webui-faceswaplab/issues/62
10 |
11 | # 1.2.3
12 |
13 | Speed up ui : change the way default settings are manage by not storing them in ui-config.json
14 |
15 | Migration : YOU NEED TO recreate ui-config.json (delete) or at least remove any faceswaplab reference to be able to use default settings again.
16 |
17 | See this for explainations : https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/6109
18 |
19 | # 1.2.2
20 |
21 | + Add NSFW filter option in settings (1 == disable)
22 | + Improve install speed
23 | + Install gpu requirements by default if --use-cpu is not used
24 | + Fix improved mask + color correction
25 | + Remove javascript, use https://github.com/w-e-w/sdwebui-close-confirmation-dialogue.git instead to prevent gradio from closing.
26 |
27 | # 1.2.1 :
28 |
29 | Add GPU support option : see https://github.com/glucauze/sd-webui-faceswaplab/pull/24
30 |
31 | # 1.2.0 :
32 |
33 | This version changes quite a few things.
34 |
35 | + The upscaled inswapper options are now moved to each face unit. This makes it possible to fine-tune the settings for each face.
36 |
37 | + Upscaled inswapper configuration in sd now concerns default values in each unit's interface.
38 |
39 | + Pre- and post-inpainting is now possible for each face. Here too, default options are set in the main sd settings.
40 |
41 | + Codeformer is no longer the default in post-processing. Don't be surprised if you get bad results by default. You can set it to default in the application's global settings
42 |
43 | Bug fixes :
44 |
45 | + The problem of saving the grid should be solved.
46 | + The downscaling problem for inpainting should be solved.
47 | + Change model download logic and add checksum. This should prevent some bugs.
48 |
49 | In terms of the API, it is now possible to create a remote checkpoint and use it in units. See the example in client_api or the tests in the tests directory.
50 |
51 | See https://github.com/glucauze/sd-webui-faceswaplab/pull/19
52 |
53 | # 1.1.2 :
54 |
55 | + Switch face checkpoint format from pkl to safetensors
56 |
57 | See https://github.com/glucauze/sd-webui-faceswaplab/pull/4
58 |
59 | ## 1.1.1 :
60 |
61 | + Add settings for default inpainting prompts
62 | + Add better api support
63 | + Add api tests
64 | + bug fixes (extract, upscaling)
65 | + improve code checking and formatting (black, mypy, and pre-commit hooks)
66 |
67 |
68 | ## 1.1.0 :
69 |
70 | All listed in features
71 |
72 | + add inpainting model selection => allow to select a different model for face inpainting
73 | + add source faces selection => allow to select the reference face if multiple face are present in reference image
74 | + add select by size => sort faces by size from larger to smaller
75 | + add batch option => allow to process images without txt2img or i2i in tabs
76 | + add segmentation mask for upscaled inpainter (based on codeformer implementation) : avoid square mask and prevent degradation of non-face parts of the image.
77 |
78 | ## 0.1.0 :
79 |
80 | ### Major :
81 | + add multiple face support
82 | + add face blending support (will blend sources faces)
83 | + add face similarity evaluation (will compare face to a reference)
84 | + add filters to discard images that are not rated similar enough to reference image and source images
85 | + add face tools tab
86 | + face extraction tool
87 | + face builder tool : will build a face model that can be reused
88 | + add faces models
89 |
90 | ### Minor :
91 |
92 | Improve performance by not reprocessing source face each time
93 |
94 | ### Breaking changes
95 |
96 | base64 and api not supported anymore (will be reintroduced in the future)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FaceSwapLab for a1111/Vlad
2 |
3 | V1.2.3 : Breaking change for settings, please read changelog.
4 |
5 | Please read the documentation here : https://glucauze.github.io/sd-webui-faceswaplab/
6 |
7 | You can also read the [doc discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions/categories/guide-doc)
8 |
9 | See [CHANGELOG.md](CHANGELOG.md) for changes in last versions.
10 |
11 | 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.
12 |
13 | Some key features include the ability to reuse faces via checkpoints, multiple face units, batch process images, sort faces based on size or gender, and support for vladmantic. It also provides a face inpainting feature.
14 |
15 | 
16 |
17 | While FaceSwapLab is still under development, it has reached a good level of stability. This makes it a reliable tool for those who are interested in face-swapping within the Stable Diffusion environment. As with all projects of this type, it’s expected to improve and evolve over time.
18 |
19 | ## Disclaimer and license
20 |
21 | In short:
22 |
23 | + **Ethical Guideline:** NSFW is now configurable due to performance issue. Please don't use this to do harm.
24 | + **License:** This software is distributed under the terms of the GNU Affero General Public License (AGPL), version 3 or later.
25 | + **Model License:** This software uses InsightFace's pre-trained models, which are available for non-commercial research purposes only.
26 |
27 | More on this here : https://glucauze.github.io/sd-webui-faceswaplab/
28 |
29 | ### Known problems (wontfix):
30 |
31 | + Older versions of gradio don't work well with the extension. See this bug : https://github.com/glucauze/sd-webui-faceswaplab/issues/5
32 |
33 | ## Quick Start
34 |
35 | Here are some gifs to explain (non cherry picked, just random pictures) :
36 |
37 | ## Simple Usage (roop like)
38 |
39 | This use codeformer on all faces (including non swapped)
40 |
41 | [simple.webm](https://github.com/glucauze/sd-webui-faceswaplab/assets/137925069/de00b685-d441-44f9-bae3-71cd7abef113)
42 |
43 | ## Advanced options
44 |
45 | This is use to improve results. This use upscaling and codeformer only on swapped faces
46 |
47 | [advanced.webm](https://github.com/glucauze/sd-webui-faceswaplab/assets/137925069/50630311-bd25-487f-871b-0a44eecd435d)
48 |
49 | ## Inpainting
50 |
51 | This add inpainting on faces :
52 |
53 | [inpainting.webm](https://github.com/glucauze/sd-webui-faceswaplab/assets/137925069/3d3508e9-5be4-4566-8c41-8301b2d08355)
54 |
55 | ## Build and use checkpoints :
56 |
57 | [build.webm](https://github.com/glucauze/sd-webui-faceswaplab/assets/137925069/e84e9a3c-840d-4536-9fbb-09ed256406d7)
58 |
59 |
60 |
61 | ### Simple
62 |
63 | 1. Put a face in the reference.
64 | 2. Select a face number.
65 | 3. Select "Enable."
66 | 4. Select "CodeFormer" in **Global Post-Processing** tab.
67 |
68 | Once you're happy with some results but want to improve, the next steps are to:
69 |
70 | + 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).
71 | + Use pre/post inpainting to tweak the image a bit for more natural results.
72 |
73 | ### Better
74 |
75 | 1. Put a face in the reference.
76 | 2. Select a face number.
77 | 3. Select "Enable."
78 |
79 | 4. In **Post-Processing** accordeon:
80 | + Select "CodeFormer"
81 | + 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).
82 | + Use sharpen, color_correction and improved mask
83 |
84 | 5. Disable "CodeFormer" in **Global Post-Processing** tab (otherwise it will be applied twice)
85 |
86 | Don't hesitate to share config in the [discussion section](https://github.com/glucauze/sd-webui-faceswaplab/discussions).
87 |
88 | ### Features
89 |
90 | + **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).
91 |
92 | + **Vladmantic and a1111 Support**
93 |
94 | + **Batch Processing**
95 |
96 | + **GPU**
97 |
98 | + **Inpainting Fixes** : supports “only masked” and mask inpainting.
99 |
100 | + **Performance Improvements**: The overall performance of the software has been enhanced.
101 |
102 | + **FaceSwapLab Tab**: providing various tools (build, compare, extract, batch)
103 |
104 | + **FaceSwapLab Settings**: FaceSwapLab settings are now part of the sd settings. To access them, navigate to the sd settings section.
105 |
106 | + **Face Reuse Via Checkpoints**: The FaceTools tab now allows creating checkpoints, which facilitate face reuse. When a checkpoint is used, it takes precedence over the reference image, and the reference source image is discarded.
107 |
108 | + **Gender Detection**: The program can now detect gender based on faces.
109 |
110 | + **Face Combination (Blending)**: Multiple versions of a face can be combined to enhance the swapping result. This blending happens during checkpoint creation.)
111 |
112 | + **Preserve Original Images**: You can opt to keep original images before the swapping process.
113 |
114 | + **Multiple Face Versions for Replacement**: The program allows the use of multiple versions of the same face for replacement.
115 |
116 | + **Face Similarity and Filtering**: You can compare faces against the reference and/or source images.
117 |
118 | + **Face Comparison**: face comparison feature.
119 |
120 | + **Face Extraction**: face extraction with or without upscaling.
121 |
122 | + **Improved Post-Processing**: codeformer, gfpgan, upscaling.
123 |
124 | + **Post Inpainting**: This feature allows the application of image-to-image inpainting specifically to faces.
125 |
126 | + **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.
127 |
128 | + **API with typing support**
129 |
130 |
131 | ## Installation
132 |
133 | See the documentation here : https://glucauze.github.io/sd-webui-faceswaplab/
--------------------------------------------------------------------------------
/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | autoflake --in-place --remove-unused-variables -r --remove-all-unused-imports .
3 | mypy --non-interactive --install-types
4 | pre-commit run --all-files
5 |
--------------------------------------------------------------------------------
/client_api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/client_api/__init__.py
--------------------------------------------------------------------------------
/client_api/api_utils.py:
--------------------------------------------------------------------------------
1 | # Keep a copy of this file here, it is used by the server side api
2 |
3 | from typing import List, Tuple
4 | from PIL import Image
5 | from pydantic import BaseModel, Field
6 | from enum import Enum
7 | import base64, io
8 | from io import BytesIO
9 | from typing import List, Tuple, Optional
10 | import numpy as np
11 | import requests
12 | import safetensors
13 |
14 |
15 | class InpaintingWhen(Enum):
16 | NEVER = "Never"
17 | BEFORE_UPSCALING = "Before Upscaling/all"
18 | BEFORE_RESTORE_FACE = "After Upscaling/Before Restore Face"
19 | AFTER_ALL = "After All"
20 |
21 |
22 | class InpaintingOptions(BaseModel):
23 | inpainting_denoising_strengh: float = Field(
24 | description="Inpainting denoising strenght", default=0, lt=1, ge=0
25 | )
26 | inpainting_prompt: str = Field(
27 | description="Inpainting denoising strenght",
28 | examples=["Portrait of a [gender]"],
29 | default="Portrait of a [gender]",
30 | )
31 | inpainting_negative_prompt: str = Field(
32 | description="Inpainting denoising strenght",
33 | examples=[
34 | "Deformed, blurry, bad anatomy, disfigured, poorly drawn face, mutation"
35 | ],
36 | default="",
37 | )
38 | inpainting_steps: int = Field(
39 | description="Inpainting steps",
40 | examples=["Portrait of a [gender]"],
41 | ge=1,
42 | le=150,
43 | default=20,
44 | )
45 | inpainting_sampler: str = Field(
46 | description="Inpainting sampler", examples=["Euler"], default="Euler"
47 | )
48 | inpainting_model: str = Field(
49 | description="Inpainting model", examples=["Current"], default="Current"
50 | )
51 | inpainting_seed: int = Field(description="Inpainting Seed", ge=-1, default=-1)
52 |
53 |
54 | class InswappperOptions(BaseModel):
55 | face_restorer_name: str = Field(
56 | description="face restorer name", default="CodeFormer"
57 | )
58 | restorer_visibility: float = Field(
59 | description="face restorer visibility", default=1, le=1, ge=0
60 | )
61 | codeformer_weight: float = Field(
62 | description="face restorer codeformer weight", default=1, le=1, ge=0
63 | )
64 | upscaler_name: str = Field(description="upscaler name", default=None)
65 | improved_mask: bool = Field(description="Use Improved Mask", default=False)
66 | color_corrections: bool = Field(description="Use Color Correction", default=False)
67 | sharpen: bool = Field(description="Sharpen Image", default=False)
68 | erosion_factor: float = Field(description="Erosion Factor", default=1, le=10, ge=0)
69 |
70 |
71 | class FaceSwapUnit(BaseModel):
72 | # The image given in reference
73 | source_img: str = Field(
74 | description="base64 reference image",
75 | examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."],
76 | default=None,
77 | )
78 | # The checkpoint file
79 | source_face: str = Field(
80 | description="face checkpoint (from models/faceswaplab/faces)",
81 | examples=["my_face.safetensors"],
82 | default=None,
83 | )
84 | # base64 batch source images
85 | batch_images: Tuple[str] = Field(
86 | description="list of base64 batch source images",
87 | examples=[
88 | "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD....",
89 | "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD....",
90 | ],
91 | default=None,
92 | )
93 |
94 | # Will blend faces if True
95 | blend_faces: bool = Field(description="Will blend faces if True", default=True)
96 |
97 | # Use same gender filtering
98 | same_gender: bool = Field(description="Use same gender filtering", default=False)
99 |
100 | # Use same gender filtering
101 | sort_by_size: bool = Field(description="Sort Faces by size", default=False)
102 |
103 | # If True, discard images with low similarity
104 | check_similarity: bool = Field(
105 | description="If True, discard images with low similarity", default=False
106 | )
107 | # if True will compute similarity and add it to the image info
108 | compute_similarity: bool = Field(
109 | description="If True will compute similarity and add it to the image info",
110 | default=False,
111 | )
112 |
113 | # Minimum similarity against the used face (reference, batch or checkpoint)
114 | min_sim: float = Field(
115 | description="Minimum similarity against the used face (reference, batch or checkpoint)",
116 | default=0.0,
117 | )
118 | # Minimum similarity against the reference (reference or checkpoint if checkpoint is given)
119 | min_ref_sim: float = Field(
120 | description="Minimum similarity against the reference (reference or checkpoint if checkpoint is given)",
121 | default=0.0,
122 | )
123 |
124 | # The face index to use for swapping
125 | faces_index: Tuple[int] = Field(
126 | description="The face index to use for swapping, list of face numbers starting from 0",
127 | default=(0,),
128 | )
129 |
130 | reference_face_index: int = Field(
131 | description="The face index to use to extract face from reference",
132 | default=0,
133 | )
134 |
135 | pre_inpainting: Optional[InpaintingOptions] = Field(
136 | description="Inpainting options",
137 | default=None,
138 | )
139 |
140 | swapping_options: Optional[InswappperOptions] = Field(
141 | description="PostProcessing & Mask options",
142 | default=None,
143 | )
144 |
145 | post_inpainting: Optional[InpaintingOptions] = Field(
146 | description="Inpainting options",
147 | default=None,
148 | )
149 |
150 | def get_batch_images(self) -> List[Image.Image]:
151 | images = []
152 | if self.batch_images:
153 | for img in self.batch_images:
154 | images.append(base64_to_pil(img))
155 | return images
156 |
157 |
158 | class PostProcessingOptions(BaseModel):
159 | face_restorer_name: str = Field(description="face restorer name", default=None)
160 | restorer_visibility: float = Field(
161 | description="face restorer visibility", default=1, le=1, ge=0
162 | )
163 | codeformer_weight: float = Field(
164 | description="face restorer codeformer weight", default=1, le=1, ge=0
165 | )
166 |
167 | upscaler_name: str = Field(description="upscaler name", default=None)
168 | scale: float = Field(description="upscaling scale", default=1, le=10, ge=0)
169 | upscaler_visibility: float = Field(
170 | description="upscaler visibility", default=1, le=1, ge=0
171 | )
172 | inpainting_when: InpaintingWhen = Field(
173 | description="When inpainting happens",
174 | examples=[e.value for e in InpaintingWhen.__members__.values()],
175 | default=InpaintingWhen.NEVER,
176 | )
177 |
178 | inpainting_options: Optional[InpaintingOptions] = Field(
179 | description="Inpainting options",
180 | default=None,
181 | )
182 |
183 |
184 | class FaceSwapRequest(BaseModel):
185 | image: str = Field(
186 | description="base64 reference image",
187 | examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."],
188 | default=None,
189 | )
190 | units: List[FaceSwapUnit]
191 | postprocessing: Optional[PostProcessingOptions] = None
192 |
193 |
194 | class FaceSwapResponse(BaseModel):
195 | images: List[str] = Field(description="base64 swapped image", default=None)
196 | infos: Optional[List[str]] # not really used atm
197 |
198 | @property
199 | def pil_images(self) -> Image.Image:
200 | return [base64_to_pil(img) for img in self.images]
201 |
202 |
203 | class FaceSwapCompareRequest(BaseModel):
204 | image1: str = Field(
205 | description="base64 reference image",
206 | examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."],
207 | default=None,
208 | )
209 | image2: str = Field(
210 | description="base64 reference image",
211 | examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."],
212 | default=None,
213 | )
214 |
215 |
216 | class FaceSwapExtractRequest(BaseModel):
217 | images: List[str] = Field(
218 | description="base64 reference image",
219 | examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQECWAJYAAD...."],
220 | default=None,
221 | )
222 | postprocessing: Optional[PostProcessingOptions]
223 |
224 |
225 | class FaceSwapExtractResponse(BaseModel):
226 | images: List[str] = Field(description="base64 face images", default=None)
227 |
228 | @property
229 | def pil_images(self) -> Image.Image:
230 | return [base64_to_pil(img) for img in self.images]
231 |
232 |
233 | def pil_to_base64(img: Image.Image) -> np.array: # type:ignore
234 | if isinstance(img, str):
235 | img = Image.open(img)
236 |
237 | buffer = BytesIO()
238 | img.save(buffer, format="PNG")
239 | img_data = buffer.getvalue()
240 | base64_data = base64.b64encode(img_data)
241 | return base64_data.decode("utf-8")
242 |
243 |
244 | def base64_to_pil(base64str: Optional[str]) -> Optional[Image.Image]:
245 | if base64str is None:
246 | return None
247 | if "base64," in base64str: # check if the base64 string has a data URL scheme
248 | base64_data = base64str.split("base64,")[-1]
249 | img_bytes = base64.b64decode(base64_data)
250 | else:
251 | # if no data URL scheme, just decode
252 | img_bytes = base64.b64decode(base64str)
253 | return Image.open(io.BytesIO(img_bytes))
254 |
255 |
256 | def compare_faces(
257 | image1: Image.Image, image2: Image.Image, base_url: str = "http://localhost:7860"
258 | ) -> float:
259 | request = FaceSwapCompareRequest(
260 | image1=pil_to_base64(image1),
261 | image2=pil_to_base64(image2),
262 | )
263 |
264 | result = requests.post(
265 | url=f"{base_url}/faceswaplab/compare",
266 | data=request.json(),
267 | headers={"Content-Type": "application/json; charset=utf-8"},
268 | )
269 |
270 | return float(result.text)
271 |
272 |
273 | def safetensors_to_base64(file_path: str) -> str:
274 | with open(file_path, "rb") as file:
275 | file_bytes = file.read()
276 | return "data:application/face;base64," + base64.b64encode(file_bytes).decode(
277 | "utf-8"
278 | )
279 |
280 |
281 | def base64_to_safetensors(base64str: str, output_path: str) -> None:
282 | try:
283 | base64_data = base64str.split("base64,")[-1]
284 | file_bytes = base64.b64decode(base64_data)
285 | with open(output_path, "wb") as file:
286 | file.write(file_bytes)
287 | with safetensors.safe_open(output_path, framework="pt") as f:
288 | print(output_path, "keys =", f.keys())
289 | except Exception as e:
290 | print("Error : failed to convert base64 string to safetensor", e)
291 | import traceback
292 |
293 | traceback.print_exc()
294 |
--------------------------------------------------------------------------------
/client_api/faceswaplab_api_example.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import requests
3 | from api_utils import (
4 | FaceSwapUnit,
5 | InswappperOptions,
6 | base64_to_safetensors,
7 | pil_to_base64,
8 | PostProcessingOptions,
9 | InpaintingWhen,
10 | InpaintingOptions,
11 | FaceSwapRequest,
12 | FaceSwapResponse,
13 | FaceSwapExtractRequest,
14 | FaceSwapCompareRequest,
15 | FaceSwapExtractResponse,
16 | safetensors_to_base64,
17 | )
18 |
19 | address = "http://127.0.0.1:7860"
20 |
21 | # This has been tested on Linux platforms. This might requires some minor adaptations for windows.
22 |
23 |
24 | #############################
25 | # FaceSwap
26 |
27 | # First face unit :
28 | unit1 = FaceSwapUnit(
29 | source_img=pil_to_base64("../references/man.png"), # The face you want to use
30 | faces_index=(0,), # Replace first face
31 | )
32 |
33 | # Second face unit :
34 | unit2 = FaceSwapUnit(
35 | source_img=pil_to_base64("../references/woman.png"), # The face you want to use
36 | same_gender=True,
37 | faces_index=(0,), # Replace first woman since same gender is on
38 | )
39 |
40 | # Post-processing config :
41 | pp = PostProcessingOptions(
42 | face_restorer_name="CodeFormer",
43 | codeformer_weight=0.5,
44 | restorer_visibility=1,
45 | upscaler_name="Lanczos",
46 | scale=4,
47 | inpainting_when=InpaintingWhen.BEFORE_RESTORE_FACE,
48 | inpainting_options=InpaintingOptions(
49 | inpainting_steps=30,
50 | inpainting_denoising_strengh=0.1,
51 | ),
52 | )
53 |
54 | # Prepare the request
55 | request = FaceSwapRequest(
56 | image=pil_to_base64("test_image.png"), units=[unit1, unit2], postprocessing=pp
57 | )
58 |
59 | # Face Swap
60 | result = requests.post(
61 | url=f"{address}/faceswaplab/swap_face",
62 | data=request.json(),
63 | headers={"Content-Type": "application/json; charset=utf-8"},
64 | )
65 | response = FaceSwapResponse.parse_obj(result.json())
66 |
67 | for img in response.pil_images:
68 | img.show()
69 |
70 | #############################
71 | # Comparison
72 |
73 | request = FaceSwapCompareRequest(
74 | image1=pil_to_base64("../references/man.png"),
75 | image2=pil_to_base64(response.pil_images[0]),
76 | )
77 |
78 | result = requests.post(
79 | url=f"{address}/faceswaplab/compare",
80 | data=request.json(),
81 | headers={"Content-Type": "application/json; charset=utf-8"},
82 | )
83 |
84 | print("similarity", result.text)
85 |
86 | #############################
87 | # Extraction
88 |
89 | # Prepare the request
90 | request = FaceSwapExtractRequest(
91 | images=[pil_to_base64(response.pil_images[0])], postprocessing=pp
92 | )
93 |
94 | result = requests.post(
95 | url=f"{address}/faceswaplab/extract",
96 | data=request.json(),
97 | headers={"Content-Type": "application/json; charset=utf-8"},
98 | )
99 | response = FaceSwapExtractResponse.parse_obj(result.json())
100 |
101 | for img in response.pil_images:
102 | img.show()
103 |
104 |
105 | #############################
106 | # Build checkpoint
107 |
108 | source_images: List[str] = [
109 | pil_to_base64("../references/man.png"),
110 | pil_to_base64("../references/woman.png"),
111 | ]
112 |
113 | result = requests.post(
114 | url=f"{address}/faceswaplab/build",
115 | json=source_images,
116 | headers={"Content-Type": "application/json; charset=utf-8"},
117 | )
118 |
119 | base64_to_safetensors(result.json(), output_path="test.safetensors")
120 |
121 | #############################
122 | # FaceSwap with local safetensors
123 |
124 | # First face unit :
125 | unit1 = FaceSwapUnit(
126 | source_face=safetensors_to_base64(
127 | "test.safetensors"
128 | ), # convert the checkpoint to base64
129 | faces_index=(0,), # Replace first face
130 | swapping_options=InswappperOptions(
131 | face_restorer_name="CodeFormer",
132 | upscaler_name="LDSR",
133 | improved_mask=True,
134 | sharpen=True,
135 | color_corrections=True,
136 | ),
137 | )
138 |
139 | # Prepare the request
140 | request = FaceSwapRequest(image=pil_to_base64("test_image.png"), units=[unit1])
141 |
142 | # Face Swap
143 | result = requests.post(
144 | url=f"{address}/faceswaplab/swap_face",
145 | data=request.json(),
146 | headers={"Content-Type": "application/json; charset=utf-8"},
147 | )
148 | response = FaceSwapResponse.parse_obj(result.json())
149 |
150 | for img in response.pil_images:
151 | img.show()
152 |
--------------------------------------------------------------------------------
/client_api/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | Pillow
3 | pydantic
4 | Requests
5 | safetensors>=0.3.1
6 |
--------------------------------------------------------------------------------
/client_api/test.safetensors:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/client_api/test.safetensors
--------------------------------------------------------------------------------
/client_api/test_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/client_api/test_image.png
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-cache
4 | .jekyll-metadata
5 | vendor
6 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /404.html
3 | layout: default
4 | ---
5 |
6 |
19 |
20 |
21 |
404
22 |
23 |
Page not found :(
24 |
The requested page could not be found.
25 |
26 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | # Hello! This is where you manage which Jekyll version is used to run.
3 | # When you want to use a different version, change it below, save the
4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
5 | #
6 | # bundle exec jekyll serve
7 | #
8 | # This will help ensure the proper Jekyll version is running.
9 | # Happy Jekylling!
10 | gem "jekyll", "~> 3.9.3"
11 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
12 | gem "minima", "~> 2.5.1"
13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
14 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
15 | gem "github-pages", "~> 228", group: :jekyll_plugins
16 |
17 | group :jekyll_plugins do
18 | gem "webrick"
19 | gem 'jekyll-toc'
20 | end
21 |
22 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
23 | # and associated library.
24 | platforms :mingw, :x64_mingw, :mswin, :jruby do
25 | gem "tzinfo", ">= 1", "< 3"
26 | gem "tzinfo-data"
27 | end
28 |
29 | # Performance-booster for watching directories on Windows
30 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
31 |
32 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
33 | # do not have a Java counterpart.
34 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
35 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (7.0.6)
5 | concurrent-ruby (~> 1.0, >= 1.0.2)
6 | i18n (>= 1.6, < 2)
7 | minitest (>= 5.1)
8 | tzinfo (~> 2.0)
9 | addressable (2.8.4)
10 | public_suffix (>= 2.0.2, < 6.0)
11 | coffee-script (2.4.1)
12 | coffee-script-source
13 | execjs
14 | coffee-script-source (1.11.1)
15 | colorator (1.1.0)
16 | commonmarker (0.23.9)
17 | concurrent-ruby (1.2.2)
18 | dnsruby (1.70.0)
19 | simpleidn (~> 0.2.1)
20 | em-websocket (0.5.3)
21 | eventmachine (>= 0.12.9)
22 | http_parser.rb (~> 0)
23 | ethon (0.16.0)
24 | ffi (>= 1.15.0)
25 | eventmachine (1.2.7)
26 | execjs (2.8.1)
27 | faraday (2.7.10)
28 | faraday-net_http (>= 2.0, < 3.1)
29 | ruby2_keywords (>= 0.0.4)
30 | faraday-net_http (3.0.2)
31 | ffi (1.15.5)
32 | forwardable-extended (2.6.0)
33 | gemoji (3.0.1)
34 | github-pages (228)
35 | github-pages-health-check (= 1.17.9)
36 | jekyll (= 3.9.3)
37 | jekyll-avatar (= 0.7.0)
38 | jekyll-coffeescript (= 1.1.1)
39 | jekyll-commonmark-ghpages (= 0.4.0)
40 | jekyll-default-layout (= 0.1.4)
41 | jekyll-feed (= 0.15.1)
42 | jekyll-gist (= 1.5.0)
43 | jekyll-github-metadata (= 2.13.0)
44 | jekyll-include-cache (= 0.2.1)
45 | jekyll-mentions (= 1.6.0)
46 | jekyll-optional-front-matter (= 0.3.2)
47 | jekyll-paginate (= 1.1.0)
48 | jekyll-readme-index (= 0.3.0)
49 | jekyll-redirect-from (= 0.16.0)
50 | jekyll-relative-links (= 0.6.1)
51 | jekyll-remote-theme (= 0.4.3)
52 | jekyll-sass-converter (= 1.5.2)
53 | jekyll-seo-tag (= 2.8.0)
54 | jekyll-sitemap (= 1.4.0)
55 | jekyll-swiss (= 1.0.0)
56 | jekyll-theme-architect (= 0.2.0)
57 | jekyll-theme-cayman (= 0.2.0)
58 | jekyll-theme-dinky (= 0.2.0)
59 | jekyll-theme-hacker (= 0.2.0)
60 | jekyll-theme-leap-day (= 0.2.0)
61 | jekyll-theme-merlot (= 0.2.0)
62 | jekyll-theme-midnight (= 0.2.0)
63 | jekyll-theme-minimal (= 0.2.0)
64 | jekyll-theme-modernist (= 0.2.0)
65 | jekyll-theme-primer (= 0.6.0)
66 | jekyll-theme-slate (= 0.2.0)
67 | jekyll-theme-tactile (= 0.2.0)
68 | jekyll-theme-time-machine (= 0.2.0)
69 | jekyll-titles-from-headings (= 0.5.3)
70 | jemoji (= 0.12.0)
71 | kramdown (= 2.3.2)
72 | kramdown-parser-gfm (= 1.1.0)
73 | liquid (= 4.0.4)
74 | mercenary (~> 0.3)
75 | minima (= 2.5.1)
76 | nokogiri (>= 1.13.6, < 2.0)
77 | rouge (= 3.26.0)
78 | terminal-table (~> 1.4)
79 | github-pages-health-check (1.17.9)
80 | addressable (~> 2.3)
81 | dnsruby (~> 1.60)
82 | octokit (~> 4.0)
83 | public_suffix (>= 3.0, < 5.0)
84 | typhoeus (~> 1.3)
85 | html-pipeline (2.14.3)
86 | activesupport (>= 2)
87 | nokogiri (>= 1.4)
88 | http_parser.rb (0.8.0)
89 | i18n (1.14.1)
90 | concurrent-ruby (~> 1.0)
91 | jekyll (3.9.3)
92 | addressable (~> 2.4)
93 | colorator (~> 1.0)
94 | em-websocket (~> 0.5)
95 | i18n (>= 0.7, < 2)
96 | jekyll-sass-converter (~> 1.0)
97 | jekyll-watch (~> 2.0)
98 | kramdown (>= 1.17, < 3)
99 | liquid (~> 4.0)
100 | mercenary (~> 0.3.3)
101 | pathutil (~> 0.9)
102 | rouge (>= 1.7, < 4)
103 | safe_yaml (~> 1.0)
104 | jekyll-avatar (0.7.0)
105 | jekyll (>= 3.0, < 5.0)
106 | jekyll-coffeescript (1.1.1)
107 | coffee-script (~> 2.2)
108 | coffee-script-source (~> 1.11.1)
109 | jekyll-commonmark (1.4.0)
110 | commonmarker (~> 0.22)
111 | jekyll-commonmark-ghpages (0.4.0)
112 | commonmarker (~> 0.23.7)
113 | jekyll (~> 3.9.0)
114 | jekyll-commonmark (~> 1.4.0)
115 | rouge (>= 2.0, < 5.0)
116 | jekyll-default-layout (0.1.4)
117 | jekyll (~> 3.0)
118 | jekyll-feed (0.15.1)
119 | jekyll (>= 3.7, < 5.0)
120 | jekyll-gist (1.5.0)
121 | octokit (~> 4.2)
122 | jekyll-github-metadata (2.13.0)
123 | jekyll (>= 3.4, < 5.0)
124 | octokit (~> 4.0, != 4.4.0)
125 | jekyll-include-cache (0.2.1)
126 | jekyll (>= 3.7, < 5.0)
127 | jekyll-mentions (1.6.0)
128 | html-pipeline (~> 2.3)
129 | jekyll (>= 3.7, < 5.0)
130 | jekyll-optional-front-matter (0.3.2)
131 | jekyll (>= 3.0, < 5.0)
132 | jekyll-paginate (1.1.0)
133 | jekyll-readme-index (0.3.0)
134 | jekyll (>= 3.0, < 5.0)
135 | jekyll-redirect-from (0.16.0)
136 | jekyll (>= 3.3, < 5.0)
137 | jekyll-relative-links (0.6.1)
138 | jekyll (>= 3.3, < 5.0)
139 | jekyll-remote-theme (0.4.3)
140 | addressable (~> 2.0)
141 | jekyll (>= 3.5, < 5.0)
142 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
143 | rubyzip (>= 1.3.0, < 3.0)
144 | jekyll-sass-converter (1.5.2)
145 | sass (~> 3.4)
146 | jekyll-seo-tag (2.8.0)
147 | jekyll (>= 3.8, < 5.0)
148 | jekyll-sitemap (1.4.0)
149 | jekyll (>= 3.7, < 5.0)
150 | jekyll-swiss (1.0.0)
151 | jekyll-theme-architect (0.2.0)
152 | jekyll (> 3.5, < 5.0)
153 | jekyll-seo-tag (~> 2.0)
154 | jekyll-theme-cayman (0.2.0)
155 | jekyll (> 3.5, < 5.0)
156 | jekyll-seo-tag (~> 2.0)
157 | jekyll-theme-dinky (0.2.0)
158 | jekyll (> 3.5, < 5.0)
159 | jekyll-seo-tag (~> 2.0)
160 | jekyll-theme-hacker (0.2.0)
161 | jekyll (> 3.5, < 5.0)
162 | jekyll-seo-tag (~> 2.0)
163 | jekyll-theme-leap-day (0.2.0)
164 | jekyll (> 3.5, < 5.0)
165 | jekyll-seo-tag (~> 2.0)
166 | jekyll-theme-merlot (0.2.0)
167 | jekyll (> 3.5, < 5.0)
168 | jekyll-seo-tag (~> 2.0)
169 | jekyll-theme-midnight (0.2.0)
170 | jekyll (> 3.5, < 5.0)
171 | jekyll-seo-tag (~> 2.0)
172 | jekyll-theme-minimal (0.2.0)
173 | jekyll (> 3.5, < 5.0)
174 | jekyll-seo-tag (~> 2.0)
175 | jekyll-theme-modernist (0.2.0)
176 | jekyll (> 3.5, < 5.0)
177 | jekyll-seo-tag (~> 2.0)
178 | jekyll-theme-primer (0.6.0)
179 | jekyll (> 3.5, < 5.0)
180 | jekyll-github-metadata (~> 2.9)
181 | jekyll-seo-tag (~> 2.0)
182 | jekyll-theme-slate (0.2.0)
183 | jekyll (> 3.5, < 5.0)
184 | jekyll-seo-tag (~> 2.0)
185 | jekyll-theme-tactile (0.2.0)
186 | jekyll (> 3.5, < 5.0)
187 | jekyll-seo-tag (~> 2.0)
188 | jekyll-theme-time-machine (0.2.0)
189 | jekyll (> 3.5, < 5.0)
190 | jekyll-seo-tag (~> 2.0)
191 | jekyll-titles-from-headings (0.5.3)
192 | jekyll (>= 3.3, < 5.0)
193 | jekyll-toc (0.18.0)
194 | jekyll (>= 3.9)
195 | nokogiri (~> 1.12)
196 | jekyll-watch (2.2.1)
197 | listen (~> 3.0)
198 | jemoji (0.12.0)
199 | gemoji (~> 3.0)
200 | html-pipeline (~> 2.2)
201 | jekyll (>= 3.0, < 5.0)
202 | kramdown (2.3.2)
203 | rexml
204 | kramdown-parser-gfm (1.1.0)
205 | kramdown (~> 2.0)
206 | liquid (4.0.4)
207 | listen (3.8.0)
208 | rb-fsevent (~> 0.10, >= 0.10.3)
209 | rb-inotify (~> 0.9, >= 0.9.10)
210 | mercenary (0.3.6)
211 | minima (2.5.1)
212 | jekyll (>= 3.5, < 5.0)
213 | jekyll-feed (~> 0.9)
214 | jekyll-seo-tag (~> 2.1)
215 | minitest (5.18.1)
216 | nokogiri (1.15.3-x86_64-linux)
217 | racc (~> 1.4)
218 | octokit (4.25.1)
219 | faraday (>= 1, < 3)
220 | sawyer (~> 0.9)
221 | pathutil (0.16.2)
222 | forwardable-extended (~> 2.6)
223 | public_suffix (4.0.7)
224 | racc (1.7.1)
225 | rb-fsevent (0.11.2)
226 | rb-inotify (0.10.1)
227 | ffi (~> 1.0)
228 | rexml (3.2.5)
229 | rouge (3.26.0)
230 | ruby2_keywords (0.0.5)
231 | rubyzip (2.3.2)
232 | safe_yaml (1.0.5)
233 | sass (3.7.4)
234 | sass-listen (~> 4.0.0)
235 | sass-listen (4.0.0)
236 | rb-fsevent (~> 0.9, >= 0.9.4)
237 | rb-inotify (~> 0.9, >= 0.9.7)
238 | sawyer (0.9.2)
239 | addressable (>= 2.3.5)
240 | faraday (>= 0.17.3, < 3)
241 | simpleidn (0.2.1)
242 | unf (~> 0.1.4)
243 | terminal-table (1.8.0)
244 | unicode-display_width (~> 1.1, >= 1.1.1)
245 | typhoeus (1.4.0)
246 | ethon (>= 0.9.0)
247 | tzinfo (2.0.6)
248 | concurrent-ruby (~> 1.0)
249 | unf (0.1.4)
250 | unf_ext
251 | unf_ext (0.0.8.2)
252 | unicode-display_width (1.8.0)
253 | webrick (1.8.1)
254 |
255 | PLATFORMS
256 | x86_64-linux
257 |
258 | DEPENDENCIES
259 | github-pages (~> 228)
260 | http_parser.rb (~> 0.6.0)
261 | jekyll (~> 3.9.3)
262 | jekyll-toc
263 | minima (~> 2.5.1)
264 | tzinfo (>= 1, < 3)
265 | tzinfo-data
266 | wdm (~> 0.1.1)
267 | webrick
268 |
269 | BUNDLED WITH
270 | 2.4.17
271 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 | #
11 | # If you need help with YAML syntax, here are some quick references for you:
12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
13 | # https://learnxinyminutes.com/docs/yaml/
14 | #
15 | # Site settings
16 | # These are used to personalize your new site. If you look in the HTML files,
17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
18 | # You can create any custom variable you would like, and they will be accessible
19 | # in the templates via {{ site.myvariable }}.
20 |
21 | title: FaceSwap Lab
22 | description: >- # this means to ignore newlines until "baseurl:"
23 | FaceSwapLab is an extension for Stable Diffusion that simplifies face-swapping.
24 | Some key functions of FaceSwapLab include the ability to reuse faces via checkpoints,
25 | batch process images, sort faces based on size or gender, and support for vladmantic.
26 | domain: glucauze.github.io
27 | url: https://glucauze.github.io
28 | baseurl: /sd-webui-faceswaplab/
29 |
30 | # Build settings
31 | theme: minima
32 |
33 | author:
34 | name: Glucauze
35 | email: ""
36 |
37 | minima:
38 | skin: dark
39 |
40 | plugins:
41 | - jekyll-toc
42 |
43 | # Exclude from processing.
44 | # The following items will not be processed, by default.
45 | # Any item listed under the `exclude:` key here will be automatically added to
46 | # the internal "default list".
47 | #
48 | # Excluded items can be processed by explicitly listing the directories or
49 | # their entries' file path in the `include:` list.
50 | #
51 | # exclude:
52 | # - .sass-cache/
53 | # - .jekyll-cache/
54 | # - gemfiles/
55 | # - Gemfile
56 | # - Gemfile.lock
57 | # - node_modules/
58 | # - vendor/bundle/
59 | # - vendor/cache/
60 | # - vendor/gems/
61 | # - vendor/ruby/
62 |
--------------------------------------------------------------------------------
/docs/_includes/footer.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/_includes/footer.html
--------------------------------------------------------------------------------
/docs/_layouts/page.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
9 |
10 |
11 | {{ content | toc }}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/_sass/minima.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | // Define defaults for each variable.
4 |
5 | $base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
6 | $base-font-size: 16px !default;
7 | $base-font-weight: 400 !default;
8 | $small-font-size: $base-font-size * 0.875 !default;
9 | $base-line-height: 1.5 !default;
10 |
11 | $spacing-unit: 30px !default;
12 |
13 | $text-color: #e7f6f2!default;
14 | $background-color: #2c3333 !default;
15 | $brand-color: #FF8C6A !default;
16 |
17 | $grey-color: lighten($brand-color, 30%) !default;
18 | $grey-color-light: lighten($grey-color, 40%) !default;
19 | $grey-color-dark: darken($grey-color, 25%) !default;
20 |
21 | $table-text-align: left !default;
22 |
23 | // Width of the content area
24 | $content-width: 800px !default;
25 |
26 | $on-palm: 600px !default;
27 | $on-laptop: 800px !default;
28 |
29 | // Use media queries like this:
30 | // @include media-query($on-palm) {
31 | // .wrapper {
32 | // padding-right: $spacing-unit / 2;
33 | // padding-left: $spacing-unit / 2;
34 | // }
35 | // }
36 | @mixin media-query($device) {
37 | @media screen and (max-width: $device) {
38 | @content;
39 | }
40 | }
41 |
42 | @mixin relative-font-size($ratio) {
43 | font-size: $base-font-size * $ratio;
44 | }
45 |
46 | // Import partials.
47 | @import
48 | "minima/base",
49 | "minima/layout",
50 | "minima/syntax-highlighting"
51 | ;
52 |
53 |
54 | img{
55 | display: block;
56 | margin : 1em;
57 | margin-left:auto;
58 | margin-right:auto;
59 | }
--------------------------------------------------------------------------------
/docs/_sass/minima/_base.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Reset some basic elements
3 | */
4 | body, h1, h2, h3, h4, h5, h6,
5 | p, blockquote, pre, hr,
6 | dl, dd, ol, ul, figure {
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 |
12 |
13 | /**
14 | * Basic styling
15 | */
16 | body {
17 | font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
18 | color: $text-color;
19 | background-color: $background-color;
20 | -webkit-text-size-adjust: 100%;
21 | -webkit-font-feature-settings: "kern" 1;
22 | -moz-font-feature-settings: "kern" 1;
23 | -o-font-feature-settings: "kern" 1;
24 | font-feature-settings: "kern" 1;
25 | font-kerning: normal;
26 | display: flex;
27 | min-height: 100vh;
28 | flex-direction: column;
29 | }
30 |
31 |
32 |
33 | /**
34 | * Set `margin-bottom` to maintain vertical rhythm
35 | */
36 | h1, h2, h3, h4, h5, h6,
37 | p, blockquote, pre,
38 | ul, ol, dl, figure,
39 | %vertical-rhythm {
40 | margin-bottom: $spacing-unit / 2;
41 | }
42 |
43 |
44 |
45 | /**
46 | * `main` element
47 | */
48 | main {
49 | display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */
50 | }
51 |
52 |
53 |
54 | /**
55 | * Images
56 | */
57 | img {
58 | max-width: 100%;
59 | vertical-align: middle;
60 | }
61 |
62 |
63 |
64 | /**
65 | * Figures
66 | */
67 | figure > img {
68 | display: block;
69 | }
70 |
71 | figcaption {
72 | font-size: $small-font-size;
73 | }
74 |
75 |
76 |
77 | /**
78 | * Lists
79 | */
80 | ul, ol {
81 | margin-left: $spacing-unit;
82 | }
83 |
84 | li {
85 | > ul,
86 | > ol {
87 | margin-bottom: 0;
88 | }
89 | }
90 |
91 |
92 |
93 | /**
94 | * Headings
95 | */
96 | h1, h2, h3, h4, h5, h6 {
97 | font-weight: $base-font-weight;
98 | }
99 |
100 |
101 |
102 | /**
103 | * Links
104 | */
105 | a {
106 | color: $brand-color;
107 | text-decoration: none;
108 |
109 | &:visited {
110 | color: darken($brand-color, 15%);
111 | }
112 |
113 | &:hover {
114 | color: $text-color;
115 | text-decoration: underline;
116 | }
117 |
118 | .social-media-list &:hover {
119 | text-decoration: none;
120 |
121 | .username {
122 | text-decoration: underline;
123 | }
124 | }
125 | }
126 |
127 |
128 | /**
129 | * Blockquotes
130 | */
131 | blockquote {
132 | color: $grey-color;
133 | border-left: 4px solid $grey-color-light;
134 | padding-left: $spacing-unit / 2;
135 | @include relative-font-size(1.125);
136 | letter-spacing: -1px;
137 | font-style: italic;
138 |
139 | > :last-child {
140 | margin-bottom: 0;
141 | }
142 | }
143 |
144 |
145 |
146 | /**
147 | * Code formatting
148 | */
149 | pre,
150 | code {
151 | @include relative-font-size(0.9375);
152 | border: 1px solid $grey-color-light;
153 | border-radius: 3px;
154 | }
155 |
156 | code {
157 | padding: 1px 5px;
158 | }
159 |
160 | pre {
161 | padding: 8px 12px;
162 | overflow-x: auto;
163 |
164 | > code {
165 | border: 0;
166 | padding-right: 0;
167 | padding-left: 0;
168 | }
169 | }
170 |
171 |
172 |
173 | /**
174 | * Wrapper
175 | */
176 | .wrapper {
177 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
178 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
179 | margin-right: auto;
180 | margin-left: auto;
181 | padding-right: $spacing-unit;
182 | padding-left: $spacing-unit;
183 | @extend %clearfix;
184 |
185 | @include media-query($on-laptop) {
186 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
187 | max-width: calc(#{$content-width} - (#{$spacing-unit}));
188 | padding-right: $spacing-unit / 2;
189 | padding-left: $spacing-unit / 2;
190 | }
191 | }
192 |
193 |
194 |
195 | /**
196 | * Clearfix
197 | */
198 | %clearfix:after {
199 | content: "";
200 | display: table;
201 | clear: both;
202 | }
203 |
204 |
205 |
206 | /**
207 | * Icons
208 | */
209 |
210 | .svg-icon {
211 | width: 16px;
212 | height: 16px;
213 | display: inline-block;
214 | fill: #{$grey-color};
215 | padding-right: 5px;
216 | vertical-align: text-top;
217 | }
218 |
219 | .social-media-list {
220 | li + li {
221 | padding-top: 5px;
222 | }
223 | }
224 |
225 |
226 |
227 | /**
228 | * Tables
229 | */
230 | table {
231 | margin-bottom: $spacing-unit;
232 | width: 100%;
233 | text-align: $table-text-align;
234 | color: lighten($text-color, 18%);
235 | border-collapse: collapse;
236 | border: 1px solid $grey-color-light;
237 | th, td {
238 | padding: ($spacing-unit / 3) ($spacing-unit / 2);
239 | }
240 | th {
241 | border: 1px solid darken($grey-color-light, 4%);
242 | border-bottom-color: darken($grey-color-light, 12%);
243 | }
244 | td {
245 | border: 1px solid $grey-color-light;
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/docs/_sass/minima/_layout.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Site header
3 | */
4 | .site-header {
5 | border-top: 5px solid $grey-color-dark;
6 | border-bottom: 1px solid $grey-color-light;
7 | min-height: $spacing-unit * 1.865;
8 |
9 | // Positioning context for the mobile navigation icon
10 | position: relative;
11 | }
12 |
13 | .site-title {
14 | @include relative-font-size(1.625);
15 | font-weight: 300;
16 | line-height: $base-line-height * $base-font-size * 2.25;
17 | letter-spacing: -1px;
18 | margin-bottom: 0;
19 | float: left;
20 |
21 | &,
22 | &:visited {
23 | color: $grey-color-dark;
24 | }
25 | }
26 |
27 | .site-nav {
28 | float: right;
29 | line-height: $base-line-height * $base-font-size * 2.25;
30 |
31 | .nav-trigger {
32 | display: none;
33 | }
34 |
35 | .menu-icon {
36 | display: none;
37 | }
38 |
39 | .page-link {
40 | color: $text-color;
41 | line-height: $base-line-height;
42 |
43 | // Gaps between nav items, but not on the last one
44 | &:not(:last-child) {
45 | margin-right: 20px;
46 | }
47 | }
48 |
49 | @include media-query($on-palm) {
50 | position: absolute;
51 | top: 9px;
52 | right: $spacing-unit / 2;
53 | background-color: $background-color;
54 | border: 1px solid $grey-color-light;
55 | border-radius: 5px;
56 | text-align: right;
57 |
58 | label[for="nav-trigger"] {
59 | display: block;
60 | float: right;
61 | width: 36px;
62 | height: 36px;
63 | z-index: 2;
64 | cursor: pointer;
65 | }
66 |
67 | .menu-icon {
68 | display: block;
69 | float: right;
70 | width: 36px;
71 | height: 26px;
72 | line-height: 0;
73 | padding-top: 10px;
74 | text-align: center;
75 |
76 | > svg {
77 | fill: $grey-color-dark;
78 | }
79 | }
80 |
81 | input ~ .trigger {
82 | clear: both;
83 | display: none;
84 | }
85 |
86 | input:checked ~ .trigger {
87 | display: block;
88 | padding-bottom: 5px;
89 | }
90 |
91 | .page-link {
92 | display: block;
93 | padding: 5px 10px;
94 |
95 | &:not(:last-child) {
96 | margin-right: 0;
97 | }
98 | margin-left: 20px;
99 | }
100 | }
101 | }
102 |
103 |
104 |
105 | /**
106 | * Site footer
107 | */
108 | .site-footer {
109 | border-top: 1px solid $grey-color-light;
110 | padding: $spacing-unit 0;
111 | }
112 |
113 | .footer-heading {
114 | @include relative-font-size(1.125);
115 | margin-bottom: $spacing-unit / 2;
116 | }
117 |
118 | .contact-list,
119 | .social-media-list {
120 | list-style: none;
121 | margin-left: 0;
122 | }
123 |
124 | .footer-col-wrapper {
125 | @include relative-font-size(0.9375);
126 | color: $grey-color;
127 | margin-left: -$spacing-unit / 2;
128 | @extend %clearfix;
129 | }
130 |
131 | .footer-col {
132 | float: left;
133 | margin-bottom: $spacing-unit / 2;
134 | padding-left: $spacing-unit / 2;
135 | }
136 |
137 | .footer-col-1 {
138 | width: -webkit-calc(35% - (#{$spacing-unit} / 2));
139 | width: calc(35% - (#{$spacing-unit} / 2));
140 | }
141 |
142 | .footer-col-2 {
143 | width: -webkit-calc(20% - (#{$spacing-unit} / 2));
144 | width: calc(20% - (#{$spacing-unit} / 2));
145 | }
146 |
147 | .footer-col-3 {
148 | width: -webkit-calc(45% - (#{$spacing-unit} / 2));
149 | width: calc(45% - (#{$spacing-unit} / 2));
150 | }
151 |
152 | @include media-query($on-laptop) {
153 | .footer-col-1,
154 | .footer-col-2 {
155 | width: -webkit-calc(50% - (#{$spacing-unit} / 2));
156 | width: calc(50% - (#{$spacing-unit} / 2));
157 | }
158 |
159 | .footer-col-3 {
160 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
161 | width: calc(100% - (#{$spacing-unit} / 2));
162 | }
163 | }
164 |
165 | @include media-query($on-palm) {
166 | .footer-col {
167 | float: none;
168 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
169 | width: calc(100% - (#{$spacing-unit} / 2));
170 | }
171 | }
172 |
173 |
174 |
175 | /**
176 | * Page content
177 | */
178 | .page-content {
179 | padding: $spacing-unit 0;
180 | flex: 1;
181 | }
182 |
183 | .page-heading {
184 | @include relative-font-size(2);
185 | }
186 |
187 | .post-list-heading {
188 | @include relative-font-size(1.75);
189 | }
190 |
191 | .post-list {
192 | margin-left: 0;
193 | list-style: none;
194 |
195 | > li {
196 | margin-bottom: $spacing-unit;
197 | }
198 | }
199 |
200 | .post-meta {
201 | font-size: $small-font-size;
202 | color: $grey-color;
203 | }
204 |
205 | .post-link {
206 | display: block;
207 | @include relative-font-size(1.5);
208 | }
209 |
210 |
211 |
212 | /**
213 | * Posts
214 | */
215 | .post-header {
216 | margin-bottom: $spacing-unit;
217 | }
218 |
219 | .post-title {
220 | @include relative-font-size(2.625);
221 | letter-spacing: -1px;
222 | line-height: 1;
223 |
224 | @include media-query($on-laptop) {
225 | @include relative-font-size(2.25);
226 | }
227 | }
228 |
229 | .post-content {
230 | margin-bottom: $spacing-unit;
231 |
232 | h2 {
233 | @include relative-font-size(2);
234 |
235 | @include media-query($on-laptop) {
236 | @include relative-font-size(1.75);
237 | }
238 | }
239 |
240 | h3 {
241 | @include relative-font-size(1.625);
242 |
243 | @include media-query($on-laptop) {
244 | @include relative-font-size(1.375);
245 | }
246 | }
247 |
248 | h4 {
249 | @include relative-font-size(1.25);
250 |
251 | @include media-query($on-laptop) {
252 | @include relative-font-size(1.125);
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/docs/_sass/minima/_syntax-highlighting.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Syntax highlighting styles
3 | */
4 | .highlight {
5 | background: #fff;
6 | @extend %vertical-rhythm;
7 |
8 | .highlighter-rouge & {
9 | background: black;
10 | }
11 |
12 | .c { color: #998; font-style: italic } // Comment
13 | .err { color: #a61717; background-color: #e3d2d2 } // Error
14 | .k { font-weight: bold } // Keyword
15 | .o { font-weight: bold } // Operator
16 | .cm { color: #998; font-style: italic } // Comment.Multiline
17 | .cp { color: #999; font-weight: bold } // Comment.Preproc
18 | .c1 { color: #998; font-style: italic } // Comment.Single
19 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
20 | .gd { color: #000; background-color: #fdd } // Generic.Deleted
21 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
22 | .ge { font-style: italic } // Generic.Emph
23 | .gr { color: #a00 } // Generic.Error
24 | .gh { color: #999 } // Generic.Heading
25 | .gi { color: #000; background-color: #dfd } // Generic.Inserted
26 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
27 | .go { color: #888 } // Generic.Output
28 | .gp { color: #555 } // Generic.Prompt
29 | .gs { font-weight: bold } // Generic.Strong
30 | .gu { color: #aaa } // Generic.Subheading
31 | .gt { color: #a00 } // Generic.Traceback
32 | .kc { font-weight: bold } // Keyword.Constant
33 | .kd { font-weight: bold } // Keyword.Declaration
34 | .kp { font-weight: bold } // Keyword.Pseudo
35 | .kr { font-weight: bold } // Keyword.Reserved
36 | .kt { color: #458; font-weight: bold } // Keyword.Type
37 | .m { color: #099 } // Literal.Number
38 | .s { color: #d14 } // Literal.String
39 | .na { color: #008080 } // Name.Attribute
40 | .nb { color: #0086B3 } // Name.Builtin
41 | .nc { color: #458; font-weight: bold } // Name.Class
42 | .no { color: #008080 } // Name.Constant
43 | .ni { color: #800080 } // Name.Entity
44 | .ne { color: #900; font-weight: bold } // Name.Exception
45 | .nf { color: #900; font-weight: bold } // Name.Function
46 | .nn { color: #777 } // Name.Namespace
47 | .nt { color: #000080 } // Name.Tag
48 | .nv { color: #008080 } // Name.Variable
49 | .ow { font-weight: bold } // Operator.Word
50 | .w { color: #bbb } // Text.Whitespace
51 | .mf { color: #099 } // Literal.Number.Float
52 | .mh { color: #099 } // Literal.Number.Hex
53 | .mi { color: #099 } // Literal.Number.Integer
54 | .mo { color: #099 } // Literal.Number.Oct
55 | .sb { color: #d14 } // Literal.String.Backtick
56 | .sc { color: #d14 } // Literal.String.Char
57 | .sd { color: #d14 } // Literal.String.Doc
58 | .s2 { color: #d14 } // Literal.String.Double
59 | .se { color: #d14 } // Literal.String.Escape
60 | .sh { color: #d14 } // Literal.String.Heredoc
61 | .si { color: #d14 } // Literal.String.Interpol
62 | .sx { color: #d14 } // Literal.String.Other
63 | .sr { color: #009926 } // Literal.String.Regex
64 | .s1 { color: #d14 } // Literal.String.Single
65 | .ss { color: #990073 } // Literal.String.Symbol
66 | .bp { color: #999 } // Name.Builtin.Pseudo
67 | .vc { color: #008080 } // Name.Variable.Class
68 | .vg { color: #008080 } // Name.Variable.Global
69 | .vi { color: #008080 } // Name.Variable.Instance
70 | .il { color: #099 } // Literal.Number.Integer.Long
71 | }
72 |
--------------------------------------------------------------------------------
/docs/assets/images/blend_face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/blend_face.png
--------------------------------------------------------------------------------
/docs/assets/images/checkpoints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/checkpoints.png
--------------------------------------------------------------------------------
/docs/assets/images/checkpoints_use.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/checkpoints_use.png
--------------------------------------------------------------------------------
/docs/assets/images/compare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/compare.png
--------------------------------------------------------------------------------
/docs/assets/images/doc_mi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/doc_mi.png
--------------------------------------------------------------------------------
/docs/assets/images/doc_pp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/doc_pp.png
--------------------------------------------------------------------------------
/docs/assets/images/doc_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/doc_tab.png
--------------------------------------------------------------------------------
/docs/assets/images/example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/example1.png
--------------------------------------------------------------------------------
/docs/assets/images/example2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/example2.png
--------------------------------------------------------------------------------
/docs/assets/images/extract.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/extract.png
--------------------------------------------------------------------------------
/docs/assets/images/face_units.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/face_units.png
--------------------------------------------------------------------------------
/docs/assets/images/gender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/gender.png
--------------------------------------------------------------------------------
/docs/assets/images/install_from_url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/install_from_url.png
--------------------------------------------------------------------------------
/docs/assets/images/inswapper_options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/inswapper_options.png
--------------------------------------------------------------------------------
/docs/assets/images/keep_orig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/keep_orig.png
--------------------------------------------------------------------------------
/docs/assets/images/main_interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/main_interface.png
--------------------------------------------------------------------------------
/docs/assets/images/multiple_face_src.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/multiple_face_src.png
--------------------------------------------------------------------------------
/docs/assets/images/post-processing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/post-processing.png
--------------------------------------------------------------------------------
/docs/assets/images/postinpainting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/postinpainting.png
--------------------------------------------------------------------------------
/docs/assets/images/postinpainting_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/postinpainting_result.png
--------------------------------------------------------------------------------
/docs/assets/images/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/settings.png
--------------------------------------------------------------------------------
/docs/assets/images/similarity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/similarity.png
--------------------------------------------------------------------------------
/docs/assets/images/step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/step1.png
--------------------------------------------------------------------------------
/docs/assets/images/step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/step2.png
--------------------------------------------------------------------------------
/docs/assets/images/step3a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/step3a.png
--------------------------------------------------------------------------------
/docs/assets/images/step3b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/step3b.png
--------------------------------------------------------------------------------
/docs/assets/images/step4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/step4.png
--------------------------------------------------------------------------------
/docs/assets/images/tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/tab.png
--------------------------------------------------------------------------------
/docs/assets/images/testein.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/testein.png
--------------------------------------------------------------------------------
/docs/assets/images/upscaled_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/upscaled_settings.png
--------------------------------------------------------------------------------
/docs/assets/images/upscalled_swapper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/docs/assets/images/upscalled_swapper.png
--------------------------------------------------------------------------------
/docs/examples.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Examples
4 | permalink: /examples/
5 | ---
6 |
7 | These examples show how to use a painting as a source. No post-processing is activated, only the upscaled inswapper with LDSR, Codeformer and segmented mask.
8 |
9 | Moliere:
10 |
11 | 
12 |
13 | Napoleon :
14 |
15 | 
--------------------------------------------------------------------------------
/docs/faq.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: FAQ
4 | permalink: /faq/
5 | toc: true
6 | ---
7 |
8 | Our issue tracker often contains requests that may originate from a misunderstanding of the software's functionality. We aim to address these queries; however, due to time constraints, we may not be able to respond to each request individually. This FAQ section serves as a preliminary source of information for commonly raised concerns. We recommend reviewing these before submitting an issue.
9 |
10 | #### Improving Quality of Results
11 |
12 | To get better quality results:
13 |
14 | 1. Ensure that the "Restore Face" option is enabled.
15 | 2. Consider using the "Upscaler" option. For finer control, you can use an upscaler from the "Extras" tab.
16 | 3. Use img2img with the denoise parameter set to `0.1`. Gradually increase this parameter until you achieve a balance of quality and resemblance.
17 |
18 | You can also use the uspcaled inswapper. I mainly use it with the following options :
19 |
20 | 
21 |
22 |
23 | #### Replacing Specific Faces
24 |
25 | If an image contains multiple faces and you want to swap specific ones: Use the "Comma separated face number(s)" option to select the face numbers you wish to swap.
26 |
27 | #### Issues with Face Swapping
28 |
29 | If a face did not get swapped, please check the following:
30 |
31 | 1. Ensure that the "Enable" option has been checked.
32 | 2. If you've ensured the above and your console doesn't show any errors, it means that the FaceSwapLab was unable to detect a face in your image or the image was detected as NSFW (Not Safe For Work).
33 |
34 | #### Controversy Surrounding NSFW Content Filtering
35 |
36 | We understand that some users might wish to have the option to disable content filtering, particularly for Not Safe for Work (NSFW) content. However, it's important to clarify our stance on this matter. We are not categorically against NSFW content. The concern arises specifically when the software is used to superimpose the face of a real person onto NSFW content.
37 |
38 | If it were reliably possible to ensure that the faces being swapped were synthetic and not tied to real individuals, the inclusion of NSFW content would pose less of an ethical dilemma. However, in the absence of such a guarantee, making this feature easily accessible could potentially lead to misuse, which is an ethically risky scenario.
39 |
40 | This is not our intention to impose our moral perspectives. Our goal is to comply with the requirements of the models used in the software and establish a balanced boundary that respects individual privacy and prevents potential misuse.
41 |
42 | Requests to provide an option to disable the content filter will not be considered.
43 |
44 | #### What is the role of the segmentation mask for the upscaled swapper?
45 |
46 | The segmentation mask for the upscaled swapper is designed to avoid the square mask and prevent degradation of the non-face parts of the image. It is based on the Codeformer implementation. If "Use improved segmented mask (use pastenet to mask only the face)" and "upscaled inswapper" are checked in the settings, the mask will only cover the face, and will not be squared. However, depending on the image, this might introduce different types of problems such as artifacts on the border of the face.
47 |
48 | #### How to increase speed of upscaled inswapper?
49 |
50 | It is possible to choose LANCZOS for speed if Codeformer is enabled in the upscaled inswapper. The result is generally satisfactory.
51 |
52 | #### Sharpening and color correction in upscaled swapper :
53 |
54 | Sharpening can provide more natural results, but it may also add artifacts. The same goes for color correction. By default, these options are set to False.
55 |
56 | #### I don't see any extension after restart
57 |
58 | If you do not see any extensions after restarting, it is likely due to missing requirements, particularly if you're using Windows. Follow the instructions below:
59 |
60 | 1. Verify that there are no error messages in the terminal.
61 | 2. Double-check the Installation section of this document to ensure all the steps have been followed.
62 |
63 | If you are running a specific configuration (for example, Python 3.11), please test the extension with a clean installation of the stable version of Diffusion before reporting an issue. This can help isolate whether the problem is related to your specific configuration or a broader issue with the extension.
64 |
65 | #### Understanding Quality of Results
66 |
67 | The model used in this extension initially reduces the resolution of the target face before generating a 128x128 image. This means that regardless of the original image's size, the resolution of the processed faces will not exceed 128x128. Consequently, this lower resolution might lead to quality limitations in the results.
68 |
69 | The output of this process might not meet high expectations, but the use of the face restorer and upscaler can help improve these results to some extent.
70 |
71 | The quality of results is inherently tied to the capabilities of the model and cannot be enhanced beyond its design. FaceSwapLab merely provides an interface for the underlying model. Therefore, unless the model from insighface is retrained and necessary alterations are made in the library (see below), the resulting quality may not meet high expectations.
72 |
73 | Consider this extension as a low-cost alternative to more sophisticated tools like Lora, or as an addition to such tools. It's important to **maintain realistic expectations of the results** provided by this extension.
74 |
75 | #### Why is a face not detected?
76 |
77 | Face detection might be influenced by various factors and settings, particularly the det_size and det_thresh parameters. Here's how these could affect detection:
78 |
79 | + Detection Size (det_size): If the detection size is set too small, it may not capture large faces adequately. A value of 320 has been found to be more effective for detecting large faces, though it might result in a loss of some quality.
80 |
81 | + Detection Threshold (det_thresh): If the threshold is set too high, it can make the detection more conservative, capturing only the most prominent faces. A lower threshold might detect more faces but could also result in more false positives.
82 |
83 | If a face is not being detected, adjusting these parameters might solve the issue. Try increasing the det_size if large faces are the problem, or experiment with different det_thresh values to find the balance that works best for your specific case.
84 |
85 |
86 | #### Issue: Incorrect Gender Detection
87 |
88 | The gender detection functionality is handled by the underlying analysis model. As such, there might be instances where the detected gender may not be accurate. This is a limitation of the model and we currently do not have a way to improve this accuracy from our end.
89 |
90 | #### Why isn't GPU support included?
91 |
92 | GPU is supported via an option see [documentation](../doc/). This is expermental, use it carefully.
93 |
94 | #### What is the 'Upscaled Inswapper' Option in SD FaceSwapLab?
95 |
96 | The 'Upscaled Inswapper' is an option in SD FaceSwapLab which allows for upscaling of each face using an upscaller prior to its integration into the image. This is achieved by modifying a small segment of the InsightFace code.
97 |
98 | The purpose of this feature is to enhance the quality of the face in the final image. While this process might slightly increase the processing time, it can deliver improved results. In certain cases, this could even eliminate the need for additional tools such as Codeformer or GFPGAN in postprocessing.
99 |
100 | #### What is Face Blending?
101 |
102 | Insighface works by creating an embedding for each face. An embedding is essentially a condensed representation of the facial characteristics.
103 |
104 | The face blending process allows for the averaging of multiple face embeddings to generate a blended or composite face.
105 |
106 | The benefits of face blending include:
107 |
108 | + Generation of a high-quality embedding based on multiple faces, thereby improving the face's representative accuracy.
109 | + Creation of a composite face that includes features from multiple individuals, which can be useful for diverse face recognition scenarios.
110 |
111 | To create a composite face, two methods are available:
112 |
113 | 1. Use the Checkpoint Builder: This tool allows you to save a set of face embeddings that can be loaded later to create a blended face.
114 | 2. Use Image Batch Sources: By dropping several images into this tool, you can generate a blended face based on the faces in the provided images.
115 |
116 | #### What is a face checkpoint?
117 |
118 | A face checkpoint is a saved embedding of a face, generated from multiple images. This is accomplished via the build tool located in the `sd` tab. The build tool blends all images dropped into the tab and saves the resulting embedding to a file.
119 |
120 | The primary advantage of face checkpoints is their size. An embedding is only around 2KB, meaning it's lightweight and can be reused later without requiring additional calculations.
121 |
122 | Face checkpoints are saved as `.safetensors` files.
123 |
124 | #### How is similarity determined?
125 |
126 | The similarity between faces is established by comparing their embeddings. In this context, a score of 1 signifies that the two faces are identical, while a score of 0 indicates that the faces are different.
127 |
128 | You can remove images from the results if the generated image does not match the reference. This is done by adjusting the sliders in the "Faces" tab.
129 |
130 | #### Which model is used?
131 |
132 | The model employed here is based on InsightFace's "InSwapper". For more specific information, you can refer [here](https://github.com/deepinsight/insightface/blob/fc622003d5410a64c96024563d7a093b2a55487c/python-package/insightface/model_zoo/inswapper.py#L12).
133 |
134 | This model was temporarily made public by the InsightFace team for research purposes. They have not provided any details about the training methodology.
135 |
136 | The model generates faces with a resolution of 128x128, which is relatively low. For better results, the generated faces need to be upscaled. The InsightFace code is not designed for higher resolutions (see the [Router](https://github.com/deepinsight/insightface/blob/fc622003d5410a64c96024563d7a093b2a55487c/python-package/insightface/model_zoo/model_zoo.py#L35) class for more information).
137 |
138 | #### Why not use SimSwap?
139 |
140 | SimSwap models are based on older InsightFace architectures, and SimSwap has not been released as a Python package. Its incorporation would complicate the process, and it does not guarantee any substantial gain.
141 |
142 | If you manage to implement SimSwap successfully, feel free to submit a pull request.
143 |
144 |
145 | #### Shasum of inswapper model
146 |
147 | Check that your model is correct and not corrupted :
148 |
149 | ```shell
150 | $>sha1sum inswapper_128.onnx
151 | 17a64851eaefd55ea597ee41e5c18409754244c5 inswapper_128.onnx
152 |
153 | $>sha256sum inswapper_128.onnx
154 | e4a3f08c753cb72d04e10aa0f7dbe3deebbf39567d4ead6dce08e98aa49e16af inswapper_128.onnx
155 |
156 | $>sha512sum inswapper_128.onnx
157 | 4311f4ccd9da58ec544e912b32ac0cba95f5ab4b1a06ac367efd3e157396efbae1097f624f10e77dd811fbba0917fa7c96e73de44563aa6099e5f46830965069 inswapper_128.onnx
158 | ```
159 |
160 | #### Gradio errors (issubclass() arg 1 must be a class)
161 |
162 | Older versions of gradio don't work well with the extension. See this bug report : https://github.com/glucauze/sd-webui-faceswaplab/issues/5
163 |
164 | It has been tested on 3.32.0
--------------------------------------------------------------------------------
/docs/features.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Features
4 | permalink: /features/
5 | ---
6 |
7 | + **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).
8 |
9 | 
10 |
11 | + **Vladmantic and a1111 Support**
12 |
13 | + **Batch Processing**
14 |
15 | + **Inpainting**: supports "only masked" and mask inpainting.
16 |
17 | + **Performance Improvements**: The overall performance of the software has been enhanced.
18 |
19 | + **FaceSwapLab Tab** providing various tools.
20 |
21 | 
22 |
23 | + **FaceSwapLab Settings**: FaceSwapLab settings are now part of the sd settings. To access them, navigate to the sd settings section.
24 |
25 | 
26 |
27 | + **Face Reuse Via Checkpoints**: The FaceTools tab now allows creating checkpoints, which facilitate face reuse. When a checkpoint is used, it takes precedence over the reference image, and the reference source image is discarded.
28 |
29 | 
30 | 
31 |
32 | + **Gender Detection**: The program can now detect gender based on faces.
33 |
34 | 
35 |
36 | + **Face Combination (Blending)**: Multiple versions of a face can be combined to enhance the swapping result. This blending happens during checkpoint creation.
37 |
38 | 
39 | 
40 |
41 | + **Preserve Original Images**: You can opt to keep original images before the swapping process.
42 |
43 | 
44 |
45 | + **Multiple Face Versions for Replacement**: The program allows the use of multiple versions of the same face for replacement.
46 |
47 | 
48 |
49 | + **Face Similarity and Filtering**: You can compare faces against the reference and/or source images.
50 |
51 | 
52 |
53 | + **Face Comparison**: face comparison feature.
54 |
55 | 
56 |
57 | + **Face Extraction**: face extraction with or without upscaling.
58 |
59 | 
60 |
61 | + **Improved Post-Processing**: codeformer, gfpgan, upscaling.
62 |
63 | 
64 |
65 | + **Post Inpainting**: This feature allows the application of image-to-image inpainting specifically to faces.
66 |
67 | 
68 | 
69 |
70 | + **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.
71 |
72 | 
73 |
74 |
75 | + **API with typing support** :
76 |
77 | ```python
78 | import base64
79 | import io
80 | import requests
81 | from PIL import Image
82 | from client_utils import FaceSwapRequest, FaceSwapUnit, PostProcessingOptions, FaceSwapResponse, pil_to_base64
83 |
84 | address = 'http:/127.0.0.1:7860'
85 |
86 | # First face unit :
87 | unit1 = FaceSwapUnit(
88 | source_img=pil_to_base64("../../references/man.png"), # The face you want to use
89 | faces_index=(0,) # Replace first face
90 | )
91 |
92 | # Second face unit :
93 | unit2 = FaceSwapUnit(
94 | source_img=pil_to_base64("../../references/woman.png"), # The face you want to use
95 | same_gender=True,
96 | faces_index=(0,) # Replace first woman since same gender is on
97 | )
98 |
99 | # Post-processing config :
100 | pp = PostProcessingOptions(
101 | face_restorer_name="CodeFormer",
102 | codeformer_weight=0.5,
103 | restorer_visibility= 1)
104 |
105 | # Prepare the request
106 | request = FaceSwapRequest (
107 | image = pil_to_base64("test_image.png"),
108 | units= [unit1, unit2],
109 | postprocessing=pp
110 | )
111 |
112 |
113 | result = requests.post(url=f'{address}/faceswaplab/swap_face', data=request.json(), headers={"Content-Type": "application/json; charset=utf-8"})
114 | response = FaceSwapResponse.parse_obj(result.json())
115 |
116 | for img, info in zip(response.pil_images, response.infos):
117 | img.show(title = info)
118 |
119 |
120 | ```
--------------------------------------------------------------------------------
/docs/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | # Feel free to add content and custom Front Matter to this file.
3 | # To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
4 |
5 | layout: home
6 | ---
7 |
8 | FaceSwapLab is an extension for Stable Diffusion that simplifies the use of [insighface models](https://insightface.ai/) for 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.
9 |
10 | Some key [features](features) include the ability to reuse faces via checkpoints, multiple face units, batch process images, sort faces based on size or gender, and support for vladmantic. It also provides a face inpainting feature.
11 |
12 | 
13 |
14 | Link to github repo : [https://github.com/glucauze/sd-webui-faceswaplab](https://github.com/glucauze/sd-webui-faceswaplab)
15 |
16 | While FaceSwapLab is still under development, it has reached a good level of stability. This makes it a reliable tool for those who are interested in face-swapping within the Stable Diffusion environment. As with all projects of this type, it's expected to improve and evolve over time.
17 |
18 |
19 | ## Disclaimer and license
20 |
21 | In short:
22 |
23 | + **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.
24 | + **License:** This software is distributed under the terms of the GNU Affero General Public License (AGPL), version 3 or later.
25 | + **Model License:** This software uses InsightFace's pre-trained models, which are available for non-commercial research purposes only.
26 |
27 | ### Ethical Guideline
28 |
29 | 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.
30 |
31 | 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.
32 |
33 | 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.
34 |
35 | Additionally, we believe it's important to make the public aware of these tools and the ease with which deepfakes can be created. As technology improves, we need to be more critical and skeptical when we encounter media content. By promoting media literacy, we can reduce the negative impact of misusing these tools and encourage responsible use in the digital world.
36 |
37 | ### Software License
38 |
39 | This software is distributed under the terms of the GNU Affero General Public License (AGPL), version 3 or later. It is provided "AS IS", without any express or implied warranties, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. In no event shall the author 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. Users are encouraged to review the license in full and to use the software in accordance with its terms.
40 |
41 | If any user violates their country's legal and ethical rules, we don't accept any liability for this code repository.
42 |
43 | ### Models License
44 |
45 | This software utilizes the pre-trained models `buffalo_l` and `inswapper_128.onnx`, which are provided by InsightFace. These models are included under the following conditions:
46 |
47 | _InsightFace's pre-trained models are available for non-commercial research purposes only. This includes both auto-downloading models and manually downloaded models._ from [insighface licence](https://github.com/deepinsight/insightface/tree/master/python-package)
48 |
49 | Users of this software must strictly adhere to these conditions of use. The developers and maintainers of this software are not responsible for any misuse of InsightFace's pre-trained models.
50 |
51 | Please note that if you intend to use this software for any commercial purposes, you will need to train your own models or find models that can be used commercially.
52 |
53 | ## Acknowledgments
54 |
55 | This project contains code adapted from the following sources:
56 | + codeformer : https://github.com/sczhou/CodeFormer
57 | + PSFRGAN : https://github.com/chaofengc/PSFRGAN
58 | + insightface : https://insightface.ai/
59 | + ifnude : https://github.com/s0md3v/ifnude
60 | + sd-webui-roop : https://github.com/s0md3v/sd-webui-roop
61 |
62 | ## Alternatives
63 |
64 | + https://github.com/idinkov/sd-deepface-1111
65 | + https://github.com/s0md3v/sd-webui-roop
66 |
--------------------------------------------------------------------------------
/docs/install.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Install
4 | permalink: /install/
5 | ---
6 |
7 | ## Requirements/Recommanded configuration
8 |
9 | The extension runs mainly on the CPU to avoid the use of VRAM. However, it is recommended to follow the specifications recommended by sd/a1111 with regard to prerequisites. At the time of writing, a version of python lower than 11 is preferable (even if it works with python 3.11, model loading and performance may fall short of expectations).
10 |
11 | Older versions of gradio don’t work well with the extension. See this bug report : https://github.com/glucauze/sd-webui-faceswaplab/issues/5. It has been tested on 3.32.0
12 |
13 | ### Windows-User : Visual Studio ! Don't neglect this !
14 |
15 | Before beginning the installation process, if you are using Windows, you need to install this requirement:
16 |
17 | 1. Install Visual Studio 2022: This step is required to build some of the dependencies. You can use the Community version of Visual Studio 2022, which can be downloaded from the following link: https://visualstudio.microsoft.com/downloads/
18 |
19 | 2. OR Install only the VS C++ Build Tools: If you don't need the full Visual Studio suite, you can choose to install only the VS C++ Build Tools. During the installation process, select the option for "Desktop Development with C++" found under the "Workloads -> Desktop & Mobile" section. The VS C++ Build Tools can be downloaded from this link: https://visualstudio.microsoft.com/visual-cpp-build-tools/
20 |
21 | 3. OR if you don't want to install either the full Visual Studio suite or the VS C++ Build Tools: Follow the instructions provided in section VIII of the documentation.
22 |
23 | ## SD.Next / Vladmantic
24 |
25 | SD.Next loading optimizations in relation to extension installation scripts can sometimes cause problems. This is particularly the case if you copy the script without installing it via the interface.
26 |
27 | If you get an error after startup, try restarting the server.
28 |
29 | ## Manual Install
30 |
31 | To install the extension, follow the steps below:
32 |
33 | 1. Open the `web-ui` application and navigate to the "Extensions" tab.
34 | 2. Use the URL `https://github.com/glucauze/sd-webui-faceswaplab` in the "install from URL" section.
35 | 3. Close the `web-ui` application and reopen it.
36 |
37 | 
38 |
39 |
40 | **You may need to restart sd once the installation process is complete.**
41 |
42 | On first launch, templates are downloaded, which may take some time. All models are located in the `models/faceswaplab` folder.
43 |
44 | If you encounter the error `'NoneType' object has no attribute 'get'`, take the following steps:
45 |
46 | 1. Download the [inswapper_128.onnx](https://huggingface.co/henryruhs/faceswaplab/resolve/main/inswapper_128.onnx) model.
47 | 2. Place the downloaded model inside the `/models/faceswaplab/` directory.
48 |
49 | ## Usage
50 |
51 | To use this extension, follow the steps below:
52 |
53 | 1. Navigate to the "faceswaplab" drop-down menu and import an image that contains a face.
54 | 2. Enable the extension by checking the "Enable" checkbox.
55 | 3. After performing the steps above, the generated result will have the face you selected.
56 |
--------------------------------------------------------------------------------
/install.py:
--------------------------------------------------------------------------------
1 | import launch
2 | import os
3 | import sys
4 | import pkg_resources
5 | from packaging.version import parse
6 |
7 |
8 | def check_install() -> None:
9 | use_gpu = True
10 |
11 | if use_gpu and sys.platform != "darwin":
12 | print("Faceswaplab : Use GPU requirements")
13 | req_file = os.path.join(
14 | os.path.dirname(os.path.realpath(__file__)), "requirements-gpu.txt"
15 | )
16 | else:
17 | print("Faceswaplab : Use CPU requirements")
18 | req_file = os.path.join(
19 | os.path.dirname(os.path.realpath(__file__)), "requirements.txt"
20 | )
21 |
22 | def is_installed(package: str) -> bool:
23 | package_name = package.split("==")[0].split(">=")[0].strip()
24 | try:
25 | installed_version = parse(
26 | pkg_resources.get_distribution(package_name).version
27 | )
28 | except pkg_resources.DistributionNotFound:
29 | return False
30 |
31 | if "==" in package:
32 | required_version = parse(package.split("==")[1])
33 | return installed_version == required_version
34 | elif ">=" in package:
35 | required_version = parse(package.split(">=")[1])
36 | return installed_version >= required_version
37 | else:
38 | if package_name == "opencv-python":
39 | return launch.is_installed(package_name) or launch.is_installed("cv2")
40 | return launch.is_installed(package_name)
41 |
42 | print("Checking faceswaplab requirements")
43 | with open(req_file) as file:
44 | for package in file:
45 | try:
46 | package = package.strip()
47 |
48 | if not is_installed(package):
49 | print(f"Install {package}")
50 | launch.run_pip(
51 | f"install {package}",
52 | f"sd-webui-faceswaplab requirement: {package}",
53 | )
54 |
55 | except Exception as e:
56 | print(e)
57 | print(
58 | f"Warning: Failed to install {package}, faceswaplab may not work. Try to restart server or install dependencies manually."
59 | )
60 | raise e
61 |
62 |
63 | import timeit
64 |
65 | try:
66 | check_time = timeit.timeit(check_install, number=1)
67 | print(check_time)
68 | except Exception as e:
69 | print("FaceswapLab install failed", e)
70 | print(
71 | "You can try to install dependencies manually by activating venv and installing requirements.txt or requirements-gpu.txt"
72 | )
73 |
--------------------------------------------------------------------------------
/models.json:
--------------------------------------------------------------------------------
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"}]
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | check_untyped_defs = True
3 | disallow_any_generics = True
4 | disallow_untyped_calls = True
5 | disallow_untyped_defs = True
6 | ignore_missing_imports = True
7 | strict_optional = False
8 | explicit_package_bases=True
--------------------------------------------------------------------------------
/preload.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser
2 |
3 |
4 | def preload(parser: ArgumentParser) -> None:
5 | parser.add_argument(
6 | "--faceswaplab_loglevel",
7 | default="INFO",
8 | choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
9 | help="Set the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
10 | )
11 |
--------------------------------------------------------------------------------
/references/man.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/references/man.png
--------------------------------------------------------------------------------
/references/woman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/references/woman.png
--------------------------------------------------------------------------------
/requirements-gpu.txt:
--------------------------------------------------------------------------------
1 | cython
2 | ifnude
3 | insightface==0.7.3
4 | onnx>=1.14.0
5 | protobuf>=3.20.2
6 | opencv-python
7 | pandas
8 | pydantic
9 | safetensors
10 | onnxruntime>=1.15.0
11 | onnxruntime-gpu>=1.15.0
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | protobuf>=3.20.2
2 | cython
3 | ifnude
4 | insightface==0.7.3
5 | onnx>=1.14.0
6 | onnxruntime>=1.15.0
7 | opencv-python
8 | pandas
9 | pydantic
10 | safetensors
--------------------------------------------------------------------------------
/scripts/configure.py:
--------------------------------------------------------------------------------
1 | import os
2 | from tqdm import tqdm
3 | import urllib.request
4 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
5 | from scripts.faceswaplab_globals import *
6 | from packaging import version
7 | import pkg_resources
8 | from scripts.faceswaplab_utils.models_utils import check_model
9 |
10 | ALREADY_DONE = False
11 |
12 |
13 | def check_configuration() -> None:
14 | global ALREADY_DONE
15 |
16 | if ALREADY_DONE:
17 | return
18 |
19 | # This has been moved here due to pb with sdnext in install.py not doing what a1111 is doing.
20 | models_dir = MODELS_DIR
21 | faces_dir = FACES_DIR
22 |
23 | model_url = "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx"
24 | model_name = os.path.basename(model_url)
25 | model_path = os.path.join(models_dir, model_name)
26 |
27 | def download(url: str, path: str) -> None:
28 | try:
29 | request = urllib.request.urlopen(url)
30 | total = int(request.headers.get("Content-Length", 0))
31 | with tqdm(
32 | total=total,
33 | desc="Downloading inswapper model",
34 | unit="B",
35 | unit_scale=True,
36 | unit_divisor=1024,
37 | ) as progress:
38 | urllib.request.urlretrieve(
39 | url,
40 | path,
41 | reporthook=lambda count, block_size, total_size: progress.update(
42 | block_size
43 | ),
44 | )
45 | except:
46 | logger.error(
47 | "Failed to download inswapper_128.onnx model, please download it manually and put it in the (/models/faceswaplab/inswapper_128.onnx) directory"
48 | )
49 |
50 | os.makedirs(models_dir, exist_ok=True)
51 | os.makedirs(faces_dir, exist_ok=True)
52 |
53 | if not os.path.exists(model_path):
54 | download(model_url, model_path)
55 | check_model()
56 |
57 | gradio_version = pkg_resources.get_distribution("gradio").version
58 |
59 | if version.parse(gradio_version) < version.parse("3.32.0"):
60 | logger.warning(
61 | "Errors may occur with gradio versions lower than 3.32.0. Your version : %s",
62 | gradio_version,
63 | )
64 |
65 | ALREADY_DONE = True
66 |
--------------------------------------------------------------------------------
/scripts/faceswaplab.py:
--------------------------------------------------------------------------------
1 | from scripts.configure import check_configuration
2 | from scripts.faceswaplab_utils.sd_utils import get_sd_option
3 |
4 | check_configuration()
5 |
6 | import importlib
7 | import traceback
8 |
9 | from scripts import faceswaplab_globals
10 | from scripts.faceswaplab_api import faceswaplab_api
11 | from scripts.faceswaplab_postprocessing import upscaling
12 | from scripts.faceswaplab_settings import faceswaplab_settings
13 | from scripts.faceswaplab_swapping import swapper
14 | from scripts.faceswaplab_ui import faceswaplab_tab, faceswaplab_unit_ui
15 | from scripts.faceswaplab_utils import faceswaplab_logging, imgutils, models_utils
16 | from scripts.faceswaplab_utils.models_utils import get_current_swap_model
17 | from scripts.faceswaplab_utils.typing import *
18 | from scripts.faceswaplab_utils.ui_utils import dataclasses_from_flat_list
19 | from scripts.faceswaplab_utils.faceswaplab_logging import logger, save_img_debug
20 |
21 | # Reload all the modules when using "apply and restart"
22 | # This is mainly done for development purposes
23 | import logging
24 |
25 | if logger.getEffectiveLevel() <= logging.DEBUG:
26 | importlib.reload(swapper)
27 | importlib.reload(faceswaplab_logging)
28 | importlib.reload(faceswaplab_globals)
29 | importlib.reload(imgutils)
30 | importlib.reload(upscaling)
31 | importlib.reload(faceswaplab_settings)
32 | importlib.reload(models_utils)
33 | importlib.reload(faceswaplab_unit_ui)
34 | importlib.reload(faceswaplab_api)
35 |
36 | import os
37 | from pprint import pformat
38 | from typing import Any, List, Optional, Tuple
39 |
40 | import gradio as gr
41 | import modules.scripts as scripts
42 | from modules import script_callbacks, scripts, shared
43 | from modules.images import save_image
44 | from modules.processing import (
45 | Processed,
46 | StableDiffusionProcessing,
47 | StableDiffusionProcessingImg2Img,
48 | )
49 | from modules.shared import opts
50 |
51 | from scripts.faceswaplab_globals import VERSION_FLAG
52 | from scripts.faceswaplab_postprocessing.postprocessing import enhance_image
53 | from scripts.faceswaplab_postprocessing.postprocessing_options import (
54 | PostProcessingOptions,
55 | )
56 | from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
57 |
58 | EXTENSION_PATH = os.path.join("extensions", "sd-webui-faceswaplab")
59 |
60 |
61 | # Register the tab, done here to prevent it from being added twice
62 | script_callbacks.on_ui_tabs(faceswaplab_tab.on_ui_tabs)
63 |
64 | try:
65 | import modules.script_callbacks as script_callbacks
66 |
67 | script_callbacks.on_app_started(faceswaplab_api.faceswaplab_api)
68 | except:
69 | logger.error("Failed to register API")
70 |
71 | traceback.print_exc()
72 |
73 |
74 | class FaceSwapScript(scripts.Script):
75 | def __init__(self) -> None:
76 | super().__init__()
77 |
78 | @property
79 | def units_count(self) -> int:
80 | return get_sd_option("faceswaplab_units_count", 3)
81 |
82 | @property
83 | def enabled(self) -> bool:
84 | """Return True if any unit is enabled and the state is not interupted"""
85 | return any([u.enable for u in self.units]) and not shared.state.interrupted
86 |
87 | @property
88 | def keep_original_images(self) -> bool:
89 | return get_sd_option("faceswaplab_keep_original", False)
90 |
91 | @property
92 | def swap_in_generated_units(self) -> List[FaceSwapUnitSettings]:
93 | return [u for u in self.units if u.swap_in_generated and u.enable]
94 |
95 | @property
96 | def swap_in_source_units(self) -> List[FaceSwapUnitSettings]:
97 | return [u for u in self.units if u.swap_in_source and u.enable]
98 |
99 | def title(self) -> str:
100 | return f"faceswaplab"
101 |
102 | def show(self, is_img2img: bool) -> bool:
103 | return scripts.AlwaysVisible # type: ignore
104 |
105 | def ui(self, is_img2img: bool) -> List[gr.components.Component]:
106 | with gr.Accordion(f"FaceSwapLab {VERSION_FLAG}", open=False):
107 | components: List[gr.components.Component] = []
108 | for i in range(1, self.units_count + 1):
109 | components += faceswaplab_unit_ui.faceswap_unit_ui(is_img2img, i)
110 | post_processing = faceswaplab_tab.postprocessing_ui()
111 | # If the order is modified, the before_process should be changed accordingly.
112 |
113 | components = components + post_processing
114 |
115 | return components
116 |
117 | def read_config(
118 | self, p: StableDiffusionProcessing, *components: Tuple[Any, ...]
119 | ) -> None:
120 | for i, c in enumerate(components):
121 | logger.debug("%s>%s", i, pformat(c))
122 |
123 | # The order of processing for the components is important
124 | # The method first process faceswap units then postprocessing units
125 | classes: List[Any] = dataclasses_from_flat_list(
126 | [FaceSwapUnitSettings] * self.units_count + [PostProcessingOptions],
127 | components,
128 | )
129 | self.units: List[FaceSwapUnitSettings] = []
130 | self.units += [u for u in classes if isinstance(u, FaceSwapUnitSettings)]
131 | self.postprocess_options = classes[-1]
132 |
133 | for i, u in enumerate(self.units):
134 | logger.debug("%s, %s", pformat(i), pformat(u))
135 |
136 | logger.debug("%s", pformat(self.postprocess_options))
137 |
138 | if self.enabled:
139 | p.do_not_save_samples = not self.keep_original_images
140 |
141 | def process(
142 | self, p: StableDiffusionProcessing, *components: Tuple[Any, ...]
143 | ) -> None:
144 | try:
145 | self.read_config(p, *components)
146 |
147 | # If is instance of img2img, we check if face swapping in source is required.
148 | if isinstance(p, StableDiffusionProcessingImg2Img):
149 | if self.enabled and len(self.swap_in_source_units) > 0:
150 | init_images: List[Tuple[Optional[PILImage], Optional[str]]] = [
151 | (img, None) for img in p.init_images
152 | ]
153 | new_inits = swapper.process_images_units(
154 | get_current_swap_model(),
155 | self.swap_in_source_units,
156 | images=init_images,
157 | force_blend=True,
158 | )
159 | logger.info(f"processed init images: {len(init_images)}")
160 | if new_inits is not None:
161 | p.init_images = [img[0] for img in new_inits]
162 | except Exception as e:
163 | logger.info("Failed to process : %s", e)
164 | traceback.print_exc()
165 |
166 | def postprocess(
167 | self, p: StableDiffusionProcessing, processed: Processed, *args: List[Any]
168 | ) -> None:
169 | try:
170 | if self.enabled:
171 | # Get the original images without the grid
172 | orig_images: List[PILImage] = processed.images[
173 | processed.index_of_first_image :
174 | ]
175 | orig_infotexts: List[str] = processed.infotexts[
176 | processed.index_of_first_image :
177 | ]
178 |
179 | keep_original = self.keep_original_images
180 |
181 | # These are were images and infos of swapped images will be stored
182 | images = []
183 | infotexts = []
184 | if (len(self.swap_in_generated_units)) > 0:
185 | for i, (img, info) in enumerate(zip(orig_images, orig_infotexts)):
186 | batch_index = i % p.batch_size
187 | swapped_images = swapper.process_images_units(
188 | get_current_swap_model(),
189 | self.swap_in_generated_units,
190 | images=[(img, info)],
191 | )
192 | if swapped_images is None:
193 | continue
194 |
195 | logger.info(f"{len(swapped_images)} images swapped")
196 | for swp_img, new_info in swapped_images:
197 | img = swp_img # Will only swap the last image in the batch in next units (FIXME : hard to fix properly but not really critical)
198 |
199 | if swp_img is not None:
200 | save_img_debug(swp_img, "Before apply mask")
201 | swp_img = imgutils.apply_mask(swp_img, p, batch_index)
202 | save_img_debug(swp_img, "After apply mask")
203 |
204 | try:
205 | if self.postprocess_options is not None:
206 | swp_img = enhance_image(
207 | swp_img, self.postprocess_options
208 | )
209 | except Exception as e:
210 | logger.error("Failed to upscale : %s", e)
211 |
212 | logger.info("Add swp image to processed")
213 | images.append(swp_img)
214 | infotexts.append(new_info)
215 | if p.outpath_samples and opts.samples_save:
216 | save_image(
217 | swp_img,
218 | p.outpath_samples,
219 | "",
220 | p.all_seeds[batch_index], # type: ignore
221 | p.all_prompts[batch_index], # type: ignore
222 | opts.samples_format,
223 | info=new_info,
224 | p=p,
225 | suffix="-swapped",
226 | )
227 | else:
228 | logger.error("swp image is None")
229 | else:
230 | keep_original = True
231 |
232 | # Generate grid :
233 | if opts.return_grid and len(images) > 1:
234 | grid = imgutils.create_square_image(images)
235 | text = processed.infotexts[0]
236 | infotexts.insert(0, text)
237 | if opts.enable_pnginfo:
238 | grid.info["parameters"] = text # type: ignore
239 | images.insert(0, grid)
240 |
241 | if opts.grid_save:
242 | save_image(
243 | grid,
244 | p.outpath_grids,
245 | "swapped-grid",
246 | p.all_seeds[0], # type: ignore
247 | p.all_prompts[0], # type: ignore
248 | opts.grid_format,
249 | info=text,
250 | short_filename=not opts.grid_extended_filename,
251 | p=p,
252 | grid=True,
253 | )
254 |
255 | if keep_original:
256 | # If we want to keep original images, we add all existing (including grid this time)
257 | images += processed.images
258 | infotexts += processed.infotexts
259 |
260 | processed.images = images
261 | processed.infotexts = infotexts
262 | except Exception as e:
263 | logger.error("Failed to swap face in postprocess method : %s", e)
264 | traceback.print_exc()
265 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_api/faceswaplab_api.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from PIL import Image
3 | import numpy as np
4 | from fastapi import FastAPI
5 | from modules.api import api
6 | from client_api.api_utils import (
7 | FaceSwapResponse,
8 | )
9 | from scripts.faceswaplab_globals import VERSION_FLAG
10 | import gradio as gr
11 | from typing import Dict, List, Optional, Union
12 | from scripts.faceswaplab_swapping import swapper
13 | from scripts.faceswaplab_ui.faceswaplab_unit_settings import FaceSwapUnitSettings
14 | from scripts.faceswaplab_utils.imgutils import (
15 | base64_to_pil,
16 | )
17 | from scripts.faceswaplab_postprocessing.postprocessing_options import (
18 | PostProcessingOptions,
19 | )
20 | from client_api import api_utils
21 | from scripts.faceswaplab_swapping.face_checkpoints import (
22 | build_face_checkpoint_and_save,
23 | )
24 | from scripts.faceswaplab_utils.typing import PILImage
25 |
26 |
27 | def encode_to_base64(image: Union[str, Image.Image, np.ndarray]) -> str: # type: ignore
28 | """
29 | Encode an image to a base64 string.
30 |
31 | The image can be a file path (str), a PIL Image, or a NumPy array.
32 |
33 | Args:
34 | image (Union[str, Image.Image, np.ndarray]): The image to encode.
35 |
36 | Returns:
37 | str: The base64-encoded image if successful, otherwise an empty string.
38 | """
39 | if isinstance(image, str):
40 | return image
41 | elif isinstance(image, Image.Image):
42 | return api.encode_pil_to_base64(image)
43 | elif isinstance(image, np.ndarray):
44 | return encode_np_to_base64(image)
45 | else:
46 | return ""
47 |
48 |
49 | def encode_np_to_base64(image: np.ndarray) -> str: # type: ignore
50 | """
51 | Encode a NumPy array to a base64 string.
52 |
53 | The array is first converted to a PIL Image, then encoded.
54 |
55 | Args:
56 | image (np.ndarray): The NumPy array to encode.
57 |
58 | Returns:
59 | str: The base64-encoded image.
60 | """
61 | pil = Image.fromarray(image)
62 | return api.encode_pil_to_base64(pil)
63 |
64 |
65 | def get_faceswap_units_settings(
66 | api_units: List[api_utils.FaceSwapUnit],
67 | ) -> List[FaceSwapUnitSettings]:
68 | units = []
69 | for u in api_units:
70 | units.append(FaceSwapUnitSettings.from_api_dto(u))
71 | return units
72 |
73 |
74 | def faceswaplab_api(_: gr.Blocks, app: FastAPI) -> None:
75 | @app.get(
76 | "/faceswaplab/version",
77 | tags=["faceswaplab"],
78 | description="Get faceswaplab version",
79 | )
80 | async def version() -> Dict[str, str]:
81 | return {"version": VERSION_FLAG}
82 |
83 | # use post as we consider the method non idempotent (which is debatable)
84 | @app.post(
85 | "/faceswaplab/swap_face",
86 | tags=["faceswaplab"],
87 | description="Swap a face in an image using units",
88 | )
89 | async def swap_face(
90 | request: api_utils.FaceSwapRequest,
91 | ) -> api_utils.FaceSwapResponse:
92 | units: List[FaceSwapUnitSettings] = []
93 | src_image: Optional[Image.Image] = base64_to_pil(request.image)
94 | response = FaceSwapResponse(images=[], infos=[])
95 |
96 | if src_image is not None:
97 | if request.postprocessing:
98 | pp_options = PostProcessingOptions.from_api_dto(request.postprocessing)
99 | else:
100 | pp_options = None
101 | units = get_faceswap_units_settings(request.units)
102 |
103 | swapped_images: Optional[List[PILImage]] = swapper.batch_process(
104 | [src_image], None, units=units, postprocess_options=pp_options
105 | )
106 |
107 | for img in swapped_images:
108 | response.images.append(encode_to_base64(img))
109 |
110 | response.infos = [] # Not used atm
111 | return response
112 |
113 | @app.post(
114 | "/faceswaplab/compare",
115 | tags=["faceswaplab"],
116 | description="Compare first face of each images",
117 | )
118 | async def compare(
119 | request: api_utils.FaceSwapCompareRequest,
120 | ) -> float:
121 | return swapper.compare_faces(
122 | base64_to_pil(request.image1), base64_to_pil(request.image2)
123 | )
124 |
125 | @app.post(
126 | "/faceswaplab/extract",
127 | tags=["faceswaplab"],
128 | description="Extract faces of each images",
129 | )
130 | async def extract(
131 | request: api_utils.FaceSwapExtractRequest,
132 | ) -> api_utils.FaceSwapExtractResponse:
133 | pp_options = None
134 | if request.postprocessing:
135 | pp_options = PostProcessingOptions.from_api_dto(request.postprocessing)
136 | images = [base64_to_pil(img) for img in request.images]
137 | faces = swapper.extract_faces(
138 | images, extract_path=None, postprocess_options=pp_options
139 | )
140 | result_images = [encode_to_base64(img) for img in faces]
141 | response = api_utils.FaceSwapExtractResponse(images=result_images)
142 | return response
143 |
144 | @app.post(
145 | "/faceswaplab/build",
146 | tags=["faceswaplab"],
147 | description="Build a face checkpoint using base64 images, return base64 satetensors",
148 | )
149 | async def build(base64_images: List[str]) -> Optional[str]:
150 | if len(base64_images) > 0:
151 | pil_images = [base64_to_pil(img) for img in base64_images]
152 | with tempfile.NamedTemporaryFile(
153 | delete=True, suffix=".safetensors"
154 | ) as temp_file:
155 | build_face_checkpoint_and_save(
156 | images=pil_images,
157 | name="api_ckpt",
158 | overwrite=True,
159 | path=temp_file.name,
160 | )
161 | return api_utils.safetensors_to_base64(temp_file.name)
162 | return None
163 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_globals.py:
--------------------------------------------------------------------------------
1 | import os
2 | from modules import scripts
3 |
4 | # Defining the absolute path for the 'faceswaplab' directory inside 'models' directory
5 | MODELS_DIR = os.path.abspath(os.path.join("models", "faceswaplab"))
6 | # Defining the absolute path for the 'analysers' directory inside 'MODELS_DIR'
7 | ANALYZER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "analysers"))
8 | # Defining the absolute path for the 'parser' directory inside 'MODELS_DIR'
9 | FACE_PARSER_DIR = os.path.abspath(os.path.join(MODELS_DIR, "parser"))
10 | # Defining the absolute path for the 'faces' directory inside 'MODELS_DIR'
11 | FACES_DIR = os.path.abspath(os.path.join(MODELS_DIR, "faces"))
12 |
13 | # Constructing the path for 'references' directory inside the 'extensions' and 'sd-webui-faceswaplab' directories, based on the base directory of scripts
14 | REFERENCE_PATH = os.path.join(
15 | scripts.basedir(), "extensions", "sd-webui-faceswaplab", "references"
16 | )
17 |
18 | # Defining the version flag for the application
19 | VERSION_FLAG: str = "v1.2.7"
20 | # Defining the path for 'sd-webui-faceswaplab' inside the 'extensions' directory
21 | EXTENSION_PATH = os.path.join("extensions", "sd-webui-faceswaplab")
22 |
23 | # Defining the expected SHA1 hash value for 'INSWAPPER'
24 | EXPECTED_INSWAPPER_SHA1 = "17a64851eaefd55ea597ee41e5c18409754244c5"
25 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_inpainting/faceswaplab_inpainting.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import List, Optional
3 | import gradio as gr
4 | from client_api import api_utils
5 |
6 |
7 | @dataclass
8 | class InpaintingOptions:
9 | inpainting_denoising_strengh: float = 0
10 | inpainting_prompt: str = ""
11 | inpainting_negative_prompt: str = ""
12 | inpainting_steps: int = 20
13 | inpainting_sampler: str = "Euler"
14 | inpainting_model: str = "Current"
15 | inpainting_seed: int = -1
16 |
17 | @staticmethod
18 | def from_gradio(components: List[gr.components.Component]) -> "InpaintingOptions":
19 | return InpaintingOptions(*components) # type: ignore
20 |
21 | @staticmethod
22 | def from_api_dto(dto: Optional[api_utils.InpaintingOptions]) -> "InpaintingOptions":
23 | """
24 | Converts a InpaintingOptions object from an API DTO (Data Transfer Object).
25 |
26 | :param options: An object of api_utils.InpaintingOptions representing the
27 | post-processing options as received from the API.
28 | :return: A InpaintingOptions instance containing the translated values
29 | from the API DTO.
30 | """
31 | if dto is None:
32 | # Return default values
33 | return InpaintingOptions()
34 |
35 | return InpaintingOptions(
36 | inpainting_denoising_strengh=dto.inpainting_denoising_strengh,
37 | inpainting_prompt=dto.inpainting_prompt,
38 | inpainting_negative_prompt=dto.inpainting_negative_prompt,
39 | inpainting_steps=dto.inpainting_steps,
40 | inpainting_sampler=dto.inpainting_sampler,
41 | inpainting_model=dto.inpainting_model,
42 | inpainting_seed=dto.inpainting_seed,
43 | )
44 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_inpainting/i2i_pp.py:
--------------------------------------------------------------------------------
1 | from scripts.faceswaplab_inpainting.faceswaplab_inpainting import InpaintingOptions
2 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
3 | from PIL import Image
4 | from modules import shared
5 | from scripts.faceswaplab_utils import imgutils
6 | from modules import shared, processing
7 | from modules.processing import StableDiffusionProcessingImg2Img
8 | from modules import sd_models
9 | import traceback
10 | from scripts.faceswaplab_swapping import swapper
11 | from scripts.faceswaplab_utils.typing import *
12 | from typing import *
13 |
14 |
15 | def img2img_diffusion(
16 | img: PILImage, options: InpaintingOptions, faces: Optional[List[Face]] = None
17 | ) -> Image.Image:
18 | if not options or options.inpainting_denoising_strengh == 0:
19 | logger.info("Discard inpainting denoising strength is 0 or no inpainting")
20 | return img
21 |
22 | try:
23 | logger.info(
24 | f"""Inpainting face
25 | Sampler : {options.inpainting_sampler}
26 | inpainting_denoising_strength : {options.inpainting_denoising_strengh}
27 | inpainting_steps : {options.inpainting_steps}
28 | """
29 | )
30 | if not isinstance(options.inpainting_sampler, str):
31 | options.inpainting_sampler = "Euler"
32 |
33 | logger.info("send faces to image to image")
34 | img = img.copy()
35 |
36 | if not faces:
37 | faces = swapper.get_faces(imgutils.pil_to_cv2(img))
38 |
39 | if faces:
40 | for face in faces:
41 | bbox = face.bbox.astype(int)
42 | mask = imgutils.create_mask(img, bbox)
43 | prompt = options.inpainting_prompt.replace(
44 | "[gender]", "man" if face["gender"] == 1 else "woman"
45 | )
46 | negative_prompt = options.inpainting_negative_prompt.replace(
47 | "[gender]", "man" if face["gender"] == 1 else "woman"
48 | )
49 | logger.info("Denoising prompt : %s", prompt)
50 | logger.info(
51 | "Denoising strenght : %s", options.inpainting_denoising_strengh
52 | )
53 |
54 | i2i_kwargs = {
55 | "init_images": [img],
56 | "sampler_name": options.inpainting_sampler,
57 | "do_not_save_samples": True,
58 | "steps": options.inpainting_steps,
59 | "width": img.width,
60 | "inpainting_fill": 1,
61 | "inpaint_full_res": True,
62 | "height": img.height,
63 | "mask": mask,
64 | "prompt": prompt,
65 | "negative_prompt": negative_prompt,
66 | "denoising_strength": options.inpainting_denoising_strengh,
67 | "seed": options.inpainting_seed,
68 | }
69 |
70 | current_model_checkpoint = shared.opts.sd_model_checkpoint
71 | if options.inpainting_model and options.inpainting_model != "Current":
72 | # Change checkpoint
73 | shared.opts.sd_model_checkpoint = options.inpainting_model
74 | sd_models.select_checkpoint
75 | sd_models.load_model()
76 | i2i_p = StableDiffusionProcessingImg2Img(**i2i_kwargs)
77 | i2i_processed = processing.process_images(i2i_p)
78 | if options.inpainting_model and options.inpainting_model != "Current":
79 | # Restore checkpoint
80 | shared.opts.sd_model_checkpoint = current_model_checkpoint
81 | sd_models.select_checkpoint
82 | sd_models.load_model()
83 |
84 | images = i2i_processed.images
85 | if len(images) > 0:
86 | img = images[0]
87 | return img
88 | except Exception as e:
89 | logger.error("Failed to apply inpainting to face : %s", e)
90 | traceback.print_exc()
91 | raise e
92 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_postprocessing/postprocessing.py:
--------------------------------------------------------------------------------
1 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
2 | from PIL import Image
3 | from scripts.faceswaplab_postprocessing.postprocessing_options import (
4 | PostProcessingOptions,
5 | InpaintingWhen,
6 | )
7 | from scripts.faceswaplab_inpainting.i2i_pp import img2img_diffusion
8 | from scripts.faceswaplab_postprocessing.upscaling import upscale_img, restore_face
9 | import traceback
10 |
11 |
12 | def enhance_image(image: Image.Image, pp_options: PostProcessingOptions) -> Image.Image:
13 | result_image = image
14 | try:
15 | logger.debug("enhance_image, inpainting : %s", pp_options.inpainting_when)
16 | result_image = image
17 |
18 | if (
19 | pp_options.inpainting_when == InpaintingWhen.BEFORE_UPSCALING.value
20 | or pp_options.inpainting_when == InpaintingWhen.BEFORE_UPSCALING
21 | ):
22 | logger.debug("Inpaint before upscale")
23 | result_image = img2img_diffusion(
24 | img=result_image, options=pp_options.inpainting_options
25 | )
26 | result_image = upscale_img(result_image, pp_options)
27 |
28 | if (
29 | pp_options.inpainting_when == InpaintingWhen.BEFORE_RESTORE_FACE.value
30 | or pp_options.inpainting_when == InpaintingWhen.BEFORE_RESTORE_FACE
31 | ):
32 | logger.debug("Inpaint before restore")
33 | result_image = img2img_diffusion(
34 | result_image, pp_options.inpainting_options
35 | )
36 |
37 | result_image = restore_face(result_image, pp_options)
38 |
39 | if (
40 | pp_options.inpainting_when == InpaintingWhen.AFTER_ALL.value
41 | or pp_options.inpainting_when == InpaintingWhen.AFTER_ALL
42 | ):
43 | logger.debug("Inpaint after all")
44 | result_image = img2img_diffusion(
45 | result_image, pp_options.inpainting_options
46 | )
47 |
48 | except Exception as e:
49 | logger.error("Failed to post-process %s", e)
50 | traceback.print_exc()
51 | return result_image
52 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_postprocessing/postprocessing_options.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from modules.face_restoration import FaceRestoration
3 | from modules.upscaler import UpscalerData
4 | from dataclasses import dataclass
5 | from modules import shared
6 | from enum import Enum
7 | from scripts.faceswaplab_inpainting.faceswaplab_inpainting import InpaintingOptions
8 | from client_api import api_utils
9 |
10 |
11 | class InpaintingWhen(Enum):
12 | NEVER = "Never"
13 | BEFORE_UPSCALING = "Before Upscaling/all"
14 | BEFORE_RESTORE_FACE = "After Upscaling/Before Restore Face"
15 | AFTER_ALL = "After All"
16 |
17 |
18 | @dataclass
19 | class PostProcessingOptions:
20 | face_restorer_name: str = ""
21 | restorer_visibility: float = 0.5
22 | codeformer_weight: float = 1
23 |
24 | upscaler_name: str = ""
25 | scale: float = 1
26 | upscale_visibility: float = 0.5
27 |
28 | inpainting_when: InpaintingWhen = InpaintingWhen.BEFORE_UPSCALING
29 |
30 | # (Don't use optional for this or gradio parsing will fail) :
31 | inpainting_options: InpaintingOptions = None # type: ignore
32 |
33 | @property
34 | def upscaler(self) -> Optional[UpscalerData]:
35 | for upscaler in shared.sd_upscalers:
36 | if upscaler.name == self.upscaler_name:
37 | return upscaler
38 | return None
39 |
40 | @property
41 | def face_restorer(self) -> Optional[FaceRestoration]:
42 | for face_restorer in shared.face_restorers:
43 | if face_restorer.name() == self.face_restorer_name:
44 | return face_restorer
45 | return None
46 |
47 | @staticmethod
48 | def from_api_dto(
49 | options: api_utils.PostProcessingOptions,
50 | ) -> "PostProcessingOptions":
51 | """
52 | Converts a PostProcessingOptions object from an API DTO (Data Transfer Object).
53 |
54 | :param options: An object of api_utils.PostProcessingOptions representing the
55 | post-processing options as received from the API.
56 | :return: A PostProcessingOptions instance containing the translated values
57 | from the API DTO.
58 | """
59 | return PostProcessingOptions(
60 | face_restorer_name=options.face_restorer_name,
61 | restorer_visibility=options.restorer_visibility,
62 | codeformer_weight=options.codeformer_weight,
63 | upscaler_name=options.upscaler_name,
64 | scale=options.scale,
65 | upscale_visibility=options.upscaler_visibility,
66 | inpainting_when=InpaintingWhen(options.inpainting_when.value),
67 | inpainting_options=InpaintingOptions.from_api_dto(
68 | options.inpainting_options
69 | ),
70 | )
71 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_postprocessing/upscaling.py:
--------------------------------------------------------------------------------
1 | from scripts.faceswaplab_postprocessing.postprocessing_options import (
2 | PostProcessingOptions,
3 | )
4 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
5 | from PIL import Image
6 | import numpy as np
7 | from modules import codeformer_model
8 | from scripts.faceswaplab_utils.typing import *
9 |
10 |
11 | def upscale_img(image: PILImage, pp_options: PostProcessingOptions) -> PILImage:
12 | if pp_options.upscaler is not None and pp_options.upscaler.name != "None":
13 | original_image: PILImage = image.copy()
14 | logger.info(
15 | "Upscale with %s scale = %s",
16 | pp_options.upscaler.name,
17 | pp_options.scale,
18 | )
19 | result_image = pp_options.upscaler.scaler.upscale(
20 | image, pp_options.scale, pp_options.upscaler.data_path # type: ignore
21 | )
22 |
23 | # FIXME : Could be better (managing images whose dimensions are not multiples of 16)
24 | if pp_options.scale == 1 and original_image.size == result_image.size:
25 | logger.debug(
26 | "Sizes orig=%s, result=%s", original_image.size, result_image.size
27 | )
28 | result_image = Image.blend(
29 | original_image, result_image, pp_options.upscale_visibility
30 | )
31 | return result_image
32 | return image
33 |
34 |
35 | def restore_face(image: Image.Image, pp_options: PostProcessingOptions) -> Image.Image:
36 | if pp_options.face_restorer is not None:
37 | original_image = image.copy()
38 | logger.info("Restore face with %s", pp_options.face_restorer.name())
39 | numpy_image = np.array(image)
40 | if pp_options.face_restorer_name == "CodeFormer":
41 | numpy_image = codeformer_model.codeformer.restore(
42 | numpy_image, w=pp_options.codeformer_weight
43 | )
44 | else:
45 | numpy_image = pp_options.face_restorer.restore(numpy_image)
46 |
47 | restored_image = Image.fromarray(numpy_image)
48 | result_image = Image.blend(
49 | original_image, restored_image, pp_options.restorer_visibility
50 | )
51 | return result_image
52 | return image
53 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_settings/faceswaplab_settings.py:
--------------------------------------------------------------------------------
1 | from scripts.faceswaplab_utils.models_utils import get_swap_models
2 | from modules import script_callbacks, shared
3 | import gradio as gr
4 |
5 |
6 | def on_ui_settings() -> None:
7 | section = ("faceswaplab", "FaceSwapLab")
8 | models = get_swap_models()
9 | shared.opts.add_option(
10 | "faceswaplab_model",
11 | shared.OptionInfo(
12 | models[0] if len(models) > 0 else "None",
13 | "FaceSwapLab FaceSwap Model",
14 | gr.Dropdown,
15 | {"interactive": True, "choices": models},
16 | section=section,
17 | ),
18 | )
19 | shared.opts.add_option(
20 | "faceswaplab_use_gpu",
21 | shared.OptionInfo(
22 | False,
23 | "Use GPU, only for CUDA on Windows/Linux - experimental and risky, can messed up dependencies (requires restart)",
24 | gr.Checkbox,
25 | {"interactive": True},
26 | section=section,
27 | ),
28 | )
29 | shared.opts.add_option(
30 | "faceswaplab_keep_original",
31 | shared.OptionInfo(
32 | False,
33 | "keep original image before swapping",
34 | gr.Checkbox,
35 | {"interactive": True},
36 | section=section,
37 | ),
38 | )
39 | shared.opts.add_option(
40 | "faceswaplab_units_count",
41 | shared.OptionInfo(
42 | 3,
43 | "Max faces units (requires restart)",
44 | gr.Slider,
45 | {"minimum": 1, "maximum": 10, "step": 1},
46 | section=section,
47 | ),
48 | )
49 | shared.opts.add_option(
50 | "faceswaplab_nsfw_threshold",
51 | shared.OptionInfo(
52 | 0.7,
53 | "NSFW score threshold. Any image part with a score above this value will be treated as NSFW (use extension responsibly !). 1=Disable filtering",
54 | gr.Slider,
55 | {"minimum": 0, "maximum": 1, "step": 0.01},
56 | section=section,
57 | ),
58 | )
59 |
60 | shared.opts.add_option(
61 | "faceswaplab_det_size",
62 | shared.OptionInfo(
63 | 640,
64 | "det_size : Size of the detection area for face analysis. Higher values may improve quality but reduce speed. Low value may improve detection of very large face.",
65 | gr.Slider,
66 | {"minimum": 320, "maximum": 640, "step": 320},
67 | section=section,
68 | ),
69 | )
70 |
71 | shared.opts.add_option(
72 | "faceswaplab_auto_det_size",
73 | shared.OptionInfo(
74 | True,
75 | "Auto det_size : Will load model twice and test faces on each if needed (old behaviour). Takes more VRAM. Precedence over fixed det_size",
76 | gr.Checkbox,
77 | {"interactive": True},
78 | section=section,
79 | ),
80 | )
81 |
82 | shared.opts.add_option(
83 | "faceswaplab_detection_threshold",
84 | shared.OptionInfo(
85 | 0.5,
86 | "det_thresh : Face Detection threshold",
87 | gr.Slider,
88 | {"minimum": 0.1, "maximum": 0.99, "step": 0.001},
89 | section=section,
90 | ),
91 | )
92 |
93 | # DEFAULT UI SETTINGS
94 |
95 | shared.opts.add_option(
96 | "faceswaplab_pp_default_face_restorer",
97 | shared.OptionInfo(
98 | None,
99 | "UI Default global post processing face restorer (requires restart)",
100 | gr.Dropdown,
101 | {
102 | "interactive": True,
103 | "choices": ["None"] + [x.name() for x in shared.face_restorers],
104 | },
105 | section=section,
106 | ),
107 | )
108 | shared.opts.add_option(
109 | "faceswaplab_pp_default_face_restorer_visibility",
110 | shared.OptionInfo(
111 | 1,
112 | "UI Default global post processing face restorer visibility (requires restart)",
113 | gr.Slider,
114 | {"minimum": 0, "maximum": 1, "step": 0.001},
115 | section=section,
116 | ),
117 | )
118 | shared.opts.add_option(
119 | "faceswaplab_pp_default_face_restorer_weight",
120 | shared.OptionInfo(
121 | 1,
122 | "UI Default global post processing face restorer weight (requires restart)",
123 | gr.Slider,
124 | {"minimum": 0, "maximum": 1, "step": 0.001},
125 | section=section,
126 | ),
127 | )
128 | shared.opts.add_option(
129 | "faceswaplab_pp_default_upscaler",
130 | shared.OptionInfo(
131 | None,
132 | "UI Default global post processing upscaler (requires restart)",
133 | gr.Dropdown,
134 | {
135 | "interactive": True,
136 | "choices": [upscaler.name for upscaler in shared.sd_upscalers],
137 | },
138 | section=section,
139 | ),
140 | )
141 | shared.opts.add_option(
142 | "faceswaplab_pp_default_upscaler_visibility",
143 | shared.OptionInfo(
144 | 1,
145 | "UI Default global post processing upscaler visibility(requires restart)",
146 | gr.Slider,
147 | {"minimum": 0, "maximum": 1, "step": 0.001},
148 | section=section,
149 | ),
150 | )
151 |
152 | # Inpainting
153 |
154 | shared.opts.add_option(
155 | "faceswaplab_pp_default_inpainting_prompt",
156 | shared.OptionInfo(
157 | "Portrait of a [gender]",
158 | "UI Default inpainting prompt [gender] is replaced by man or woman (requires restart)",
159 | gr.Textbox,
160 | {},
161 | section=section,
162 | ),
163 | )
164 |
165 | shared.opts.add_option(
166 | "faceswaplab_pp_default_inpainting_negative_prompt",
167 | shared.OptionInfo(
168 | "blurry",
169 | "UI Default inpainting negative prompt [gender] (requires restart)",
170 | gr.Textbox,
171 | {},
172 | section=section,
173 | ),
174 | )
175 |
176 | # UPSCALED SWAPPER
177 |
178 | shared.opts.add_option(
179 | "faceswaplab_default_upscaled_swapper_upscaler",
180 | shared.OptionInfo(
181 | None,
182 | "Default Upscaled swapper upscaler (Recommanded : LDSR but slow) (requires restart)",
183 | gr.Dropdown,
184 | {
185 | "interactive": True,
186 | "choices": [upscaler.name for upscaler in shared.sd_upscalers],
187 | },
188 | section=section,
189 | ),
190 | )
191 | shared.opts.add_option(
192 | "faceswaplab_default_upscaled_swapper_sharpen",
193 | shared.OptionInfo(
194 | False,
195 | "Default Upscaled swapper sharpen",
196 | gr.Checkbox,
197 | {"interactive": True},
198 | section=section,
199 | ),
200 | )
201 | shared.opts.add_option(
202 | "faceswaplab_default_upscaled_swapper_fixcolor",
203 | shared.OptionInfo(
204 | False,
205 | "Default Upscaled swapper color corrections (requires restart)",
206 | gr.Checkbox,
207 | {"interactive": True},
208 | section=section,
209 | ),
210 | )
211 | shared.opts.add_option(
212 | "faceswaplab_default_upscaled_swapper_improved_mask",
213 | shared.OptionInfo(
214 | False,
215 | "Default Use improved segmented mask (use pastenet to mask only the face) (requires restart)",
216 | gr.Checkbox,
217 | {"interactive": True},
218 | section=section,
219 | ),
220 | )
221 | shared.opts.add_option(
222 | "faceswaplab_default_upscaled_swapper_face_restorer",
223 | shared.OptionInfo(
224 | None,
225 | "Default Upscaled swapper face restorer (requires restart)",
226 | gr.Dropdown,
227 | {
228 | "interactive": True,
229 | "choices": ["None"] + [x.name() for x in shared.face_restorers],
230 | },
231 | section=section,
232 | ),
233 | )
234 | shared.opts.add_option(
235 | "faceswaplab_default_upscaled_swapper_face_restorer_visibility",
236 | shared.OptionInfo(
237 | 1,
238 | "Default Upscaled swapper face restorer visibility (requires restart)",
239 | gr.Slider,
240 | {"minimum": 0, "maximum": 1, "step": 0.001},
241 | section=section,
242 | ),
243 | )
244 | shared.opts.add_option(
245 | "faceswaplab_default_upscaled_swapper_face_restorer_weight",
246 | shared.OptionInfo(
247 | 1,
248 | "Default Upscaled swapper face restorer weight (codeformer) (requires restart)",
249 | gr.Slider,
250 | {"minimum": 0, "maximum": 1, "step": 0.001},
251 | section=section,
252 | ),
253 | )
254 | shared.opts.add_option(
255 | "faceswaplab_default_upscaled_swapper_erosion",
256 | shared.OptionInfo(
257 | 1,
258 | "Default 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. (requires restart)",
259 | gr.Slider,
260 | {"minimum": 0, "maximum": 10, "step": 0.001},
261 | section=section,
262 | ),
263 | )
264 |
265 |
266 | script_callbacks.on_ui_settings(on_ui_settings)
267 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_swapping/face_checkpoints.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | from typing import *
4 | from insightface.app.common import Face
5 | from safetensors.torch import save_file, safe_open
6 | import torch
7 |
8 | import modules.scripts as scripts
9 | from modules import scripts
10 | from scripts.faceswaplab_swapping.upcaled_inswapper_options import InswappperOptions
11 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
12 | from scripts.faceswaplab_utils.typing import *
13 | from scripts.faceswaplab_utils import imgutils
14 | from scripts.faceswaplab_utils.models_utils import get_swap_models
15 | import traceback
16 |
17 | from scripts.faceswaplab_swapping import swapper
18 | from pprint import pformat
19 | import re
20 | from client_api import api_utils
21 | import tempfile
22 |
23 |
24 | def sanitize_name(name: str) -> str:
25 | """
26 | Sanitize the input name by removing special characters and replacing spaces with underscores.
27 |
28 | Parameters:
29 | name (str): The input name to be sanitized.
30 |
31 | Returns:
32 | str: The sanitized name with special characters removed and spaces replaced by underscores.
33 | """
34 | name = re.sub("[^A-Za-z0-9_. ]+", "", name)
35 | name = name.replace(" ", "_")
36 | return name[:255]
37 |
38 |
39 | def build_face_checkpoint_and_save(
40 | images: List[PILImage],
41 | name: str,
42 | gender: Gender = Gender.AUTO,
43 | overwrite: bool = False,
44 | path: Optional[str] = None,
45 | ) -> Optional[PILImage]:
46 | """
47 | Builds a face checkpoint using the provided image files, performs face swapping,
48 | and saves the result to a file. If a blended face is successfully obtained and the face swapping
49 | process succeeds, the resulting image is returned. Otherwise, None is returned.
50 |
51 | Args:
52 | batch_files (list): List of image file paths used to create the face checkpoint.
53 | name (str): The name assigned to the face checkpoint.
54 |
55 | Returns:
56 | PIL.PILImage or None: The resulting swapped face image if the process is successful; None otherwise.
57 | """
58 |
59 | try:
60 | name = sanitize_name(name)
61 | images = images or []
62 | logger.info("Build %s with %s images", name, len(images))
63 | faces: List[Face] = swapper.get_faces_from_img_files(images=images)
64 | if faces is None or len(faces) == 0:
65 | logger.error("No source faces found")
66 | return None
67 |
68 | blended_face: Optional[Face] = swapper.blend_faces(faces, gender=gender)
69 | preview_path = os.path.join(
70 | scripts.basedir(), "extensions", "sd-webui-faceswaplab", "references"
71 | )
72 |
73 | reference_preview_img: PILImage
74 | if blended_face:
75 | if blended_face["gender"] == 0:
76 | reference_preview_img = Image.open(
77 | os.path.join(preview_path, "woman.png")
78 | )
79 | else:
80 | reference_preview_img = Image.open(
81 | os.path.join(preview_path, "man.png")
82 | )
83 |
84 | if name == "":
85 | name = "default_name"
86 | logger.debug("Face %s", pformat(blended_face))
87 | target_face = swapper.get_or_default(
88 | swapper.get_faces(imgutils.pil_to_cv2(reference_preview_img)), 0, None
89 | )
90 | if target_face is None:
91 | logger.error(
92 | "Failed to open reference image, cannot create preview : That should not happen unless you deleted the references folder or change the detection threshold."
93 | )
94 | else:
95 | result: swapper.ImageResult = swapper.swap_face(
96 | target_faces=[target_face],
97 | source_face=blended_face,
98 | target_img=reference_preview_img,
99 | model=get_swap_models()[0],
100 | swapping_options=InswappperOptions(
101 | face_restorer_name="CodeFormer",
102 | restorer_visibility=1,
103 | upscaler_name="Lanczos",
104 | codeformer_weight=1,
105 | improved_mask=True,
106 | color_corrections=False,
107 | sharpen=True,
108 | ),
109 | )
110 | preview_image = result.image
111 |
112 | if path:
113 | file_path = path
114 | else:
115 | file_path = os.path.join(
116 | get_checkpoint_path(), f"{name}.safetensors"
117 | )
118 | if not overwrite:
119 | file_number = 1
120 | while os.path.exists(file_path):
121 | file_path = os.path.join(
122 | get_checkpoint_path(),
123 | f"{name}_{file_number}.safetensors",
124 | )
125 | file_number += 1
126 | save_face(filename=file_path, face=blended_face)
127 | preview_image.save(file_path + ".png")
128 | try:
129 | data = load_face(file_path)
130 | logger.debug(data)
131 | except Exception as e:
132 | logger.error("Error loading checkpoint, after creation %s", e)
133 | traceback.print_exc()
134 |
135 | return preview_image
136 |
137 | else:
138 | logger.error("No face found")
139 | return None # type: ignore
140 | except Exception as e:
141 | logger.error("Failed to build checkpoint %s", e)
142 | traceback.print_exc()
143 | return None
144 |
145 |
146 | def save_face(face: Face, filename: str) -> None:
147 | try:
148 | tensors = {
149 | "embedding": torch.tensor(face["embedding"]),
150 | "gender": torch.tensor(face["gender"]),
151 | "age": torch.tensor(face["age"]),
152 | }
153 | save_file(tensors, filename)
154 | except Exception as e:
155 | traceback.print_exc
156 | logger.error("Failed to save checkpoint %s", e)
157 | raise e
158 |
159 |
160 | def load_face(name: str) -> Optional[Face]:
161 | if name.startswith("data:application/face;base64,"):
162 | with tempfile.NamedTemporaryFile(delete=True) as temp_file:
163 | api_utils.base64_to_safetensors(name, temp_file.name)
164 | face = {}
165 | with safe_open(temp_file.name, framework="pt", device="cpu") as f:
166 | for k in f.keys():
167 | logger.debug("load key %s", k)
168 | face[k] = f.get_tensor(k).numpy()
169 | return Face(face)
170 |
171 | filename = matching_checkpoint(name)
172 | if filename is None:
173 | return None
174 |
175 | if filename.endswith(".pkl"):
176 | logger.warning(
177 | "Pkl files for faces are deprecated to enhance safety, you need to convert them"
178 | )
179 | logger.warning("The file will be converted to .safetensors")
180 | logger.warning(
181 | "You can also use this script https://gist.github.com/glucauze/4a3c458541f2278ad801f6625e5b9d3d"
182 | )
183 | return None
184 |
185 | elif filename.endswith(".safetensors"):
186 | face = {}
187 | with safe_open(filename, framework="pt", device="cpu") as f:
188 | for k in f.keys():
189 | logger.debug("load key %s", k)
190 | face[k] = f.get_tensor(k).numpy()
191 | return Face(face)
192 |
193 | raise NotImplementedError("Unknown file type, face extraction not implemented")
194 |
195 |
196 | def get_checkpoint_path() -> str:
197 | checkpoint_path = os.path.join(scripts.basedir(), "models", "faceswaplab", "faces")
198 | os.makedirs(checkpoint_path, exist_ok=True)
199 | return checkpoint_path
200 |
201 |
202 | def matching_checkpoint(name: str) -> Optional[str]:
203 | """
204 | Retrieve the full path of a checkpoint file matching the given name.
205 |
206 | If the name already includes a path separator, it is returned as-is. Otherwise, the function looks for a matching
207 | file with the extensions ".safetensors" or ".pkl" in the checkpoint directory.
208 |
209 | Args:
210 | name (str): The name or path of the checkpoint file.
211 |
212 | Returns:
213 | Optional[str]: The full path of the matching checkpoint file, or None if no match is found.
214 | """
215 |
216 | # If the name already includes a path separator, return it as is
217 | if os.path.sep in name:
218 | return name
219 |
220 | # If the name doesn't end with the specified extensions, look for a matching file
221 | if not (name.endswith(".safetensors") or name.endswith(".pkl")):
222 | # Try appending each extension and check if the file exists in the checkpoint path
223 | for ext in [".safetensors", ".pkl"]:
224 | full_path = os.path.join(get_checkpoint_path(), name + ext)
225 | if os.path.exists(full_path):
226 | return full_path
227 | # If no matching file is found, return None
228 | return None
229 |
230 | # If the name already ends with the specified extensions, simply complete the path
231 | return os.path.join(get_checkpoint_path(), name)
232 |
233 |
234 | def get_face_checkpoints() -> List[str]:
235 | """
236 | Retrieve a list of face checkpoint paths.
237 |
238 | This function searches for face files with the extension ".safetensors" in the specified directory and returns a list
239 | containing the paths of those files.
240 |
241 | Returns:
242 | list: A list of face paths, including the string "None" as the first element.
243 | """
244 | faces_path = os.path.join(get_checkpoint_path(), "*.safetensors")
245 | faces = glob.glob(faces_path)
246 |
247 | faces_path = os.path.join(get_checkpoint_path(), "*.pkl")
248 | faces += glob.glob(faces_path)
249 |
250 | return ["None"] + [os.path.basename(face) for face in sorted(faces)]
251 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_swapping/facemask.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import torch
4 | from torchvision.transforms.functional import normalize
5 | from scripts.faceswaplab_swapping.parsing import init_parsing_model
6 | from functools import lru_cache
7 | from typing import Union, List
8 | from torch import device as torch_device
9 |
10 |
11 | @lru_cache
12 | def get_parsing_model(device: torch_device) -> torch.nn.Module:
13 | """
14 | Returns an instance of the parsing model.
15 | The returned model is cached for faster subsequent access.
16 |
17 | Args:
18 | device: The torch device to use for computations.
19 |
20 | Returns:
21 | The parsing model.
22 | """
23 | return init_parsing_model(device=device) # type: ignore
24 |
25 |
26 | def convert_image_to_tensor(
27 | images: Union[np.ndarray, List[np.ndarray]],
28 | convert_bgr_to_rgb: bool = True,
29 | use_float32: bool = True,
30 | ) -> Union[torch.Tensor, List[torch.Tensor]]:
31 | """
32 | Converts an image or a list of images to PyTorch tensor.
33 |
34 | Args:
35 | images: An image or a list of images in numpy.ndarray format.
36 | convert_bgr_to_rgb: A boolean flag indicating if the conversion from BGR to RGB should be performed.
37 | use_float32: A boolean flag indicating if the tensor should be converted to float32.
38 |
39 | Returns:
40 | PyTorch tensor or a list of PyTorch tensors.
41 | """
42 |
43 | def _convert_single_image_to_tensor(
44 | image: np.ndarray, convert_bgr_to_rgb: bool, use_float32: bool
45 | ) -> torch.Tensor:
46 | if image.shape[2] == 3 and convert_bgr_to_rgb:
47 | if image.dtype == "float64":
48 | image = image.astype("float32")
49 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
50 | image_tensor = torch.from_numpy(image.transpose(2, 0, 1))
51 | if use_float32:
52 | image_tensor = image_tensor.float()
53 | return image_tensor
54 |
55 | if isinstance(images, list):
56 | return [
57 | _convert_single_image_to_tensor(image, convert_bgr_to_rgb, use_float32)
58 | for image in images
59 | ]
60 | else:
61 | return _convert_single_image_to_tensor(images, convert_bgr_to_rgb, use_float32)
62 |
63 |
64 | def generate_face_mask(face_image: np.ndarray, device: torch.device) -> np.ndarray:
65 | """
66 | Generates a face mask given a face image.
67 |
68 | Args:
69 | face_image: The face image in numpy.ndarray format.
70 | device: The torch device to use for computations.
71 |
72 | Returns:
73 | The face mask as a numpy.ndarray.
74 | """
75 | # Resize the face image for the model
76 | resized_face_image = cv2.resize(
77 | face_image, (512, 512), interpolation=cv2.INTER_LINEAR
78 | )
79 |
80 | # Preprocess the image
81 | face_input = convert_image_to_tensor(
82 | (resized_face_image.astype("float32") / 255.0),
83 | convert_bgr_to_rgb=True,
84 | use_float32=True,
85 | )
86 | normalize(face_input, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) # type: ignore
87 | assert isinstance(face_input, torch.Tensor)
88 | face_input = torch.unsqueeze(face_input, 0).to(device)
89 |
90 | # Pass the image through the model
91 | with torch.no_grad():
92 | model_output = get_parsing_model(device)(face_input)[0]
93 | model_output = model_output.argmax(dim=1).squeeze().cpu().numpy()
94 |
95 | # Generate the mask from the model output
96 | parse_mask = np.zeros(model_output.shape)
97 | MASK_COLOR_MAP = [
98 | 0,
99 | 255,
100 | 255,
101 | 255,
102 | 255,
103 | 255,
104 | 255,
105 | 255,
106 | 255,
107 | 255,
108 | 255,
109 | 255,
110 | 255,
111 | 255,
112 | 0,
113 | 255,
114 | 0,
115 | 0,
116 | 0,
117 | ]
118 | for idx, color in enumerate(MASK_COLOR_MAP):
119 | parse_mask[model_output == idx] = color
120 |
121 | # Resize the mask to match the original image
122 | face_mask = cv2.resize(parse_mask, (face_image.shape[1], face_image.shape[0]))
123 |
124 | return face_mask
125 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_swapping/parsing/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Code from codeformer https://github.com/sczhou/CodeFormer
3 |
4 | S-Lab License 1.0
5 |
6 | Copyright 2022 S-Lab
7 |
8 | Redistribution and use for non-commercial purpose in source and
9 | binary forms, with or without modification, are permitted provided
10 | that the following conditions are met:
11 |
12 | 1. Redistributions of source code must retain the above copyright
13 | notice, this list of conditions and the following disclaimer.
14 |
15 | 2. Redistributions in binary form must reproduce the above copyright
16 | notice, this list of conditions and the following disclaimer in
17 | the documentation and/or other materials provided with the
18 | distribution.
19 |
20 | 3. Neither the name of the copyright holder nor the names of its
21 | contributors may be used to endorse or promote products derived
22 | from this software without specific prior written permission.
23 |
24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 |
36 | In the event that redistribution and/or use for commercial purpose in
37 | source or binary forms, with or without modification is required,
38 | please contact the contributor(s) of the work.
39 | """
40 |
41 |
42 | import torch
43 | import os
44 | import torch
45 | from torch.hub import download_url_to_file, get_dir
46 | from .parsenet import ParseNet
47 | from urllib.parse import urlparse
48 | from scripts.faceswaplab_globals import FACE_PARSER_DIR
49 |
50 | ROOT_DIR = FACE_PARSER_DIR
51 |
52 |
53 | def load_file_from_url(url: str, model_dir=None, progress=True, file_name=None):
54 | """Ref:https://github.com/1adrianb/face-alignment/blob/master/face_alignment/utils.py"""
55 | if model_dir is None:
56 | hub_dir = get_dir()
57 | model_dir = os.path.join(hub_dir, "checkpoints")
58 |
59 | os.makedirs(os.path.join(ROOT_DIR, model_dir), exist_ok=True)
60 |
61 | parts = urlparse(url)
62 | filename = os.path.basename(parts.path)
63 | if file_name is not None:
64 | filename = file_name
65 | cached_file = os.path.abspath(os.path.join(ROOT_DIR, model_dir, filename))
66 | if not os.path.exists(cached_file):
67 | print(f'Downloading: "{url}" to {cached_file}\n')
68 | download_url_to_file(url, cached_file, hash_prefix=None, progress=progress)
69 | return cached_file
70 |
71 |
72 | def init_parsing_model(device="cuda"):
73 | model = ParseNet(in_size=512, out_size=512, parsing_ch=19)
74 | model_url = "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth"
75 | model_path = load_file_from_url(
76 | url=model_url, model_dir="weights/facelib", progress=True, file_name=None
77 | )
78 | load_net = torch.load(model_path, map_location=lambda storage, loc: storage)
79 | model.load_state_dict(load_net, strict=True)
80 | model.eval()
81 | model = model.to(device)
82 | return model
83 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_swapping/upcaled_inswapper_options.py:
--------------------------------------------------------------------------------
1 | from dataclasses import *
2 | from typing import Optional
3 | from client_api import api_utils
4 |
5 |
6 | @dataclass
7 | class InswappperOptions:
8 | face_restorer_name: Optional[str] = None
9 | restorer_visibility: float = 1
10 | codeformer_weight: float = 1
11 | upscaler_name: Optional[str] = None
12 | improved_mask: bool = False
13 | color_corrections: bool = False
14 | sharpen: bool = False
15 | erosion_factor: float = 1.0
16 |
17 | @staticmethod
18 | def from_api_dto(dto: Optional[api_utils.InswappperOptions]) -> "InswappperOptions":
19 | """
20 | Converts a InpaintingOptions object from an API DTO (Data Transfer Object).
21 |
22 | :param options: An object of api_utils.InpaintingOptions representing the
23 | post-processing options as received from the API.
24 | :return: A InpaintingOptions instance containing the translated values
25 | from the API DTO.
26 | """
27 | if dto is None:
28 | return InswappperOptions()
29 |
30 | return InswappperOptions(
31 | face_restorer_name=dto.face_restorer_name,
32 | restorer_visibility=dto.restorer_visibility,
33 | codeformer_weight=dto.codeformer_weight,
34 | upscaler_name=dto.upscaler_name,
35 | improved_mask=dto.improved_mask,
36 | color_corrections=dto.color_corrections,
37 | sharpen=dto.sharpen,
38 | erosion_factor=dto.erosion_factor,
39 | )
40 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_ui/faceswaplab_inpainting_ui.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import gradio as gr
3 | from modules import sd_models, sd_samplers
4 | from scripts.faceswaplab_utils.sd_utils import get_sd_option
5 |
6 |
7 | def face_inpainting_ui(id_prefix: str = "faceswaplab") -> List[gr.components.Component]:
8 | inpainting_denoising_strength = gr.Slider(
9 | 0,
10 | 1,
11 | 0,
12 | step=0.01,
13 | elem_id=f"{id_prefix}_pp_inpainting_denoising_strength",
14 | label="Denoising strenght",
15 | )
16 |
17 | inpainting_denoising_prompt = gr.Textbox(
18 | get_sd_option(
19 | "faceswaplab_pp_default_inpainting_prompt", "Portrait of a [gender]"
20 | ),
21 | elem_id=f"{id_prefix}_pp_inpainting_denoising_prompt",
22 | label="Inpainting prompt use [gender] instead of men or woman",
23 | )
24 | inpainting_denoising_negative_prompt = gr.Textbox(
25 | get_sd_option("faceswaplab_pp_default_inpainting_negative_prompt", "blurry"),
26 | elem_id=f"{id_prefix}_pp_inpainting_denoising_neg_prompt",
27 | label="Inpainting negative prompt use [gender] instead of men or woman",
28 | )
29 | with gr.Row():
30 | samplers_names = [s.name for s in sd_samplers.all_samplers]
31 | inpainting_sampler = gr.Dropdown(
32 | choices=samplers_names,
33 | value=[samplers_names[0]],
34 | label="Inpainting Sampler",
35 | elem_id=f"{id_prefix}_pp_inpainting_sampler",
36 | )
37 | inpainting_denoising_steps = gr.Slider(
38 | 1,
39 | 150,
40 | 20,
41 | step=1,
42 | label="Inpainting steps",
43 | elem_id=f"{id_prefix}_pp_inpainting_steps",
44 | )
45 |
46 | inpaiting_model = gr.Dropdown(
47 | choices=["Current"] + sd_models.checkpoint_tiles(),
48 | default="Current",
49 | label="sd model (experimental)",
50 | elem_id=f"{id_prefix}_pp_inpainting_sd_model",
51 | )
52 |
53 | inpaiting_seed = gr.Number(
54 | label="Inpainting seed",
55 | value=0,
56 | minimum=0,
57 | precision=0,
58 | elem_id=f"{id_prefix}_pp_inpainting_seed",
59 | )
60 |
61 | gradio_components: List[gr.components.Component] = [
62 | inpainting_denoising_strength,
63 | inpainting_denoising_prompt,
64 | inpainting_denoising_negative_prompt,
65 | inpainting_denoising_steps,
66 | inpainting_sampler,
67 | inpaiting_model,
68 | inpaiting_seed,
69 | ]
70 |
71 | for component in gradio_components:
72 | setattr(component, "do_not_save_to_config", True)
73 |
74 | return gradio_components
75 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_ui/faceswaplab_postprocessing_ui.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import gradio as gr
3 | from modules import shared
4 | from scripts.faceswaplab_postprocessing.postprocessing_options import InpaintingWhen
5 | from scripts.faceswaplab_utils.sd_utils import get_sd_option
6 | from scripts.faceswaplab_ui.faceswaplab_inpainting_ui import face_inpainting_ui
7 |
8 |
9 | def postprocessing_ui() -> List[gr.components.Component]:
10 | with gr.Tab(f"Global Post-Processing"):
11 | gr.Markdown(
12 | """Upscaling is performed on the whole image and all faces (including not swapped). Upscaling happens before face restoration."""
13 | )
14 | with gr.Row():
15 | face_restorer_name = gr.Radio(
16 | label="Restore Face",
17 | choices=["None"] + [x.name() for x in shared.face_restorers],
18 | value=get_sd_option(
19 | "faceswaplab_pp_default_face_restorer",
20 | shared.face_restorers[0].name(),
21 | ),
22 | type="value",
23 | elem_id="faceswaplab_pp_face_restorer",
24 | )
25 |
26 | with gr.Column():
27 | face_restorer_visibility = gr.Slider(
28 | 0,
29 | 1,
30 | value=get_sd_option(
31 | "faceswaplab_pp_default_face_restorer_visibility", 1
32 | ),
33 | step=0.001,
34 | label="Restore visibility",
35 | elem_id="faceswaplab_pp_face_restorer_visibility",
36 | )
37 | codeformer_weight = gr.Slider(
38 | 0,
39 | 1,
40 | value=get_sd_option(
41 | "faceswaplab_pp_default_face_restorer_weight", 1
42 | ),
43 | step=0.001,
44 | label="codeformer weight",
45 | elem_id="faceswaplab_pp_face_restorer_weight",
46 | )
47 | upscaler_name = gr.Dropdown(
48 | choices=[upscaler.name for upscaler in shared.sd_upscalers],
49 | value=get_sd_option("faceswaplab_pp_default_upscaler", "None"),
50 | label="Upscaler",
51 | elem_id="faceswaplab_pp_upscaler",
52 | )
53 | upscaler_scale = gr.Slider(
54 | 1,
55 | 8,
56 | 1,
57 | step=0.1,
58 | label="Upscaler scale",
59 | elem_id="faceswaplab_pp_upscaler_scale",
60 | )
61 | upscaler_visibility = gr.Slider(
62 | 0,
63 | 1,
64 | value=get_sd_option("faceswaplab_pp_default_upscaler_visibility", 1),
65 | step=0.1,
66 | label="Upscaler visibility (if scale = 1)",
67 | elem_id="faceswaplab_pp_upscaler_visibility",
68 | )
69 |
70 | with gr.Accordion(label="Global-Inpainting (all faces)", open=False):
71 | gr.Markdown(
72 | "Inpainting sends image to inpainting with a mask on face (once for each faces)."
73 | )
74 | inpainting_when = gr.Dropdown(
75 | elem_id="faceswaplab_pp_inpainting_when",
76 | choices=[e.value for e in InpaintingWhen.__members__.values()],
77 | value=[InpaintingWhen.BEFORE_RESTORE_FACE.value],
78 | label="Enable/When",
79 | )
80 | global_inpainting = face_inpainting_ui("faceswaplab_gpp")
81 |
82 | components = [
83 | face_restorer_name,
84 | face_restorer_visibility,
85 | codeformer_weight,
86 | upscaler_name,
87 | upscaler_scale,
88 | upscaler_visibility,
89 | inpainting_when,
90 | ] + global_inpainting
91 |
92 | # Ask sd to not store in ui-config.json
93 | for component in components:
94 | setattr(component, "do_not_save_to_config", True)
95 | return components
96 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_ui/faceswaplab_unit_settings.py:
--------------------------------------------------------------------------------
1 | from scripts.faceswaplab_swapping import swapper
2 | import base64
3 | import io
4 | from dataclasses import dataclass
5 | from typing import List, Optional, Set, Union
6 | import gradio as gr
7 | from insightface.app.common import Face
8 | from PIL import Image
9 | from scripts.faceswaplab_swapping.upcaled_inswapper_options import InswappperOptions
10 | from scripts.faceswaplab_utils.imgutils import pil_to_cv2
11 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
12 | from scripts.faceswaplab_swapping import face_checkpoints
13 | from scripts.faceswaplab_inpainting.faceswaplab_inpainting import InpaintingOptions
14 | from client_api import api_utils
15 |
16 |
17 | @dataclass
18 | class FaceSwapUnitSettings:
19 | # ORDER of parameters is IMPORTANT. It should match the result of faceswap_unit_ui
20 |
21 | # The image given in reference
22 | source_img: Optional[Union[Image.Image, str]]
23 | # The checkpoint file
24 | source_face: Optional[str]
25 | # The batch source images
26 | _batch_files: Optional[Union[gr.components.File, List[Image.Image]]]
27 | # Will blend faces if True
28 | blend_faces: bool
29 | # Enable this unit
30 | enable: bool
31 | # Use same gender filtering
32 | same_gender: bool
33 | # Sort faces by their size (from larger to smaller)
34 | sort_by_size: bool
35 | # If True, discard images with low similarity
36 | check_similarity: bool
37 | # if True will compute similarity and add it to the image info
38 | _compute_similarity: bool
39 |
40 | # Minimum similarity against the used face (reference, batch or checkpoint)
41 | min_sim: float
42 | # Minimum similarity against the reference (reference or checkpoint if checkpoint is given)
43 | min_ref_sim: float
44 | # The face index to use for swapping
45 | _faces_index: str
46 | # The face index to get image from source
47 | reference_face_index: int
48 |
49 | # Swap in the source image in img2img (before processing)
50 | swap_in_source: bool
51 | # Swap in the generated image in img2img (always on for txt2img)
52 | swap_in_generated: bool
53 | # Pre inpainting configuration (Don't use optional for this or gradio parsing will fail) :
54 | pre_inpainting: InpaintingOptions
55 | # Configure swapping options
56 | swapping_options: InswappperOptions
57 | # Post inpainting configuration (Don't use optional for this or gradio parsing will fail) :
58 | post_inpainting: InpaintingOptions
59 |
60 | @staticmethod
61 | def from_api_dto(dto: api_utils.FaceSwapUnit) -> "FaceSwapUnitSettings":
62 | """
63 | Converts a InpaintingOptions object from an API DTO (Data Transfer Object).
64 |
65 | :param options: An object of api_utils.InpaintingOptions representing the
66 | post-processing options as received from the API.
67 | :return: A InpaintingOptions instance containing the translated values
68 | from the API DTO.
69 | """
70 | return FaceSwapUnitSettings(
71 | source_img=api_utils.base64_to_pil(dto.source_img),
72 | source_face=dto.source_face,
73 | _batch_files=dto.get_batch_images(),
74 | blend_faces=dto.blend_faces,
75 | enable=True,
76 | same_gender=dto.same_gender,
77 | sort_by_size=dto.sort_by_size,
78 | check_similarity=dto.check_similarity,
79 | _compute_similarity=dto.compute_similarity,
80 | min_ref_sim=dto.min_ref_sim,
81 | min_sim=dto.min_sim,
82 | _faces_index=",".join([str(i) for i in (dto.faces_index)]),
83 | reference_face_index=dto.reference_face_index,
84 | swap_in_generated=True,
85 | swap_in_source=False,
86 | pre_inpainting=InpaintingOptions.from_api_dto(dto.pre_inpainting),
87 | swapping_options=InswappperOptions.from_api_dto(dto.swapping_options),
88 | post_inpainting=InpaintingOptions.from_api_dto(dto.post_inpainting),
89 | )
90 |
91 | @property
92 | def faces_index(self) -> Set[int]:
93 | """
94 | Convert _faces_index from str to int
95 | """
96 | faces_index = {
97 | int(x) for x in self._faces_index.strip(",").split(",") if x.isnumeric()
98 | }
99 | if len(faces_index) == 0:
100 | return {0}
101 |
102 | logger.debug("FACES INDEX : %s", faces_index)
103 |
104 | return faces_index
105 |
106 | @property
107 | def compute_similarity(self) -> bool:
108 | return self._compute_similarity or self.check_similarity
109 |
110 | @property
111 | def batch_files(self) -> List[gr.File]:
112 | """
113 | Return empty array instead of None for batch files
114 | """
115 | return self._batch_files or []
116 |
117 | @property
118 | def reference_face(self) -> Optional[Face]:
119 | """
120 | Extract reference face (only once and store it for the rest of processing).
121 | Reference face is the checkpoint or the source image or the first image in the batch in that order.
122 | """
123 | if not hasattr(self, "_reference_face"):
124 | if self.source_face and self.source_face != "None":
125 | try:
126 | logger.info(f"loading face {self.source_face}")
127 | face = face_checkpoints.load_face(self.source_face)
128 | self._reference_face = face
129 | except Exception as e:
130 | logger.error("Failed to load checkpoint : %s", e)
131 | raise e
132 | elif self.source_img is not None:
133 | if isinstance(self.source_img, str): # source_img is a base64 string
134 | if (
135 | "base64," in self.source_img
136 | ): # check if the base64 string has a data URL scheme
137 | base64_data = self.source_img.split("base64,")[-1]
138 | img_bytes = base64.b64decode(base64_data)
139 | else:
140 | # if no data URL scheme, just decode
141 | img_bytes = base64.b64decode(self.source_img)
142 | self.source_img = Image.open(io.BytesIO(img_bytes))
143 | source_img = pil_to_cv2(self.source_img)
144 | self._reference_face = swapper.get_or_default(
145 | swapper.get_faces(source_img), self.reference_face_index, None
146 | )
147 | if self._reference_face is None:
148 | logger.error("Face not found in reference image")
149 | else:
150 | self._reference_face = None
151 |
152 | if self._reference_face is None:
153 | logger.error("You need at least one reference face")
154 | raise Exception("No reference face found")
155 |
156 | return self._reference_face
157 |
158 | @property
159 | def faces(self) -> List[Face]:
160 | """_summary_
161 | Extract all faces (including reference face) to provide an array of faces
162 | Only processed once.
163 | """
164 | if self.batch_files is not None and not hasattr(self, "_faces"):
165 | self._faces = (
166 | [self.reference_face] if self.reference_face is not None else []
167 | )
168 | for file in self.batch_files:
169 | if isinstance(file, Image.Image):
170 | img = file
171 | else:
172 | img = Image.open(file.name) # type: ignore
173 |
174 | face = swapper.get_or_default(
175 | swapper.get_faces(pil_to_cv2(img)), 0, None
176 | )
177 | if face is not None:
178 | self._faces.append(face)
179 | return self._faces
180 |
181 | @property
182 | def blended_faces(self) -> Face:
183 | """
184 | Blend the faces using the mean of all embeddings
185 | """
186 | if not hasattr(self, "_blended_faces"):
187 | self._blended_faces = swapper.blend_faces(self.faces)
188 |
189 | return self._blended_faces
190 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_ui/faceswaplab_unit_ui.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from scripts.faceswaplab_ui.faceswaplab_inpainting_ui import face_inpainting_ui
3 | from scripts.faceswaplab_swapping.face_checkpoints import get_face_checkpoints
4 | import gradio as gr
5 | from modules import shared
6 | from scripts.faceswaplab_utils.sd_utils import get_sd_option
7 |
8 |
9 | def faceswap_unit_advanced_options(
10 | is_img2img: bool, unit_num: int = 1, id_prefix: str = "faceswaplab_"
11 | ) -> List[gr.components.Component]:
12 | with gr.Accordion(f"Post-Processing & Advanced Mask Options", open=False):
13 | gr.Markdown(
14 | """Post-processing and mask settings for unit faces. Best result : checks all, use LDSR, use Codeformer"""
15 | )
16 | with gr.Row():
17 | face_restorer_name = gr.Radio(
18 | label="Restore Face",
19 | choices=["None"] + [x.name() for x in shared.face_restorers],
20 | value=get_sd_option(
21 | "faceswaplab_default_upscaled_swapper_face_restorer",
22 | "None",
23 | ),
24 | type="value",
25 | elem_id=f"{id_prefix}_face{unit_num}_face_restorer",
26 | )
27 | with gr.Column():
28 | face_restorer_visibility = gr.Slider(
29 | 0,
30 | 1,
31 | value=get_sd_option(
32 | "faceswaplab_default_upscaled_swapper_face_restorer_visibility",
33 | 1.0,
34 | ),
35 | step=0.001,
36 | label="Restore visibility",
37 | elem_id=f"{id_prefix}_face{unit_num}_face_restorer_visibility",
38 | )
39 | codeformer_weight = gr.Slider(
40 | 0,
41 | 1,
42 | value=get_sd_option(
43 | "faceswaplab_default_upscaled_swapper_face_restorer_weight", 1.0
44 | ),
45 | step=0.001,
46 | label="codeformer weight",
47 | elem_id=f"{id_prefix}_face{unit_num}_face_restorer_weight",
48 | )
49 | upscaler_name = gr.Dropdown(
50 | choices=[upscaler.name for upscaler in shared.sd_upscalers],
51 | value=get_sd_option("faceswaplab_default_upscaled_swapper_upscaler", ""),
52 | label="Upscaler",
53 | elem_id=f"{id_prefix}_face{unit_num}_upscaler",
54 | )
55 |
56 | improved_mask = gr.Checkbox(
57 | get_sd_option("faceswaplab_default_upscaled_swapper_improved_mask", False),
58 | interactive=True,
59 | label="Use improved segmented mask (use pastenet to mask only the face)",
60 | elem_id=f"{id_prefix}_face{unit_num}_improved_mask",
61 | )
62 | color_corrections = gr.Checkbox(
63 | get_sd_option("faceswaplab_default_upscaled_swapper_fixcolor", False),
64 | interactive=True,
65 | label="Use color corrections",
66 | elem_id=f"{id_prefix}_face{unit_num}_color_corrections",
67 | )
68 | sharpen_face = gr.Checkbox(
69 | get_sd_option("faceswaplab_default_upscaled_swapper_sharpen", False),
70 | interactive=True,
71 | label="sharpen face",
72 | elem_id=f"{id_prefix}_face{unit_num}_sharpen_face",
73 | )
74 | erosion_factor = gr.Slider(
75 | 0.0,
76 | 10.0,
77 | get_sd_option("faceswaplab_default_upscaled_swapper_erosion", 1.0),
78 | step=0.01,
79 | label="Upscaled swapper mask erosion factor, 1 = default behaviour.",
80 | elem_id=f"{id_prefix}_face{unit_num}_erosion_factor",
81 | )
82 |
83 | components = [
84 | face_restorer_name,
85 | face_restorer_visibility,
86 | codeformer_weight,
87 | upscaler_name,
88 | improved_mask,
89 | color_corrections,
90 | sharpen_face,
91 | erosion_factor,
92 | ]
93 |
94 | for component in components:
95 | setattr(component, "do_not_save_to_config", True)
96 |
97 | return components
98 |
99 |
100 | def faceswap_unit_ui(
101 | is_img2img: bool, unit_num: int = 1, id_prefix: str = "faceswaplab"
102 | ) -> List[gr.components.Component]:
103 | with gr.Tab(f"Face {unit_num}"):
104 | with gr.Column():
105 | gr.Markdown(
106 | """Reference is an image. First face will be extracted.
107 | First face of batches sources will be extracted and used as input (or blended if blend is activated)."""
108 | )
109 | with gr.Row():
110 | img = gr.components.Image(
111 | type="pil",
112 | label="Reference",
113 | elem_id=f"{id_prefix}_face{unit_num}_reference_image",
114 | )
115 | batch_files = gr.components.File(
116 | type="file",
117 | file_count="multiple",
118 | label="Batch Sources Images",
119 | optional=True,
120 | elem_id=f"{id_prefix}_face{unit_num}_batch_source_face_files",
121 | )
122 | gr.Markdown(
123 | """Face checkpoint built with the checkpoint builder in tools. Will overwrite reference image."""
124 | )
125 | with gr.Row():
126 | face = gr.Dropdown(
127 | choices=get_face_checkpoints(),
128 | label="Face Checkpoint (precedence over reference face)",
129 | elem_id=f"{id_prefix}_face{unit_num}_face_checkpoint",
130 | )
131 | refresh = gr.Button(
132 | value="↻",
133 | variant="tool",
134 | elem_id=f"{id_prefix}_face{unit_num}_refresh_checkpoints",
135 | )
136 |
137 | def refresh_fn(selected: str):
138 | return gr.Dropdown.update(
139 | value=selected, choices=get_face_checkpoints()
140 | )
141 |
142 | refresh.click(fn=refresh_fn, inputs=face, outputs=face)
143 |
144 | with gr.Row():
145 | enable = gr.Checkbox(
146 | False,
147 | placeholder="enable",
148 | label="Enable",
149 | elem_id=f"{id_prefix}_face{unit_num}_enable",
150 | )
151 | blend_faces = gr.Checkbox(
152 | True,
153 | placeholder="Blend Faces",
154 | label="Blend Faces ((Source|Checkpoint)+References = 1)",
155 | elem_id=f"{id_prefix}_face{unit_num}_blend_faces",
156 | interactive=True,
157 | )
158 |
159 | gr.Markdown(
160 | """Select the face to be swapped, you can sort by size or use the same gender as the desired face:"""
161 | )
162 | with gr.Row():
163 | same_gender = gr.Checkbox(
164 | False,
165 | placeholder="Same Gender",
166 | label="Same Gender",
167 | elem_id=f"{id_prefix}_face{unit_num}_same_gender",
168 | )
169 | sort_by_size = gr.Checkbox(
170 | False,
171 | placeholder="Sort by size",
172 | label="Sort by size (larger>smaller)",
173 | elem_id=f"{id_prefix}_face{unit_num}_sort_by_size",
174 | )
175 | target_faces_index = gr.Textbox(
176 | value=f"{unit_num-1}",
177 | placeholder="Which face to swap (comma separated), start from 0 (by gender if same_gender is enabled)",
178 | label="Target face : Comma separated face number(s)",
179 | elem_id=f"{id_prefix}_face{unit_num}_target_faces_index",
180 | )
181 | gr.Markdown(
182 | """The following will only affect reference face image (and is not affected by sort by size) :"""
183 | )
184 | reference_faces_index = gr.Number(
185 | value=0,
186 | precision=0,
187 | minimum=0,
188 | placeholder="Which face to get from reference image start from 0",
189 | label="Reference source face : start from 0",
190 | elem_id=f"{id_prefix}_face{unit_num}_reference_face_index",
191 | )
192 | gr.Markdown(
193 | """Configure swapping. Swapping can occure before img2img, after or both :""",
194 | visible=is_img2img,
195 | )
196 | swap_in_source = gr.Checkbox(
197 | False,
198 | placeholder="Swap face in source image",
199 | label="Swap in source image (blended face)",
200 | visible=is_img2img,
201 | elem_id=f"{id_prefix}_face{unit_num}_swap_in_source",
202 | )
203 | swap_in_generated = gr.Checkbox(
204 | True,
205 | placeholder="Swap face in generated image",
206 | label="Swap in generated image",
207 | visible=is_img2img,
208 | elem_id=f"{id_prefix}_face{unit_num}_swap_in_generated",
209 | )
210 |
211 | gr.Markdown(
212 | """
213 | ## Advanced Options
214 |
215 | **Simple :** If you have bad results and don't want to fine-tune here, just enable Codeformer in "Global Post-Processing".
216 | Otherwise, read the [doc](https://glucauze.github.io/sd-webui-faceswaplab/doc/) to understand following options.
217 |
218 | """
219 | )
220 |
221 | with gr.Accordion("Similarity", open=False):
222 | gr.Markdown("""Discard images with low similarity or no faces :""")
223 | with gr.Row():
224 | check_similarity = gr.Checkbox(
225 | False,
226 | placeholder="discard",
227 | label="Check similarity",
228 | elem_id=f"{id_prefix}_face{unit_num}_check_similarity",
229 | )
230 | compute_similarity = gr.Checkbox(
231 | False,
232 | label="Compute similarity",
233 | elem_id=f"{id_prefix}_face{unit_num}_compute_similarity",
234 | )
235 | min_sim = gr.Slider(
236 | 0,
237 | 1,
238 | 0,
239 | step=0.01,
240 | label="Min similarity",
241 | elem_id=f"{id_prefix}_face{unit_num}_min_similarity",
242 | )
243 | min_ref_sim = gr.Slider(
244 | 0,
245 | 1,
246 | 0,
247 | step=0.01,
248 | label="Min reference similarity",
249 | elem_id=f"{id_prefix}_face{unit_num}_min_ref_similarity",
250 | )
251 |
252 | with gr.Accordion(label="Pre-Inpainting (before swapping)", open=False):
253 | gr.Markdown("Pre-inpainting sends face to inpainting before swapping")
254 | pre_inpainting = face_inpainting_ui(
255 | id_prefix=f"{id_prefix}_face{unit_num}_preinpainting",
256 | )
257 |
258 | options = faceswap_unit_advanced_options(is_img2img, unit_num, id_prefix)
259 |
260 | with gr.Accordion(label="Post-Inpainting (After swapping)", open=False):
261 | gr.Markdown("Pre-inpainting sends face to inpainting before swapping")
262 | post_inpainting = face_inpainting_ui(
263 | id_prefix=f"{id_prefix}_face{unit_num}_postinpainting",
264 | )
265 |
266 | gradio_components: List[gr.components.Component] = (
267 | [
268 | img,
269 | face,
270 | batch_files,
271 | blend_faces,
272 | enable,
273 | same_gender,
274 | sort_by_size,
275 | check_similarity,
276 | compute_similarity,
277 | min_sim,
278 | min_ref_sim,
279 | target_faces_index,
280 | reference_faces_index,
281 | swap_in_source,
282 | swap_in_generated,
283 | ]
284 | + pre_inpainting
285 | + options
286 | + post_inpainting
287 | )
288 |
289 | # If changed, you need to change FaceSwapUnitSettings accordingly
290 | # ORDER of parameters is IMPORTANT. It should match the result of FaceSwapUnitSettings
291 | return gradio_components
292 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/faceswaplab_logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import copy
3 | import sys
4 | from typing import Any
5 | from modules import shared
6 | from PIL import Image
7 | from logging import LogRecord
8 |
9 |
10 | class ColoredFormatter(logging.Formatter):
11 | """
12 | A custom logging formatter that outputs logs with level names colored.
13 |
14 | Class Attributes:
15 | COLORS (dict): A dictionary mapping logging level names to their corresponding color codes.
16 |
17 | Inherits From:
18 | logging.Formatter
19 | """
20 |
21 | COLORS: dict[str, str] = {
22 | "DEBUG": "\033[0;36m", # CYAN
23 | "INFO": "\033[0;32m", # GREEN
24 | "WARNING": "\033[0;33m", # YELLOW
25 | "ERROR": "\033[0;31m", # RED
26 | "CRITICAL": "\033[0;37;41m", # WHITE ON RED
27 | "RESET": "\033[0m", # RESET COLOR
28 | }
29 |
30 | def format(self, record: LogRecord) -> str:
31 | """
32 | Format the specified record as text.
33 |
34 | The record's attribute dictionary is used as the operand to a string
35 | formatting operation which yields the returned string. Before formatting
36 | the dictionary, a check is made to see if the format uses the levelname
37 | of the record. If it does, a colorized version is created and used.
38 |
39 | Args:
40 | record (LogRecord): The log record to be formatted.
41 |
42 | Returns:
43 | str: The formatted string which includes the colorized levelname.
44 | """
45 | colored_record = copy.copy(record)
46 | levelname = colored_record.levelname
47 | seq = self.COLORS.get(levelname, self.COLORS["RESET"])
48 | colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
49 | return super().format(colored_record)
50 |
51 |
52 | # Create a new logger
53 | logger = logging.getLogger("FaceSwapLab")
54 | logger.propagate = False
55 |
56 | # Add handler if we don't have one.
57 | if not logger.handlers:
58 | handler = logging.StreamHandler(sys.stdout)
59 | handler.setFormatter(
60 | ColoredFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
61 | )
62 | logger.addHandler(handler)
63 |
64 | # Configure logger
65 | loglevel_string = getattr(shared.cmd_opts, "faceswaplab_loglevel", "INFO")
66 | loglevel = getattr(logging, loglevel_string.upper(), "INFO")
67 | logger.setLevel(loglevel)
68 |
69 | import tempfile
70 |
71 | if logger.getEffectiveLevel() <= logging.DEBUG:
72 | DEBUG_DIR = tempfile.mkdtemp()
73 |
74 |
75 | def save_img_debug(img: Image.Image, message: str, *opts: Any) -> None:
76 | """
77 | Saves an image to a temporary file if the logger's effective level is set to DEBUG or lower.
78 | After saving, it logs a debug message along with the file URI of the image.
79 |
80 | Parameters
81 | ----------
82 | img : Image.Image
83 | The image to be saved.
84 | message : str
85 | The message to be logged.
86 | *opts : Any
87 | Additional arguments to be passed to the logger's debug method.
88 |
89 | Returns
90 | -------
91 | None
92 | """
93 | if logger.getEffectiveLevel() <= logging.DEBUG:
94 | with tempfile.NamedTemporaryFile(
95 | dir=DEBUG_DIR, delete=False, suffix=".png"
96 | ) as temp_file:
97 | img_path = temp_file.name
98 | img.save(img_path)
99 |
100 | message_with_link = f"{message}\nImage: file://{img_path}"
101 | logger.debug(message_with_link, *opts)
102 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/imgutils.py:
--------------------------------------------------------------------------------
1 | import io
2 | from typing import List, Optional, Union, Dict
3 | from PIL import Image
4 | import cv2
5 | import numpy as np
6 | from math import isqrt, ceil
7 | import torch
8 | from modules import processing
9 | import base64
10 | from collections import Counter
11 | from scripts.faceswaplab_utils.sd_utils import get_sd_option
12 | from scripts.faceswaplab_utils.typing import BoxCoords, CV2ImgU8, PILImage
13 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
14 |
15 |
16 | def check_against_nsfw(img: PILImage) -> bool:
17 | """
18 | Check if an image exceeds the Not Safe for Work (NSFW) score.
19 |
20 | Parameters:
21 | img (PILImage): The image to be checked.
22 |
23 | Returns:
24 | bool: True if any part of the image is considered NSFW, False otherwise.
25 | """
26 |
27 | NSFW_SCORE_THRESHOLD = get_sd_option("faceswaplab_nsfw_threshold", 0.7)
28 |
29 | # For testing purpose :
30 | if NSFW_SCORE_THRESHOLD >= 1:
31 | return False
32 |
33 | from ifnude import detect
34 |
35 | shapes: List[bool] = []
36 | chunks: List[Dict[str, Union[int, float]]] = detect(img)
37 |
38 | for chunk in chunks:
39 | logger.debug(
40 | f"chunck score {chunk['score']}, threshold : {NSFW_SCORE_THRESHOLD}"
41 | )
42 | shapes.append(chunk["score"] > NSFW_SCORE_THRESHOLD)
43 |
44 | return any(shapes)
45 |
46 |
47 | def pil_to_cv2(pil_img: PILImage) -> CV2ImgU8:
48 | """
49 | Convert a PIL Image into an OpenCV image (cv2).
50 |
51 | Args:
52 | pil_img (PILImage): An image in PIL format.
53 |
54 | Returns:
55 | CV2ImgU8: The input image converted to OpenCV format (BGR).
56 | """
57 | return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR).astype("uint8")
58 |
59 |
60 | def cv2_to_pil(cv2_img: CV2ImgU8) -> PILImage: # type: ignore
61 | """
62 | Convert an OpenCV image (cv2) into a PIL Image.
63 |
64 | Args:
65 | cv2_img (CV2ImgU8): An image in OpenCV format (BGR).
66 |
67 | Returns:
68 | PILImage: The input image converted to PIL format (RGB).
69 | """
70 | return Image.fromarray(cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB))
71 |
72 |
73 | def torch_to_pil(tensor: torch.Tensor) -> List[PILImage]:
74 | """
75 | Converts a tensor image or a batch of tensor images to a PIL image or a list of PIL images.
76 |
77 | Parameters
78 | ----------
79 | images : torch.Tensor
80 | A tensor representing an image or a batch of images.
81 |
82 | Returns
83 | -------
84 | list
85 | A list of PIL images.
86 | """
87 | images: CV2ImgU8 = tensor.cpu().permute(0, 2, 3, 1).numpy()
88 | if images.ndim == 3:
89 | images = images[None, ...]
90 | images = (images * 255).round().astype("uint8")
91 | pil_images = [Image.fromarray(image) for image in images]
92 | return pil_images
93 |
94 |
95 | def pil_to_torch(pil_images: Union[PILImage, List[PILImage]]) -> torch.Tensor:
96 | """
97 | Converts a PIL image or a list of PIL images to a torch tensor or a batch of torch tensors.
98 |
99 | Parameters
100 | ----------
101 | pil_images : Union[PILImage, List[PILImage]]
102 | A PIL image or a list of PIL images.
103 |
104 | Returns
105 | -------
106 | torch.Tensor
107 | A tensor representing an image or a batch of images.
108 | """
109 | if isinstance(pil_images, list):
110 | numpy_images = [np.array(image) for image in pil_images]
111 | torch_images = torch.from_numpy(np.stack(numpy_images)).permute(0, 3, 1, 2)
112 | return torch_images
113 |
114 | numpy_image = np.array(pil_images)
115 | torch_image = torch.from_numpy(numpy_image).permute(2, 0, 1)
116 | return torch_image
117 |
118 |
119 | def create_square_image(image_list: List[PILImage]) -> Optional[PILImage]:
120 | """
121 | Creates a square image by combining multiple images in a grid pattern.
122 |
123 | Args:
124 | image_list (list): List of PIL Image objects to be combined.
125 |
126 | Returns:
127 | PIL Image object: The resulting square image.
128 | None: If the image_list is empty or contains only one image.
129 | """
130 |
131 | # Count the occurrences of each image size in the image_list
132 | size_counter = Counter(image.size for image in image_list)
133 |
134 | # Get the most common image size (size with the highest count)
135 | common_size = size_counter.most_common(1)[0][0]
136 |
137 | # Filter the image_list to include only images with the common size
138 | image_list = [image for image in image_list if image.size == common_size]
139 |
140 | # Get the dimensions (width and height) of the common size
141 | size = common_size
142 |
143 | # If there are more than one image in the image_list
144 | if len(image_list) > 1:
145 | num_images = len(image_list)
146 |
147 | # Calculate the number of rows and columns for the grid
148 | rows = isqrt(num_images)
149 | cols = ceil(num_images / rows)
150 |
151 | # Calculate the size of the square image
152 | square_size = (cols * size[0], rows * size[1])
153 |
154 | # Create a new RGB image with the square size
155 | square_image = Image.new("RGB", square_size)
156 |
157 | # Paste each image onto the square image at the appropriate position
158 | for i, image in enumerate(image_list):
159 | row = i // cols
160 | col = i % cols
161 |
162 | square_image.paste(image, (col * size[0], row * size[1]))
163 |
164 | # Return the resulting square image
165 | return square_image
166 |
167 | # Return None if there are no images or only one image in the image_list
168 | return None
169 |
170 |
171 | def create_mask(
172 | image: PILImage,
173 | box_coords: BoxCoords,
174 | ) -> PILImage:
175 | """
176 | Create a binary mask for a given image and bounding box coordinates.
177 |
178 | Args:
179 | image (PILImage): The input image.
180 | box_coords (Tuple[int, int, int, int]): A tuple of 4 integers defining the bounding box.
181 | It follows the pattern (x1, y1, x2, y2), where (x1, y1) is the top-left coordinate of the
182 | box and (x2, y2) is the bottom-right coordinate of the box.
183 |
184 | Returns:
185 | PILImage: A binary mask of the same size as the input image, where pixels within
186 | the bounding box are white (255) and pixels outside the bounding box are black (0).
187 | """
188 | width, height = image.size
189 | mask = Image.new("L", (width, height), 0)
190 | x1, y1, x2, y2 = box_coords
191 | for x in range(x1, x2 + 1):
192 | for y in range(y1, y2 + 1):
193 | mask.putpixel((x, y), 255)
194 | return mask
195 |
196 |
197 | def apply_mask(
198 | img: PILImage, p: processing.StableDiffusionProcessing, batch_index: int
199 | ) -> PILImage:
200 | """
201 | Apply mask overlay and color correction to an image if enabled
202 |
203 | Args:
204 | img: PIL Image objects.
205 | p : The processing object
206 | batch_index : the batch index
207 |
208 | Returns:
209 | PIL Image object
210 | """
211 | if isinstance(p, processing.StableDiffusionProcessingImg2Img):
212 | if p.inpaint_full_res:
213 | overlays = p.overlay_images
214 | if overlays is None or batch_index >= len(overlays):
215 | return img
216 | overlay: PILImage = overlays[batch_index]
217 | logger.debug("Overlay size %s, Image size %s", overlay.size, img.size)
218 | if overlay.size != img.size:
219 | overlay = overlay.resize((img.size), resample=Image.Resampling.LANCZOS)
220 | img = img.copy()
221 | img.paste(overlay, (0, 0), overlay)
222 | return img
223 |
224 | img = processing.apply_overlay(img, p.paste_to, batch_index, p.overlay_images)
225 | if p.color_corrections is not None and batch_index < len(p.color_corrections):
226 | img = processing.apply_color_correction(
227 | p.color_corrections[batch_index], img
228 | )
229 | return img
230 |
231 |
232 | def prepare_mask(mask: PILImage, p: processing.StableDiffusionProcessing) -> PILImage:
233 | """
234 | Prepare an image mask for the inpainting process. (This comes from controlnet)
235 |
236 | This function takes as input a PIL Image object and an instance of the
237 | StableDiffusionProcessing class, and performs the following steps to prepare the mask:
238 |
239 | 1. Convert the mask to grayscale (mode "L").
240 | 2. If the 'inpainting_mask_invert' attribute of the processing instance is True,
241 | invert the mask colors.
242 | 3. If the 'mask_blur' attribute of the processing instance is greater than 0,
243 | apply a Gaussian blur to the mask with a radius equal to 'mask_blur'.
244 |
245 | Args:
246 | mask (PILImage): The input mask as a PIL Image object.
247 | p (processing.StableDiffusionProcessing): An instance of the StableDiffusionProcessing class
248 | containing the processing parameters.
249 |
250 | Returns:
251 | mask (PILImage): The prepared mask as a PIL Image object.
252 | """
253 | mask = mask.convert("L")
254 | # FIXME : Properly fix blur
255 | # if getattr(p, "mask_blur", 0) > 0:
256 | # mask = mask.filter(ImageFilter.GaussianBlur(p.mask_blur))
257 | return mask
258 |
259 |
260 | def base64_to_pil(base64str: Optional[str]) -> Optional[PILImage]:
261 | """
262 | Converts a base64 string to a PIL Image object.
263 |
264 | Parameters:
265 | base64str (Optional[str]): The base64 string to convert. This string may contain a data URL scheme
266 | (i.e., 'data:image/jpeg;base64,') or just be the raw base64 encoded data. If None, the function
267 | will return None.
268 |
269 | Returns:
270 | Optional[PILImage]: A PIL Image object created from the base64 string. If the input is None,
271 | the function returns None.
272 |
273 | Raises:
274 | binascii.Error: If the base64 string is not properly formatted or encoded.
275 | PIL.UnidentifiedImageError: If the image format cannot be identified.
276 | """
277 |
278 | if base64str is None:
279 | return None
280 |
281 | # Check if the base64 string has a data URL scheme
282 | if "base64," in base64str:
283 | base64_data = base64str.split("base64,")[-1]
284 | img_bytes = base64.b64decode(base64_data)
285 | else:
286 | # If no data URL scheme, just decode
287 | img_bytes = base64.b64decode(base64str)
288 |
289 | return Image.open(io.BytesIO(img_bytes))
290 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/install_utils.py:
--------------------------------------------------------------------------------
1 | from types import ModuleType
2 |
3 |
4 | def check_install() -> None:
5 | # Very ugly hack :( due to sdnext optimization not calling install.py every time if git log has not changed
6 | import importlib.util
7 | import sys
8 | import os
9 |
10 | current_dir = os.path.dirname(os.path.realpath(__file__))
11 | check_install_path = os.path.join(current_dir, "..", "..", "install.py")
12 | spec = importlib.util.spec_from_file_location("check_install", check_install_path)
13 | if spec != None:
14 | check_install: ModuleType = importlib.util.module_from_spec(spec)
15 | sys.modules["check_install"] = check_install
16 | spec.loader.exec_module(check_install) # type: ignore
17 | check_install.check_install() # type: ignore
18 | #### End of ugly hack :( !
19 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/models_utils.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | from typing import List
4 | import modules.scripts as scripts
5 | from modules import scripts
6 | from scripts.faceswaplab_globals import EXPECTED_INSWAPPER_SHA1, EXTENSION_PATH
7 | from modules.shared import opts
8 | from scripts.faceswaplab_utils.faceswaplab_logging import logger
9 | import traceback
10 | import hashlib
11 |
12 |
13 | def is_sha1_matching(file_path: str, expected_sha1: str) -> bool:
14 | sha1_hash = hashlib.sha1(usedforsecurity=False)
15 | try:
16 | with open(file_path, "rb") as file:
17 | for byte_block in iter(lambda: file.read(4096), b""):
18 | sha1_hash.update(byte_block)
19 | if sha1_hash.hexdigest() == expected_sha1:
20 | return True
21 | else:
22 | return False
23 | except Exception as e:
24 | logger.error(
25 | "Failed to check model hash, check the model is valid or has been downloaded adequately : %e",
26 | e,
27 | )
28 | traceback.print_exc()
29 | return False
30 |
31 |
32 | def check_model() -> bool:
33 | model_path = get_current_swap_model()
34 | if not is_sha1_matching(
35 | file_path=model_path, expected_sha1=EXPECTED_INSWAPPER_SHA1
36 | ):
37 | logger.error(
38 | "Suspicious sha1 for model %s, check the model is valid or has been downloaded adequately. Should be %s",
39 | model_path,
40 | EXPECTED_INSWAPPER_SHA1,
41 | )
42 | return False
43 | return True
44 |
45 |
46 | def get_swap_models() -> List[str]:
47 | """
48 | Retrieve a list of swap model files.
49 |
50 | This function searches for model files in the specified directories and returns a list of file paths.
51 | The supported file extensions are ".onnx".
52 |
53 | Returns:
54 | A list of file paths of the model files.
55 | """
56 | models_path = os.path.join(scripts.basedir(), EXTENSION_PATH, "models", "*")
57 | models = glob.glob(models_path)
58 |
59 | # Add an additional models directory and find files in it
60 | models_path = os.path.join(scripts.basedir(), "models", "faceswaplab", "*")
61 | models += glob.glob(models_path)
62 |
63 | # Filter the list to include only files with the supported extensions
64 | models = [x for x in models if x.endswith(".onnx")]
65 |
66 | return models
67 |
68 |
69 | def get_current_swap_model() -> str:
70 | model = opts.data.get("faceswaplab_model", None) # type: ignore
71 | if model is None:
72 | models = get_swap_models()
73 | model = models[0] if len(models) else None
74 | logger.info("Try to use model : %s", model)
75 | try:
76 | if not model or not os.path.isfile(model): # type: ignore
77 | logger.error("The model %s cannot be found or loaded", model)
78 | raise FileNotFoundError(
79 | "No faceswap model found. Please add it to the faceswaplab directory. Ensure the model is in the proper directory (/models/faceswaplab/inswapper_128.onnx)"
80 | )
81 | except:
82 | raise FileNotFoundError(
83 | "No faceswap model found. Please add it to the faceswaplab directory. Ensure the model is in the proper directory (/models/faceswaplab/inswapper_128.onnx)"
84 | )
85 |
86 | assert model is not None
87 | return model
88 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/sd_utils.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | from modules.shared import opts
3 |
4 |
5 | def get_sd_option(name: str, default: Any) -> Any:
6 | assert opts.data is not None
7 | return opts.data.get(name, default)
8 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/typing.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 | from numpy import uint8
3 | from insightface.app.common import Face as IFace
4 | from PIL import Image
5 | import numpy as np
6 | from enum import Enum
7 |
8 | PILImage = Image.Image
9 | CV2ImgU8 = np.ndarray[int, np.dtype[uint8]]
10 | Face = IFace
11 | BoxCoords = Tuple[int, int, int, int]
12 |
13 |
14 | class Gender(Enum):
15 | AUTO = -1
16 | FEMALE = 0
17 | MALE = 1
18 |
--------------------------------------------------------------------------------
/scripts/faceswaplab_utils/ui_utils.py:
--------------------------------------------------------------------------------
1 | from dataclasses import fields, is_dataclass
2 | from typing import *
3 |
4 |
5 | def dataclass_from_flat_list(cls: type, values: Tuple[Any, ...]) -> Any:
6 | if not is_dataclass(cls):
7 | raise TypeError(f"{cls} is not a dataclass")
8 |
9 | idx = 0
10 | init_values = {}
11 | for field in fields(cls):
12 | if is_dataclass(field.type):
13 | inner_values = [values[idx + i] for i in range(len(fields(field.type)))]
14 | init_values[field.name] = field.type(*inner_values)
15 | idx += len(inner_values)
16 | else:
17 | if idx >= len(values):
18 | raise IndexError(
19 | f"Expected more values for dataclass {cls}. Current index: {idx}, values length: {len(values)}"
20 | )
21 | value = values[idx]
22 | init_values[field.name] = value
23 | idx += 1
24 | return cls(**init_values)
25 |
26 |
27 | def dataclasses_from_flat_list(
28 | classes_mapping: List[type], values: Tuple[Any, ...]
29 | ) -> List[Any]:
30 | instances = []
31 | idx = 0
32 | for cls in classes_mapping:
33 | num_fields = sum(
34 | len(fields(field.type)) if is_dataclass(field.type) else 1
35 | for field in fields(cls)
36 | )
37 | instance = dataclass_from_flat_list(cls, values[idx : idx + num_fields])
38 | instances.append(instance)
39 | idx += num_fields
40 | assert [
41 | isinstance(i, t) for i, t in zip(instances, classes_mapping)
42 | ], "Instances should match types"
43 | return instances
44 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ./check.sh
3 | pytest -p no:warnings
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import pytest
3 | import requests
4 | import sys
5 | import tempfile
6 | import safetensors
7 |
8 | sys.path.append(".")
9 |
10 | import requests
11 | from client_api.api_utils import (
12 | FaceSwapUnit,
13 | InswappperOptions,
14 | pil_to_base64,
15 | PostProcessingOptions,
16 | InpaintingWhen,
17 | InpaintingOptions,
18 | FaceSwapRequest,
19 | FaceSwapResponse,
20 | FaceSwapExtractRequest,
21 | FaceSwapCompareRequest,
22 | FaceSwapExtractResponse,
23 | compare_faces,
24 | base64_to_pil,
25 | base64_to_safetensors,
26 | safetensors_to_base64,
27 | )
28 | from PIL import Image
29 |
30 | base_url = "http://127.0.0.1:7860"
31 |
32 |
33 | @pytest.fixture
34 | def face_swap_request() -> FaceSwapRequest:
35 | # First face unit
36 | unit1 = FaceSwapUnit(
37 | source_img=pil_to_base64("references/man.png"), # The face you want to use
38 | faces_index=(0,), # Replace first face
39 | )
40 |
41 | # Second face unit
42 | unit2 = FaceSwapUnit(
43 | source_img=pil_to_base64("references/woman.png"), # The face you want to use
44 | same_gender=True,
45 | faces_index=(0,), # Replace first woman since same gender is on
46 | swapping_options=InswappperOptions(
47 | face_restorer_name="CodeFormer",
48 | upscaler_name="LDSR",
49 | improved_mask=True,
50 | sharpen=True,
51 | color_corrections=True,
52 | ),
53 | )
54 |
55 | # Post-processing config
56 | pp = PostProcessingOptions(
57 | face_restorer_name="CodeFormer",
58 | codeformer_weight=0.5,
59 | restorer_visibility=1,
60 | upscaler_name="Lanczos",
61 | scale=4,
62 | inpainting_when=InpaintingWhen.BEFORE_RESTORE_FACE,
63 | inpainting_options=InpaintingOptions(
64 | inpainting_steps=30,
65 | inpainting_denoising_strengh=0.1,
66 | ),
67 | )
68 | # Prepare the request
69 | request = FaceSwapRequest(
70 | image=pil_to_base64("tests/test_image.png"),
71 | units=[unit1, unit2],
72 | postprocessing=pp,
73 | )
74 |
75 | return request
76 |
77 |
78 | def test_version() -> None:
79 | response = requests.get(f"{base_url}/faceswaplab/version")
80 | assert response.status_code == 200
81 | assert "version" in response.json()
82 |
83 |
84 | def test_compare() -> None:
85 | request = FaceSwapCompareRequest(
86 | image1=pil_to_base64("references/man.png"),
87 | image2=pil_to_base64("references/man.png"),
88 | )
89 |
90 | response = requests.post(
91 | url=f"{base_url}/faceswaplab/compare",
92 | data=request.json(),
93 | headers={"Content-Type": "application/json; charset=utf-8"},
94 | )
95 | assert response.status_code == 200
96 | similarity = float(response.text)
97 | assert similarity > 0.90
98 |
99 |
100 | def test_extract() -> None:
101 | pp = PostProcessingOptions(
102 | face_restorer_name="CodeFormer",
103 | codeformer_weight=0.5,
104 | restorer_visibility=1,
105 | upscaler_name="Lanczos",
106 | )
107 |
108 | request = FaceSwapExtractRequest(
109 | images=[pil_to_base64("tests/test_image.png")], postprocessing=pp
110 | )
111 |
112 | response = requests.post(
113 | url=f"{base_url}/faceswaplab/extract",
114 | data=request.json(),
115 | headers={"Content-Type": "application/json; charset=utf-8"},
116 | )
117 | assert response.status_code == 200
118 |
119 | res = FaceSwapExtractResponse.parse_obj(response.json())
120 |
121 | assert len(res.pil_images) == 2
122 |
123 | # First face is the man
124 | assert (
125 | compare_faces(
126 | res.pil_images[0], Image.open("tests/test_image.png"), base_url=base_url
127 | )
128 | > 0.5
129 | )
130 |
131 |
132 | def test_faceswap(face_swap_request: FaceSwapRequest) -> None:
133 | response = requests.post(
134 | f"{base_url}/faceswaplab/swap_face",
135 | data=face_swap_request.json(),
136 | headers={"Content-Type": "application/json; charset=utf-8"},
137 | )
138 |
139 | assert response.status_code == 200
140 | data = response.json()
141 | assert "images" in data
142 | assert "infos" in data
143 |
144 | res = FaceSwapResponse.parse_obj(response.json())
145 | images: List[Image.Image] = res.pil_images
146 | assert len(images) == 1
147 | image = images[0]
148 | orig_image = base64_to_pil(face_swap_request.image)
149 | assert image.width == orig_image.width * face_swap_request.postprocessing.scale
150 | assert image.height == orig_image.height * face_swap_request.postprocessing.scale
151 |
152 | # Compare the result and ensure similarity for the man (first face)
153 |
154 | request = FaceSwapCompareRequest(
155 | image1=pil_to_base64("references/man.png"),
156 | image2=res.images[0],
157 | )
158 |
159 | response = requests.post(
160 | url=f"{base_url}/faceswaplab/compare",
161 | data=request.json(),
162 | headers={"Content-Type": "application/json; charset=utf-8"},
163 | )
164 | assert response.status_code == 200
165 | similarity = float(response.text)
166 | assert similarity > 0.50
167 |
168 |
169 | def test_faceswap_inpainting(face_swap_request: FaceSwapRequest) -> None:
170 | face_swap_request.units[0].pre_inpainting = InpaintingOptions(
171 | inpainting_denoising_strengh=0.4,
172 | inpainting_prompt="Photo of a funny man",
173 | inpainting_negative_prompt="blurry, bad art",
174 | inpainting_steps=100,
175 | )
176 |
177 | face_swap_request.units[0].post_inpainting = InpaintingOptions(
178 | inpainting_denoising_strengh=0.4,
179 | inpainting_prompt="Photo of a funny man",
180 | inpainting_negative_prompt="blurry, bad art",
181 | inpainting_steps=20,
182 | inpainting_sampler="Euler a",
183 | )
184 |
185 | response = requests.post(
186 | f"{base_url}/faceswaplab/swap_face",
187 | data=face_swap_request.json(),
188 | headers={"Content-Type": "application/json; charset=utf-8"},
189 | )
190 |
191 | assert response.status_code == 200
192 | data = response.json()
193 | assert "images" in data
194 | assert "infos" in data
195 |
196 |
197 | def test_faceswap_checkpoint_building() -> None:
198 | source_images: List[str] = [
199 | pil_to_base64("references/man.png"),
200 | pil_to_base64("references/woman.png"),
201 | ]
202 |
203 | response = requests.post(
204 | url=f"{base_url}/faceswaplab/build",
205 | json=source_images,
206 | headers={"Content-Type": "application/json; charset=utf-8"},
207 | )
208 |
209 | assert response.status_code == 200
210 |
211 | with tempfile.NamedTemporaryFile(delete=True) as temp_file:
212 | base64_to_safetensors(response.json(), output_path=temp_file.name)
213 | with safetensors.safe_open(temp_file.name, framework="pt") as f:
214 | assert "age" in f.keys()
215 | assert "gender" in f.keys()
216 | assert "embedding" in f.keys()
217 |
218 |
219 | def test_faceswap_checkpoint_building_and_using() -> None:
220 | source_images: List[str] = [
221 | pil_to_base64("references/man.png"),
222 | ]
223 |
224 | response = requests.post(
225 | url=f"{base_url}/faceswaplab/build",
226 | json=source_images,
227 | headers={"Content-Type": "application/json; charset=utf-8"},
228 | )
229 |
230 | assert response.status_code == 200
231 |
232 | with tempfile.NamedTemporaryFile(delete=True) as temp_file:
233 | base64_to_safetensors(response.json(), output_path=temp_file.name)
234 | with safetensors.safe_open(temp_file.name, framework="pt") as f:
235 | assert "age" in f.keys()
236 | assert "gender" in f.keys()
237 | assert "embedding" in f.keys()
238 |
239 | # First face unit :
240 | unit1 = FaceSwapUnit(
241 | source_face=safetensors_to_base64(
242 | temp_file.name
243 | ), # convert the checkpoint to base64
244 | faces_index=(0,), # Replace first face
245 | swapping_options=InswappperOptions(
246 | face_restorer_name="CodeFormer",
247 | upscaler_name="LDSR",
248 | improved_mask=True,
249 | sharpen=True,
250 | color_corrections=True,
251 | ),
252 | )
253 |
254 | # Prepare the request
255 | request = FaceSwapRequest(
256 | image=pil_to_base64("tests/test_image.png"), units=[unit1]
257 | )
258 |
259 | # Face Swap
260 | response = requests.post(
261 | url=f"{base_url}/faceswaplab/swap_face",
262 | data=request.json(),
263 | headers={"Content-Type": "application/json; charset=utf-8"},
264 | )
265 | assert response.status_code == 200
266 | fsr = FaceSwapResponse.parse_obj(response.json())
267 | data = response.json()
268 | assert "images" in data
269 | assert "infos" in data
270 |
271 | # First face is the man
272 | assert (
273 | compare_faces(
274 | fsr.pil_images[0], Image.open("references/man.png"), base_url=base_url
275 | )
276 | > 0.5
277 | )
278 |
--------------------------------------------------------------------------------
/tests/test_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glucauze/sd-webui-faceswaplab/42d1c75b68bb7fdb55e2b3753eda65189f82b56d/tests/test_image.png
--------------------------------------------------------------------------------