├── .gitignore ├── LICENSE ├── README.md ├── assets └── lbpcascade_animeface.xml ├── face_editor ├── entities │ ├── definitions.py │ ├── face.py │ ├── option.py │ └── rect.py ├── inferencers │ ├── README.md │ ├── anime_segmentation │ │ ├── README.md │ │ ├── installer.py │ │ └── mask_generator.py │ ├── bisenet_mask_generator.py │ ├── blur_processor.py │ ├── debug_processor.py │ ├── ellipse_mask_generator.py │ ├── face_area_mask_generator.py │ ├── img2img_face_processor.py │ ├── insightface │ │ ├── README.md │ │ ├── detector.py │ │ ├── installer.py │ │ └── mask_generator.py │ ├── lbpcascade_animeface_detector.py │ ├── mediapipe │ │ ├── README.md │ │ ├── face_detector.py │ │ ├── installer.py │ │ └── mask_generator.py │ ├── no_mask_generator.py │ ├── no_op_processor.py │ ├── openmmlab │ │ ├── README.md │ │ ├── installer.py │ │ └── mmseg_mask_generator.py │ ├── retinaface_detector.py │ ├── rotate_face_processor.py │ ├── vignette_mask_generator.py │ └── yolo │ │ ├── README.md │ │ ├── detector.py │ │ ├── inferencer.py │ │ ├── installer.py │ │ └── mask_generator.py ├── io │ └── util.py ├── ui │ ├── param_value_parser.py │ ├── ui_builder.py │ └── workflow_editor.py └── use_cases │ ├── face_detector.py │ ├── face_processor.py │ ├── image_processing_util.py │ ├── image_processor.py │ ├── installer.py │ ├── mask_generator.py │ ├── query_matcher.py │ ├── registry.py │ └── workflow_manager.py ├── images ├── deno-4.jpg ├── deno-6.jpg ├── deno-8.jpg ├── face-margin.jpg ├── image-01.jpg ├── inferencers │ ├── anime_segmentation │ │ └── mask.jpg │ ├── bisenet-face-hair-hat-neck.jpg │ ├── bisenet-face-hair-hat.jpg │ ├── bisenet-face-hair.jpg │ ├── bisenet-face.jpg │ ├── blur.jpg │ ├── ellipse.jpg │ ├── face-area-rect.jpg │ ├── insightface │ │ └── mask.jpg │ ├── mediapipe │ │ └── mask.jpg │ ├── no-mask.jpg │ ├── openmmlab │ │ └── mask.jpg │ ├── vignette-default.jpg │ └── vignette-use-minimal-area.jpg ├── mask-00.jpg ├── mask-10.jpg ├── mask-20.jpg ├── screen-shot.jpg ├── setup-01.png ├── step-1.jpg ├── step-2.jpg ├── step-3.jpg ├── step-4.jpg ├── step-5.jpg ├── step-6.jpg ├── tips-01.png ├── tips-02.jpg ├── tips-02.png ├── tips-03.jpg ├── tips-03.png ├── tips-04.png ├── tips-05.jpg ├── tips-06.png ├── tips-07.jpg ├── tips-08.png ├── usage-01.png ├── usage-02.png ├── v1.0.0.jpg ├── workflow-01.jpg ├── workflow-editor-01.png ├── workflow-editor-02.png ├── workflow-editor-03.png ├── workflow-editor-04.png └── workflow-editor-05.png ├── install.py ├── scripts ├── face_editor.py └── face_editor_extension.py └── workflows ├── default.json └── examples ├── README.md ├── adetailer.json ├── blur.json ├── blur_non_center_faces.json ├── blur_young_people.json ├── insightface.json ├── lbpcascade_animeface.json ├── mediapipe.json ├── openmmlab.json └── yolov8n.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | workflows/test* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 KAWAKAMI Shinichi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face Editor 2 | Face Editor for Stable Diffusion. This Extension is useful for the following purposes: 3 | 4 | - Fixing broken faces 5 | - Changing facial expressions 6 | - Apply blurring or other processing 7 | 8 | ![example](./images/v1.0.0.jpg) 9 | 10 | This is a [extension](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Extensions) of [AUTOMATIC1111's Stable Diffusion Web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui). If you are using [SD.Next](https://github.com/vladmandic/automatic), use the [sd.next](https://github.com/ototadana/sd-face-editor/tree/sd.next) branch. 11 | 12 | ## Setup 13 | 1. Open the "Extensions" tab then the "Install from URL" tab. 14 | 2. Enter "https://github.com/ototadana/sd-face-editor.git" in the "URL of the extension's git repository" field. 15 | ![Install from URL](./images/setup-01.png) 16 | 3. Click the "Install" button and wait for the "Installed into /home/ototadana/stable-diffusion-webui/extensions/sd-face-editor. Use Installed tab to restart." message to appear. 17 | 4. Go to "Installed" tab and click "Apply and restart UI". 18 | 19 | 20 | ## Usage 21 | 1. Click "Face Editor" and check **"Enabled"**. 22 | ![Check Enabled](./images/usage-01.png) 23 | 2. Then enter the prompts as usual and click the "Generate" button to modify the faces in the generated images. 24 | ![Result](./images/usage-02.png) 25 | 3. If you are not satisfied with the results, adjust the [parameters](#parameters) and rerun. see [Tips](#tips). 26 | 27 | 28 | ## Tips 29 | ### Contour discomfort 30 | If you feel uncomfortable with the facial contours, try increasing the **"Mask size"** value. This discomfort often occurs when the face is not facing straight ahead. 31 | 32 | ![Mask size](./images/tips-02.jpg) 33 | 34 | If the forelock interferes with rendering the face properly, generally, selecting "Hair" from **"Affected areas"** results in a more natural image. 35 | 36 | ![Affected ares - UI](./images/tips-08.png) 37 | 38 | This setting modifies the mask area as illustrated below: 39 | 40 | ![Affected ares - Mask images](./images/tips-07.jpg) 41 | 42 | 43 | --- 44 | ### When multiple faces are close together 45 | When multiple faces are close together, one face may collapse under the influence of the other. 46 | In such cases, enable **"Use minimal area (for close faces)"**. 47 | 48 | ![Use minimal area for close faces](./images/tips-04.png) 49 | 50 | --- 51 | ### Change facial expression 52 | Use **"Prompt for face"** option if you want to change the facial expression. 53 | 54 | ![Prompt for face](./images/tips-03.jpg) 55 | 56 | #### Individual instructions for multiple faces 57 | ![Individual instructions for multiple faces](./images/tips-05.jpg) 58 | 59 | Faces can be individually directed with prompts separated by `||` (two vertical lines). 60 | 61 | ![Individual instructions for multiple faces - screen shot](./images/tips-06.png) 62 | 63 | - Each prompt is applied to the faces on the image in order from left to right. 64 | - The number of prompts does not have to match the number of faces to work. 65 | - If you write the string `@@`, the normal prompts (written at the top of the screen) will be expanded at that position. 66 | - If you are using the [Wildcards Extension](https://github.com/AUTOMATIC1111/stable-diffusion-webui-wildcards), you can use the `__name__` syntax and the text file in the directory of the wildcards extension as well as the normal prompts. 67 | 68 | --- 69 | ### Fixing images that already exist 70 | If you wish to modify the face of an already existing image instead of creating a new one, follow these steps: 71 | 72 | 1. Open the image to be edited in the img2img tab 73 | It is recommended that you use the same settings (prompt, sampling steps and method, seed, etc.) as for the original image. 74 | So, it is a good idea to start with the **PNG Info** tab. 75 | 1. Click **PNG Info** tab. 76 | 2. Upload the image to be edited. 77 | 3. Click **Send to img2img** button. 78 | 2. Set the value of **"Denoising strength"** of img2img to `0`. This setting is good for preventing changes to areas other than the faces and for reducing processing time. 79 | 3. Click "Face Editor" and check "Enabled". 80 | 4. Then, set the desired parameters and click the Generate button. 81 | 82 | --- 83 | ## How it works 84 | This script performs the following steps: 85 | 86 | ### Step 0 87 | First, image(s) are generated as usual according to prompts and other settings. This script acts as a post-processor for those images. 88 | 89 | ### Step 1. Face Detection 90 |
91 | Detects faces on the image. 92 | 93 | ![step-1](./images/step-1.jpg) 94 |
95 | 96 | 97 | 98 | ### Step 2. Crop and Resize the Faces 99 |
100 | Crop the detected face image and resize it to 512x512. 101 | 102 | ![step-2](./images/step-2.jpg) 103 |
104 | 105 | ### Step 3. Recreate the Faces 106 |
107 | Run img2img with the image to create a new face image. 108 | 109 | ![step-3](./images/step-3.jpg) 110 |
111 | 112 | ### Step 4. Paste the Faces 113 |
114 | Resize the new face image and paste it at the original image location. 115 | 116 | ![step-4](./images/step-4.jpg) 117 |
118 | 119 | ### Step 5. Blend the entire image 120 |
121 | To remove the borders generated when pasting the image, mask all but the face and run inpaint. 122 | 123 | ![step-5](./images/step-5.jpg) 124 |
125 | 126 | ### Completed 127 |
128 | Show sample image 129 | 130 | ![step-6](./images/step-6.jpg) 131 |
132 | 133 | ## Parameters 134 | ### Basic Options 135 | ##### Workflow 136 | Select a workflow. "Search workflows in subdirectories" can be enabled in the Face Editor section of the "Settings" tab to try some experimental workflows. You can also add your own workflows. 137 | 138 | For more detailed information, please refer to the [Workflow Editor](#workflow-editor) section. 139 | 140 | ##### Use minimal area (for close faces) 141 | When pasting the generated image to its original location, the rectangle of the detected face area is used. If this option is not enabled, the generated image itself is pasted. In other words, enabling this option applies a smaller face image, while disabling it applies a larger face image. 142 | 143 | ##### Save original image 144 | This option allows you to save the original, unmodified image. 145 | 146 | ##### Show original image 147 | This option allows you to display the original, unmodified image. 148 | 149 | ##### Show intermediate steps 150 | This option enables the display of images that depict detected faces and masks. 151 | If the generated image is unnatural, enabling it may reveal the cause. 152 | 153 | ##### Prompt for face 154 | Prompt for generating a new face. 155 | If this parameter is not specified, the prompt entered at the top of the screen is used. 156 | 157 | For more information, please see: [here](#change-facial-expression). 158 | 159 | 160 | ##### Mask size (0-64) 161 | Size of the mask area when inpainting to blend the new face with the whole image. 162 | 163 |
164 | Show sample images 165 | 166 | **size: 0** 167 | ![mask size 0](./images/mask-00.jpg) 168 | 169 | **size: 10** 170 | ![mask size 10](./images/mask-10.jpg) 171 | 172 | **size: 20** 173 | ![mask size 20](./images/mask-20.jpg) 174 |
175 | 176 | 177 | ##### Mask blur (0-64) 178 | Size of the blur area when inpainting to blend the new face with the whole image. 179 | 180 | 181 | --- 182 | ### Advanced Options 183 | #### Step 1. Face Detection 184 | ##### Maximum number of faces to detect (1-20) 185 | Use this parameter when you want to reduce the number of faces to be detected. 186 | If more faces are found than the number set here, the smaller faces will be ignored. 187 | 188 | ##### Face detection confidence (0.7-1.0) 189 | Confidence threshold for face detection. Set a lower value if you want to detect more faces. 190 | 191 | #### Step 2. Crop and Resize the Faces 192 | ##### Face margin (1.0-2.0) 193 | Specify the size of the margin for face cropping by magnification. 194 | 195 | If other parameters are exactly the same but this value is different, the atmosphere of the new face created will be different. 196 | 197 |
198 | Show sample images 199 | 200 | ![face margin](./images/face-margin.jpg) 201 |
202 | 203 | 204 | ##### Size of the face when recreating 205 | Specifies one side of the image size when creating a face image. If you are using the SDXL model, we recommend changing to **1024**. For other models, there is usually no need to change from the default value (512), but you may see interesting changes if you do. 206 | 207 | ##### Ignore faces larger than specified size 208 | Ignore if the size of the detected face is larger than the size specified in "Size of the face when recreating". 209 | 210 | For more information, please see: [here](https://github.com/ototadana/sd-face-editor/issues/65). 211 | 212 | ##### Upscaler 213 | Select the upscaler to be used to scale the face image. 214 | 215 | #### Step 3. Recreate the Faces 216 | ##### Denoising strength for face images (0.1-0.8) 217 | Denoising strength for generating a new face. 218 | If the value is too small, facial collapse cannot be corrected, but if it is too large, it is difficult to blend with the entire image. 219 | 220 |
221 | Show sample images 222 | 223 | **strength: 0.4** 224 | ![strength 0.4](./images/deno-4.jpg) 225 | 226 | **strength: 0.6** 227 | ![strength 0.6](./images/deno-6.jpg) 228 | 229 | **strength: 0.8** 230 | ![strength 0.8](./images/deno-8.jpg) 231 |
232 | 233 | 234 | ##### Tilt adjustment threshold (0-180) 235 | This option defines the angle, in degrees, above which tilt correction will be automatically applied to detected faces. For instance, if set to 20, any face detected with a tilt greater than 20 degrees will be adjusted. However, if the "Adjust tilt for detected faces" option in the Face Editor section of the "Settings" tab is enabled, tilt correction will always be applied, regardless of the tilt adjustment threshold value. 236 | 237 | #### Step 4. Paste the Faces 238 | ##### Apply inside mask only 239 | Paste an image cut out in the shape of a face instead of a square image. 240 | 241 | For more information, please see: [here](https://github.com/ototadana/sd-face-editor/issues/33). 242 | 243 | #### Step 5. Blend the entire image 244 | ##### Denoising strength for the entire image (0.0-1.0) 245 | Denoising strength when inpainting to blend the new face with the whole image. 246 | If the border lines are too prominent, increase this value. 247 | 248 | --- 249 | ## API 250 | If you want to use this script as an extension (alwayson_scripts) in the [API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API), specify **"face editor ex"** as the script name as follows: 251 | 252 | ``` 253 | "alwayson_scripts": { 254 | "face editor ex": { 255 | "args": [{"prompt_for_face": "smile"}] 256 | }, 257 | ``` 258 | 259 | By specifying an **object** as the first argument of args as above, parameters can be specified by keywords. We recommend this approach as it can minimize the impact of modifications to the software. If you use a script instead of an extension, you can also specify parameters in the same way as follows: 260 | 261 | ``` 262 | "script_name": "face editor", 263 | "script_args": [{"prompt_for_face": "smile"}], 264 | ``` 265 | 266 | - See [source code](https://github.com/ototadana/sd-face-editor/blob/main/face_editor/entities/option.py) for available keywords. 267 | 268 | --- 269 | ## Workflow Editor 270 | Workflow Editor is where you can customize and experiment with various options beyond just the standard settings. 271 | 272 | ![Workflow Editor](images/workflow-editor-01.png) 273 | 274 | - The editor allows you to select from a variety of implementations, each offering unique behaviors compared to the default settings. 275 | - It provides a platform for freely combining these implementations, enabling you to optimize the workflow according to your needs. 276 | - Within this workflow, you will define a combination of three components: the "Face Detector" for identifying faces within an image, the "Face Processor" for adjusting the detected faces, and the "Mask Generator" for integrating the processed faces back into the original image. 277 | - As you experiment with different settings, ensure to activate the "Show intermediate steps" option. This allows you to understand precisely the impact of each modification. 278 | 279 | ### Using the Workflow Editor UI 280 | #### Workflow list and Refresh button 281 | ![Workflow list and Refresh button](images/workflow-editor-02.png) 282 | 283 | - Lists workflow definition files (.json) stored in the [workflows](workflows) folder. 284 | - The option "Search workflows in subdirectories" can be enabled in the Face Editor section of the "Settings" tab to use sample workflow definition files. 285 | - The Refresh button (🔄) can be clicked to update the contents of the list. 286 | 287 | #### File name and Save button 288 | ![Workflow list and Refresh button](images/workflow-editor-03.png) 289 | 290 | - This feature is used to save the edited workflow definition. 291 | - A file name can be entered in the text box and the Save button (💾) can be clicked to save the file. 292 | 293 | #### Workflow definition editor and Validation button 294 | ![Workflow definition editor and Validation button](images/workflow-editor-04.png) 295 | 296 | - This feature allows you to edit workflow definitions. Workflows are described in JSON format. 297 | - The Validation button (✅) can be clicked to check the description. If there is an error, it will be displayed in the message area to the left of the button. 298 | 299 | ## Example Workflows 300 | 301 | This project includes several example workflows to help you get started. Each example provides a JSON definition for a specific use case, which can be used as is or customized to suit your needs. To access these example workflows from the Workflow Editor, you need to enable the "Search workflows in subdirectories" option located in the Face Editor section of the "Settings" tab. 302 | 303 | ![Settings](images/workflow-editor-05.png) 304 | 305 | For more details about these example workflows and how to use them, please visit the [workflows/examples/README.md](workflows/examples/README.md). 306 | 307 | 308 | ## Workflow Components (Inferencers) 309 | 310 | In this project, the components used in the workflow are also referred to as "inferencers". These inferencers are part of the process that modifies the faces in the generated images: 311 | 312 | 1. **Face Detectors:** These components are used to identify and locate faces within an image. They provide the coordinates of the detected faces, which will be used in the following steps. 313 | 2. **Face Processors:** Once the faces are detected and cropped, these components modify or enhance the faces. 314 | 3. **Mask Generators:** After the faces have been processed, these components are used to create a mask. The mask defines the area of the image where the modifications made by the Face Processors will be applied. 315 | 316 | The "General components" provide the basic functionalities for these categories, and they can be used without the need for additional software installations. On the other hand, each functionality can also be achieved by different technologies or methods, which are categorized here as "Additional components". These "Additional components" provide more advanced or specialized ways to perform the tasks of face detection, face processing, and mask generation. 317 | 318 | In this project, the components used in the workflow are also referred to as "inferencers". These inferencers fall into three functional categories: Face Detectors, Face Processors, and Mask Generators. 319 | 320 | > **Note:** When using "Additional components", ensure that the features you want to use are enabled in the "Additional Components" section of the "Settings" tab under "Face Editor". For detailed descriptions and usage of each component, please refer to the corresponding README. 321 | 322 | ### General components 323 | - [General components](face_editor/inferencers/README.md) 324 | 325 | ### Additional components 326 | - [Anime Segmentation components](face_editor/inferencers/anime_segmentation/README.md) 327 | - [InsightFace components](face_editor/inferencers/insightface/README.md) 328 | - [Mediapipe components](face_editor/inferencers/mediapipe/README.md) 329 | - [OpenMMLab components](face_editor/inferencers/openmmlab/README.md) 330 | - [YOLO components](face_editor/inferencers/yolo/README.md) 331 | 332 | 333 | ### Workflow JSON Reference 334 | 335 | - `face_detector` (string or object, required): The face detector component to be used in the workflow. 336 | - When specified as a string, it is considered as the `name` of the face detector implementation. 337 | - When specified as an object: 338 | - `name` (string, required): The name of the face detector implementation. 339 | - `params` (object, optional): Parameters for the component, represented as key-value pairs. 340 | - `rules` (array or object, required): One or more rules to be applied. 341 | - Each rule can be an object that consists of `when` and `then`: 342 | - `when` (object, optional): The condition for the rule. 343 | - `tag` (string, optional): A tag corresponding to the type of face detected by the face detector. This tag can optionally include a query following the tag name, separated by a '?'. This query is a complex condition that defines attribute-value comparisons using operators. The query can combine multiple comparisons using logical operators. For example, a tag could be "face?age<30&gender=M", which means that the tag name is "face" and the query is "age<30&gender=M". The query indicates that the rule should apply to faces that are identified as male and are less than 30 years old. 344 | - The available operators are as follows: 345 | - `=`: Checks if the attribute is equal to the value. 346 | - `<`: Checks if the attribute is less than the value. 347 | - `>`: Checks if the attribute is greater than the value. 348 | - `<=`: Checks if the attribute is less than or equal to the value. 349 | - `>=`: Checks if the attribute is greater than or equal to the value. 350 | - `!=`: Checks if the attribute is not equal to the value. 351 | - `~=`: Checks if the attribute value contains the value. 352 | - `*=`: Checks if the attribute value starts with the value. 353 | - `=*`: Checks if the attribute value ends with the value. 354 | - `~*`: Checks if the attribute value does not contain the value. 355 | - The logical operators are as follows: 356 | - `&`: Represents logical AND. 357 | - `|`: Represents logical OR. 358 | - `criteria` (string, optional): This determines which faces will be processed, based on their position or size. Available options for position include 'left', 'right', 'center', 'top', 'middle', 'bottom'. For size, 'small', 'large' are available. The selection of faces to be processed that match the specified criteria can be defined in this string, following the pattern `{position/size}:{index range}`. The `{index range}` can be a single index, a range of indices, or a combination of these separated by a comma. For example, specifying left:0 will process the face that is located the most to the left on the screen. left:0-2 will process the three faces that are the most left, and left:0,2,5 will process the most left face, the third from the left, and the sixth from the left. If left is specified without an index or range, it will default to processing the face most left in the frame. Essentially, this is the same as specifying left:0. 359 | - `then` (object or array of objects, required): The job or list of jobs to be executed if the `when` condition is met. 360 | - Each job is an object with the following properties: 361 | - `face_processor` (object or string, required): The face processor component to be used in the job. 362 | - When specified as a string, it is considered as the `name` of the face processor implementation. 363 | - When specified as an object: 364 | - `name` (string, required): The name of the face processor implementation. 365 | - `params` (object, optional): Parameters for the component, represented as key-value pairs. 366 | - `mask_generator` (object or string, required): The mask generator component to be used in the job. 367 | - When specified as a string, it is considered as the `name` of the mask generator implementation. 368 | - When specified as an object: 369 | - `name` (string, required): The name of the mask generator implementation. 370 | - `params` (object, optional): Parameters for the component, represented as key-value pairs. 371 | 372 | Rules are processed in the order they are specified. Once a face is processed by a rule, it will not be processed by subsequent rules. The last rule can be specified with `then` only (i.e., without `when`), which will process all faces that have not been processed by previous rules. 373 | 374 | --- 375 | ## Settings 376 | 377 | In the "Face Editor" section of the "Settings" tab, the following settings can be configured. 378 | 379 | ### "Search workflows in subdirectories" 380 | 381 | **Overview** 382 | "Search workflows in subdirectories" is a setting option that controls whether Face Editor includes subdirectories in its workflow search. 383 | 384 | **Value and Impact** 385 | The value is a boolean, with either `True` or `False` to be specified. The default value is `False`, which indicates that the search does not include subdirectories. When set to `True`, the workflow search extends into subdirectories, allowing for the reference of sample workflows. 386 | 387 | --- 388 | 389 | ### "Additional components" 390 | 391 | **Overview** 392 | "Additional components" is a setting option that specifies the additional components available for use in Face Editor. 393 | 394 | **Value and Impact** 395 | This setting is a series of checkboxes labeled with component names. Checking a box (setting to Enabled) activates the corresponding component in Face Editor. 396 | 397 | --- 398 | 399 | ### "Save original image if face detection fails" 400 | 401 | **Overview** 402 | "Save original image if face detection fails" is a setting option that specifies whether to save the original image if face detection fails. 403 | 404 | **Value and Impact** 405 | The value is a boolean, with either `True` or `False` to be specified. The default value is `True`, which means that the original image will be saved if face detection fails. 406 | 407 | --- 408 | 409 | ### "Adjust tilt for detected faces" 410 | 411 | **Overview** 412 | "Adjust tilt for detected faces" is a setting option that specifies whether to adjust the tilt for detected faces. 413 | 414 | **Value and Impact** 415 | The value is a boolean, with either `True` or `False` to be specified. The default value is `False`, indicating that no tilt correction will be applied when a face is detected. Even when "Adjust tilt for detected faces" is not enabled, the tilt correction may still be applied based on the "Tilt adjustment threshold" setting. 416 | 417 | --- 418 | 419 | ### "Auto face size adjustment by model" 420 | 421 | **Overview** 422 | "Auto face size adjustment by model" is a setting option that determines whether the Face Editor automatically adjusts the size of the face based on the selected model. 423 | 424 | **Value and Impact** 425 | The setting is a checkbox. When checked (enabled): 426 | 427 | - The face size will be set to 1024 if the SDXL model is selected. For other models, the face size will be set to 512. 428 | - The ["Size of the face when recreating"](#size-of-the-face-when-recreating) setting will be hidden and its value will be ignored, since the face size will be determined based on the chosen model. 429 | 430 | --- 431 | 432 | ### "The position in postprocess at which this script will be executed" 433 | 434 | **Overview** 435 | "The position in postprocess at which this script will be executed" is a setting option that specifies the position at which this script will be executed during postprocessing. 436 | 437 | **Value and Impact** 438 | The value is an integer, with a value from 0 to 99 to be specified. A smaller value means that the script will be executed earlier. The default value is `99`, which indicates that this script will likely be executed last during postprocessing. 439 | 440 | 441 | --- 442 | ## Contribution 443 | 444 | We warmly welcome contributions to this project! If you're someone who is interested in machine learning, face processing, or just passionate about open-source, we'd love for you to contribute. 445 | 446 | ### What we are looking for: 447 | - **Workflow Definitions:** Help us expand our array of workflow definitions. If you have a unique or interesting workflow design, please don't hesitate to submit it as a sample! 448 | - **Implementations of `FaceDetector`, `FaceProcessor`, and `MaskGenerator`:** If you have alternative approaches or models for any of these components, we'd be thrilled to include your contributions in our project. 449 | 450 | Before starting your contribution, please make sure to check out our existing code base and follow the general structure. If you have any questions, don't hesitate to open an issue. We appreciate your understanding and cooperation. 451 | 452 | We're excited to see your contributions and are always here to help or provide guidance if needed. Happy coding! 453 | -------------------------------------------------------------------------------- /face_editor/entities/definitions.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Union 2 | 3 | from pydantic import BaseModel, root_validator, validator 4 | 5 | 6 | class Worker(BaseModel): 7 | name: str 8 | params: Optional[Dict] 9 | 10 | @root_validator(pre=True) 11 | def default_params(cls, values): 12 | if isinstance(values, list): 13 | values = {str(i): v for i, v in enumerate(values)} 14 | if "params" not in values or values["params"] is None: 15 | values["params"] = {} 16 | return values 17 | 18 | @validator("name") 19 | def lowercase_name(cls, v): 20 | return v.lower() 21 | 22 | 23 | def parse_worker_field(value: Union[str, Dict, Worker]) -> Worker: 24 | if isinstance(value, Dict): 25 | return Worker(**value) 26 | if isinstance(value, str): 27 | return Worker(name=value) 28 | return value 29 | 30 | 31 | class Condition(BaseModel): 32 | tag: Optional[str] 33 | criteria: Optional[str] 34 | 35 | def get_indices(self) -> List[int]: 36 | if self.criteria is None or ":" not in self.criteria: 37 | return [0] 38 | 39 | indices: List[int] = [] 40 | for part in self.criteria.split(":")[1].split(","): 41 | part = part.strip() 42 | if "-" in part: 43 | start, end = map(int, [x.strip() for x in part.split("-")]) 44 | indices.extend(range(start, end + 1)) 45 | else: 46 | indices.append(int(part)) 47 | 48 | return indices 49 | 50 | def get_criteria(self) -> str: 51 | if self.criteria is None or self.criteria == "": 52 | return "" 53 | return self.criteria.split(":")[0].strip().lower() 54 | 55 | def has_criteria(self) -> bool: 56 | return len(self.get_criteria()) > 0 57 | 58 | @validator("criteria") 59 | def validate_criteria(cls, value): 60 | c = cls() 61 | c.criteria = value 62 | c.get_indices() 63 | return value 64 | 65 | 66 | class Job(BaseModel): 67 | face_processor: Worker 68 | mask_generator: Worker 69 | 70 | @validator("face_processor", "mask_generator", pre=True) 71 | def parse_worker_fields(cls, value): 72 | return parse_worker_field(value) 73 | 74 | 75 | class Rule(BaseModel): 76 | when: Optional[Condition] = None 77 | then: Union[Job, List[Job]] 78 | 79 | @validator("then", pre=True) 80 | def parse_jobs(cls, value): 81 | if isinstance(value, Dict): 82 | return [Job.parse_obj(value)] 83 | elif isinstance(value, List): 84 | return [Job.parse_obj(job) for job in value] 85 | 86 | 87 | class Workflow(BaseModel): 88 | face_detector: Union[Worker, List[Worker]] 89 | rules: Union[Rule, List[Rule]] 90 | 91 | @validator("face_detector", pre=True) 92 | def parse_face_detector(cls, value): 93 | if isinstance(value, List): 94 | return [parse_worker_field(item) for item in value] 95 | else: 96 | return [parse_worker_field(value)] 97 | 98 | @validator("rules", pre=True) 99 | def wrap_rule_in_list(cls, value): 100 | if not isinstance(value, List): 101 | return [value] 102 | return value 103 | -------------------------------------------------------------------------------- /face_editor/entities/face.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | import cv2 4 | import numpy as np 5 | from face_editor.entities.option import Option 6 | from face_editor.entities.rect import Point, Rect 7 | from modules import images 8 | from PIL import Image 9 | 10 | 11 | class Face: 12 | def __init__(self, entire_image: np.ndarray, face_area: Rect, face_margin: float, face_size: int, upscaler: str): 13 | self.face_area = face_area 14 | self.center = face_area.center 15 | left, top, right, bottom = face_area.to_square() 16 | 17 | self.left, self.top, self.right, self.bottom = self.__ensure_margin( 18 | left, top, right, bottom, entire_image, face_margin 19 | ) 20 | 21 | self.width = self.right - self.left 22 | self.height = self.bottom - self.top 23 | 24 | self.image = self.__crop_face_image(entire_image, face_size, upscaler) 25 | self.face_size = face_size 26 | self.scale_factor = face_size / self.width 27 | self.face_area_on_image = self.__get_face_area_on_image() 28 | self.landmarks_on_image = self.__get_landmarks_on_image() 29 | 30 | def __get_face_area_on_image(self): 31 | left = int((self.face_area.left - self.left) * self.scale_factor) 32 | top = int((self.face_area.top - self.top) * self.scale_factor) 33 | right = int((self.face_area.right - self.left) * self.scale_factor) 34 | bottom = int((self.face_area.bottom - self.top) * self.scale_factor) 35 | return self.__clip_values(left, top, right, bottom) 36 | 37 | def __get_landmarks_on_image(self): 38 | landmarks = [] 39 | if self.face_area.landmarks is not None: 40 | for landmark in self.face_area.landmarks: 41 | landmarks.append( 42 | Point( 43 | int((landmark.x - self.left) * self.scale_factor), 44 | int((landmark.y - self.top) * self.scale_factor), 45 | ) 46 | ) 47 | return landmarks 48 | 49 | def __crop_face_image(self, entire_image: np.ndarray, face_size: int, upscaler: str): 50 | cropped = entire_image[self.top : self.bottom, self.left : self.right, :] 51 | if upscaler and upscaler != Option.DEFAULT_UPSCALER: 52 | return images.resize_image(0, Image.fromarray(cropped), face_size, face_size, upscaler) 53 | else: 54 | return Image.fromarray(cv2.resize(cropped, dsize=(face_size, face_size))) 55 | 56 | def __ensure_margin(self, left: int, top: int, right: int, bottom: int, entire_image: np.ndarray, margin: float): 57 | entire_height, entire_width = entire_image.shape[:2] 58 | 59 | side_length = right - left 60 | margin = min(min(entire_height, entire_width) / side_length, margin) 61 | diff = int((side_length * margin - side_length) / 2) 62 | 63 | top = top - diff 64 | bottom = bottom + diff 65 | left = left - diff 66 | right = right + diff 67 | 68 | if top < 0: 69 | bottom = bottom - top 70 | top = 0 71 | if left < 0: 72 | right = right - left 73 | left = 0 74 | 75 | if bottom > entire_height: 76 | top = top - (bottom - entire_height) 77 | bottom = entire_height 78 | if right > entire_width: 79 | left = left - (right - entire_width) 80 | right = entire_width 81 | 82 | return left, top, right, bottom 83 | 84 | def get_angle(self) -> float: 85 | landmarks = getattr(self.face_area, "landmarks", None) 86 | if landmarks is None: 87 | return 0 88 | 89 | eye1 = getattr(landmarks, "eye1", None) 90 | eye2 = getattr(landmarks, "eye2", None) 91 | if eye2 is None or eye1 is None: 92 | return 0 93 | 94 | try: 95 | dx = eye2.x - eye1.x 96 | dy = eye2.y - eye1.y 97 | if dx == 0: 98 | dx = 1 99 | angle = np.arctan(dy / dx) * 180 / np.pi 100 | 101 | if dx < 0: 102 | angle = (angle + 180) % 360 103 | return angle 104 | except Exception: 105 | print(traceback.format_exc()) 106 | return 0 107 | 108 | def rotate_face_area_on_image(self, angle: float): 109 | center = [ 110 | (self.face_area_on_image[0] + self.face_area_on_image[2]) / 2, 111 | (self.face_area_on_image[1] + self.face_area_on_image[3]) / 2, 112 | ] 113 | 114 | points = [ 115 | [self.face_area_on_image[0], self.face_area_on_image[1]], 116 | [self.face_area_on_image[2], self.face_area_on_image[3]], 117 | ] 118 | 119 | angle = np.radians(angle) 120 | rot_matrix = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) 121 | 122 | points = np.array(points) - center 123 | points = np.dot(points, rot_matrix.T) 124 | points += center 125 | left, top, right, bottom = (int(points[0][0]), int(points[0][1]), int(points[1][0]), int(points[1][1])) 126 | 127 | left, right = (right, left) if left > right else (left, right) 128 | top, bottom = (bottom, top) if top > bottom else (top, bottom) 129 | 130 | width, height = right - left, bottom - top 131 | if width < height: 132 | left, right = left - (height - width) // 2, right + (height - width) // 2 133 | elif height < width: 134 | top, bottom = top - (width - height) // 2, bottom + (width - height) // 2 135 | return self.__clip_values(left, top, right, bottom) 136 | 137 | def __clip_values(self, *args): 138 | result = [] 139 | for val in args: 140 | if val < 0: 141 | result.append(0) 142 | elif val > self.face_size: 143 | result.append(self.face_size) 144 | else: 145 | result.append(val) 146 | return tuple(result) 147 | -------------------------------------------------------------------------------- /face_editor/entities/option.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | from face_editor.io.util import workflows_dir 5 | 6 | 7 | class Option: 8 | DEFAULT_FACE_MARGIN = 1.6 9 | DEFAULT_CONFIDENCE = 0.97 10 | DEFAULT_STRENGTH1 = 0.4 11 | DEFAULT_STRENGTH2 = 0.0 12 | DEFAULT_MAX_FACE_COUNT = 20 13 | DEFAULT_MASK_SIZE = 0 14 | DEFAULT_MASK_BLUR = 12 15 | DEFAULT_PROMPT_FOR_FACE = "" 16 | DEFAULT_APPLY_INSIDE_MASK_ONLY = True 17 | DEFAULT_SAVE_ORIGINAL_IMAGE = False 18 | DEFAULT_SHOW_ORIGINAL_IMAGE = False 19 | DEFAULT_SHOW_INTERMEDIATE_STEPS = False 20 | DEFAULT_APPLY_SCRIPTS_TO_FACES = False 21 | DEFAULT_FACE_SIZE = 512 22 | DEFAULT_USE_MINIMAL_AREA = False 23 | DEFAULT_IGNORE_LARGER_FACES = True 24 | DEFAULT_AFFECTED_AREAS = ["Face"] 25 | DEFAULT_WORKFLOW = open(os.path.join(workflows_dir, "default.json")).read() 26 | DEFAULT_UPSCALER = "None" 27 | DEFAULT_TILT_ADJUSTMENT_THRESHOLD = 40 28 | 29 | def __init__(self, *args) -> None: 30 | self.extra_options: Dict[str, Dict[str, str]] = {} 31 | self.face_margin = Option.DEFAULT_FACE_MARGIN 32 | self.confidence = Option.DEFAULT_CONFIDENCE 33 | self.strength1 = Option.DEFAULT_STRENGTH1 34 | self.strength2 = Option.DEFAULT_STRENGTH2 35 | self.max_face_count = Option.DEFAULT_MAX_FACE_COUNT 36 | self.mask_size = Option.DEFAULT_MASK_SIZE 37 | self.mask_blur = Option.DEFAULT_MASK_BLUR 38 | self.prompt_for_face = Option.DEFAULT_PROMPT_FOR_FACE 39 | self.apply_inside_mask_only = Option.DEFAULT_APPLY_INSIDE_MASK_ONLY 40 | self.save_original_image = Option.DEFAULT_SAVE_ORIGINAL_IMAGE 41 | self.show_intermediate_steps = Option.DEFAULT_SHOW_INTERMEDIATE_STEPS 42 | self.apply_scripts_to_faces = Option.DEFAULT_APPLY_SCRIPTS_TO_FACES 43 | self.face_size = Option.DEFAULT_FACE_SIZE 44 | self.use_minimal_area = Option.DEFAULT_USE_MINIMAL_AREA 45 | self.ignore_larger_faces = Option.DEFAULT_IGNORE_LARGER_FACES 46 | self.affected_areas = Option.DEFAULT_AFFECTED_AREAS 47 | self.show_original_image = Option.DEFAULT_SHOW_ORIGINAL_IMAGE 48 | self.workflow = Option.DEFAULT_WORKFLOW 49 | self.upscaler = Option.DEFAULT_UPSCALER 50 | self.tilt_adjustment_threshold = Option.DEFAULT_TILT_ADJUSTMENT_THRESHOLD 51 | 52 | if len(args) > 0 and isinstance(args[0], dict): 53 | self.update_by_dict(args[0]) 54 | else: 55 | self.update_by_list(args) 56 | 57 | self.apply_scripts_to_faces = False 58 | 59 | def update_by_list(self, args: tuple) -> None: 60 | arg_len = len(args) 61 | self.face_margin = args[0] if arg_len > 0 and isinstance(args[0], (float, int)) else self.face_margin 62 | self.confidence = args[1] if arg_len > 1 and isinstance(args[1], (float, int)) else self.confidence 63 | self.strength1 = args[2] if arg_len > 2 and isinstance(args[2], (float, int)) else self.strength1 64 | self.strength2 = args[3] if arg_len > 3 and isinstance(args[3], (float, int)) else self.strength2 65 | self.max_face_count = args[4] if arg_len > 4 and isinstance(args[4], int) else self.max_face_count 66 | self.mask_size = args[5] if arg_len > 5 and isinstance(args[5], int) else self.mask_size 67 | self.mask_blur = args[6] if arg_len > 6 and isinstance(args[6], int) else self.mask_blur 68 | self.prompt_for_face = args[7] if arg_len > 7 and isinstance(args[7], str) else self.prompt_for_face 69 | self.apply_inside_mask_only = ( 70 | args[8] if arg_len > 8 and isinstance(args[8], bool) else self.apply_inside_mask_only 71 | ) 72 | self.save_original_image = args[9] if arg_len > 9 and isinstance(args[9], bool) else self.save_original_image 73 | self.show_intermediate_steps = ( 74 | args[10] if arg_len > 10 and isinstance(args[10], bool) else self.show_intermediate_steps 75 | ) 76 | self.apply_scripts_to_faces = ( 77 | args[11] if arg_len > 11 and isinstance(args[11], bool) else self.apply_scripts_to_faces 78 | ) 79 | self.face_size = args[12] if arg_len > 12 and isinstance(args[12], int) else self.face_size 80 | self.use_minimal_area = args[13] if arg_len > 13 and isinstance(args[13], bool) else self.use_minimal_area 81 | self.ignore_larger_faces = args[14] if arg_len > 14 and isinstance(args[14], bool) else self.ignore_larger_faces 82 | self.affected_areas = args[15] if arg_len > 15 and isinstance(args[15], list) else self.affected_areas 83 | self.show_original_image = args[16] if arg_len > 16 and isinstance(args[16], bool) else self.show_original_image 84 | self.workflow = args[17] if arg_len > 17 and isinstance(args[17], str) else self.workflow 85 | self.upscaler = args[18] if arg_len > 18 and isinstance(args[18], str) else self.upscaler 86 | self.tilt_adjustment_threshold = ( 87 | args[19] if arg_len > 19 and isinstance(args[19], int) else self.tilt_adjustment_threshold 88 | ) 89 | 90 | def update_by_dict(self, params: dict) -> None: 91 | self.face_margin = params.get("face_margin", self.face_margin) 92 | self.confidence = params.get("confidence", self.confidence) 93 | self.strength1 = params.get("strength1", self.strength1) 94 | self.strength2 = params.get("strength2", self.strength2) 95 | self.max_face_count = params.get("max_face_count", self.max_face_count) 96 | self.mask_size = params.get("mask_size", self.mask_size) 97 | self.mask_blur = params.get("mask_blur", self.mask_blur) 98 | self.prompt_for_face = params.get("prompt_for_face", self.prompt_for_face) 99 | self.apply_inside_mask_only = params.get("apply_inside_mask_only", self.apply_inside_mask_only) 100 | self.save_original_image = params.get("save_original_image", self.save_original_image) 101 | self.show_intermediate_steps = params.get("show_intermediate_steps", self.show_intermediate_steps) 102 | self.apply_scripts_to_faces = params.get("apply_scripts_to_faces", self.apply_scripts_to_faces) 103 | self.face_size = params.get("face_size", self.face_size) 104 | self.use_minimal_area = params.get("use_minimal_area", self.use_minimal_area) 105 | self.ignore_larger_faces = params.get("ignore_larger_faces", self.ignore_larger_faces) 106 | self.affected_areas = params.get("affected_areas", self.affected_areas) 107 | self.show_original_image = params.get("show_original_image", self.show_original_image) 108 | self.workflow = params.get("workflow", self.workflow) 109 | self.upscaler = params.get("upscaler", self.upscaler) 110 | self.tilt_adjustment_threshold = params.get("tilt_adjustment_threshold", self.tilt_adjustment_threshold) 111 | 112 | for k, v in params.items(): 113 | if isinstance(v, dict): 114 | self.extra_options[k] = v 115 | 116 | def to_dict(self) -> dict: 117 | d = { 118 | Option.add_prefix("enabled"): True, 119 | Option.add_prefix("face_margin"): self.face_margin, 120 | Option.add_prefix("confidence"): self.confidence, 121 | Option.add_prefix("strength1"): self.strength1, 122 | Option.add_prefix("strength2"): self.strength2, 123 | Option.add_prefix("max_face_count"): self.max_face_count, 124 | Option.add_prefix("mask_size"): self.mask_size, 125 | Option.add_prefix("mask_blur"): self.mask_blur, 126 | Option.add_prefix("prompt_for_face"): self.prompt_for_face if len(self.prompt_for_face) > 0 else '""', 127 | Option.add_prefix("apply_inside_mask_only"): self.apply_inside_mask_only, 128 | Option.add_prefix("apply_scripts_to_faces"): self.apply_scripts_to_faces, 129 | Option.add_prefix("face_size"): self.face_size, 130 | Option.add_prefix("use_minimal_area"): self.use_minimal_area, 131 | Option.add_prefix("ignore_larger_faces"): self.ignore_larger_faces, 132 | Option.add_prefix("affected_areas"): str.join(";", self.affected_areas), 133 | Option.add_prefix("workflow"): self.workflow, 134 | Option.add_prefix("upscaler"): self.upscaler, 135 | Option.add_prefix("tilt_adjustment_threshold"): self.tilt_adjustment_threshold, 136 | } 137 | 138 | for option_group_name, options in self.extra_options.items(): 139 | prefix = Option.add_prefix(option_group_name) 140 | for k, v in options.items(): 141 | d[f"{prefix}_{k}"] = v 142 | 143 | return d 144 | 145 | def add_options(self, option_group_name: str, options: Dict[str, str]): 146 | self.extra_options[option_group_name] = options 147 | 148 | @staticmethod 149 | def add_prefix(text: str) -> str: 150 | return "face_editor_" + text 151 | -------------------------------------------------------------------------------- /face_editor/entities/rect.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, NamedTuple, Tuple 2 | 3 | import numpy as np 4 | 5 | 6 | class Point(NamedTuple): 7 | x: int 8 | y: int 9 | 10 | 11 | class Landmarks(NamedTuple): 12 | eye1: Point 13 | eye2: Point 14 | nose: Point 15 | mouth1: Point 16 | mouth2: Point 17 | 18 | 19 | class Rect: 20 | def __init__( 21 | self, 22 | left: int, 23 | top: int, 24 | right: int, 25 | bottom: int, 26 | tag: str = "face", 27 | landmarks: Landmarks = None, 28 | attributes: Dict[str, str] = {}, 29 | ) -> None: 30 | self.tag = tag 31 | self.left = left 32 | self.top = top 33 | self.right = right 34 | self.bottom = bottom 35 | self.center = int((right + left) / 2) 36 | self.middle = int((top + bottom) / 2) 37 | self.width = right - left 38 | self.height = bottom - top 39 | self.size = self.width * self.height 40 | self.landmarks = landmarks 41 | self.attributes = attributes 42 | 43 | @classmethod 44 | def from_ndarray( 45 | cls, 46 | face_box: np.ndarray, 47 | tag: str = "face", 48 | landmarks: Landmarks = None, 49 | attributes: Dict[str, str] = {}, 50 | ) -> "Rect": 51 | left, top, right, bottom, *_ = list(map(int, face_box)) 52 | return cls(left, top, right, bottom, tag, landmarks, attributes) 53 | 54 | def to_tuple(self) -> Tuple[int, int, int, int]: 55 | return self.left, self.top, self.right, self.bottom 56 | 57 | def to_square(self): 58 | left, top, right, bottom = self.to_tuple() 59 | 60 | width = right - left 61 | height = bottom - top 62 | 63 | if width % 2 == 1: 64 | right = right + 1 65 | width = width + 1 66 | if height % 2 == 1: 67 | bottom = bottom + 1 68 | height = height + 1 69 | 70 | diff = int(abs(width - height) / 2) 71 | if width > height: 72 | top = top - diff 73 | bottom = bottom + diff 74 | else: 75 | left = left - diff 76 | right = right + diff 77 | 78 | return left, top, right, bottom 79 | -------------------------------------------------------------------------------- /face_editor/inferencers/README.md: -------------------------------------------------------------------------------- 1 | # General components 2 | 3 | ## 1. Face Detector 4 | ### 1.1 RetinaFace 5 | This face detector is used in the default workflow. It's built directly into the stable-diffusion-webui, so no additional software installation is required, reducing the chance of operational issues. 6 | 7 | This component is implemented using [facexlib/detection](https://github.com/xinntao/facexlib/blob/master/facexlib/detection/__init__.py). 8 | 9 | #### Name 10 | - RetinaFace 11 | 12 | #### Implementation 13 | - [RetinafaceDetector](retinaface_detector.py) 14 | 15 | #### Recognized UI settings 16 | - Advanced Options - (1) Face Detection - Face detection confidence 17 | 18 | #### Configuration Parameters (in JSON) 19 | - N/A 20 | 21 | #### Returns 22 | - tag: "face" 23 | - attributes: N/A 24 | - landmarks: 5 (both eyes, the nose, and both ends of the mouth) 25 | 26 | #### Usage in Workflows 27 | - [default.json](../../workflows/default.json) 28 | 29 | 30 | --- 31 | 32 | ### 1.2 lbpcascade_animeface 33 | This face detector is designed specifically for anime/manga faces. 34 | 35 | This component is implemented using [lbpcascade_animeface](https://github.com/nagadomi/lbpcascade_animeface). 36 | 37 | #### Name 38 | - lbpcascade_animeface 39 | 40 | #### Implementation 41 | - [LbpcascadeAnimefaceDetector](lbpcascade_animeface_detector.py) 42 | 43 | #### Recognized UI settings 44 | - N/A 45 | 46 | #### Configuration Parameters (in JSON) 47 | - `min_neighbors` (integer, default: 5): Specifies the minimum number of neighboring rectangles that make up an object face. A higher number results in fewer detections but higher quality, while a lower number improves detection rate but may result in more false positives. 48 | 49 | #### Returns 50 | - tag: "face" 51 | - attributes: N/A 52 | - landmarks: N/A 53 | 54 | #### Usage in Workflows 55 | - [lbpcascade_animeface.json](../../workflows/examples/lbpcascade_animeface.json) 56 | 57 | --- 58 | 59 | ## 2. Face Processor 60 | 61 | ### 2.1 img2img 62 | This is the implementation used for enhancing enlarged face images in the default workflow. 63 | 64 | #### Name 65 | - img2img 66 | 67 | #### Implementation 68 | - [Img2ImgFaceProcessor](img2img_face_processor.py) 69 | 70 | #### Recognized UI settings 71 | - Prompt for face 72 | - Advanced Options - (3) Recreate the Faces - Denoising strength for face images 73 | 74 | #### Configuration Parameters (in JSON) 75 | - `pp` (string): positive prompt. 76 | - `np` (string): negative prompt. 77 | 78 | #### Usage in Workflows 79 | - [default.json](../../workflows/default.json) 80 | 81 | --- 82 | 83 | ### 2.2 Blur 84 | This face processor applies a Gaussian blur to the detected face region. The intensity of the blur can be specified using the `radius` parameter in the 'params' of the JSON configuration. The larger the radius, the more intense the blur effect. 85 | 86 | ![Blur](../../images/inferencers/blur.jpg) 87 | 88 | #### Name 89 | - Blur 90 | 91 | #### Implementation 92 | - [BlurProcessor](blur_processor.py) 93 | 94 | #### Recognized UI settings 95 | - N/A 96 | 97 | #### Configuration Parameters (in JSON) 98 | - `radius` (integer, default: 20): The radius of the Gaussian blur filter. 99 | 100 | #### Usage in Workflows 101 | - [blur.json](../../workflows/examples/blur.json) 102 | - [blur_non_center_faces.json](../../workflows/examples/blur_non_center_faces.json) 103 | - [blur_young_people.json](../../workflows/examples/blur_young_people.json) 104 | 105 | --- 106 | 107 | ### 2.3 NoOp 108 | This face processor does not apply any processing to the detected faces. It can be used when no face enhancement or modification is desired, and only detection or other aspects of the workflow are needed. 109 | 110 | #### Name 111 | - NoOp 112 | 113 | #### Implementation 114 | - [NoOpProcessor](no_op_processor.py) 115 | 116 | #### Recognized UI settings 117 | - N/A 118 | 119 | #### Configuration Parameters (in JSON) 120 | - N/A 121 | 122 | 123 | --- 124 | 125 | ## 3. Mask Generator 126 | 127 | ### 3.1 BiSeNet 128 | This operates as the Mask Generator in the default workflow. Similar to RetinaFace Face Detector, it's integrated into the stable-diffusion-webui, making it easy to use without the need for additional software installations. It generates a mask using a deep learning model (BiSeNet) based on the face area of the image. Unique to BiSeNet, it can recognize the "Affected areas" specified by the user, including 'Face', 'Hair', 'Hat', and 'Neck'. This option will be invalid with other mask generators. It includes a fallback mechanism where if the BiSeNet fails to generate an appropriate mask, it can fall back to the `VignetteMaskGenerator`. 129 | 130 | This component is implemented using [facexlib/parsing](https://github.com/xinntao/facexlib/blob/master/facexlib/parsing/__init__.py). 131 | 132 | #### Affected areas - Face 133 | ![Face](../../images/inferencers/bisenet-face.jpg) 134 | 135 | #### Affected areas - Face, Hair 136 | ![Face, Hair](../../images/inferencers/bisenet-face-hair.jpg) 137 | 138 | #### Affected areas - Face, Hair, Hat 139 | ![Face, Hair, Hat](../../images/inferencers/bisenet-face-hair-hat.jpg) 140 | 141 | #### Affected areas - Face, Hair, Hat, Neck 142 | ![Face, Hair, Hat, Neck](../../images/inferencers/bisenet-face-hair-hat-neck.jpg) 143 | 144 | 145 | #### Name 146 | - BiSeNet 147 | 148 | #### Implementation 149 | - [BiSeNetMaskGenerator](bisenet_mask_generator.py) 150 | 151 | #### Recognized UI settings 152 | - Use minimal area (for close faces) 153 | - Affected areas 154 | - Mask size 155 | 156 | #### Configuration Parameters (in JSON) 157 | - `fallback_ratio` (float, default: 0.25): Extent to which a mask must cover the face before switching to the fallback generator. Any mask covering less than this ratio of the face will be replaced by a mask generated by the **Vignette** MaskGenerator`. 158 | 159 | #### Usage in Workflows 160 | - [default.json](../../workflows/default.json) 161 | 162 | --- 163 | 164 | ### 3.2 Vignette 165 | This mask generator creates a mask by applying a Gaussian (circular fade-out effect) to the face area. It is less computationally demanding than deep-learning-based mask generators and can consistently produce a mask under conditions where deep-learning-based mask generators such as BiSeNet or YOLO may struggle, such as with unusual face orientations or expressions. It serves as the fallback mask generator for the BiSeNet Mask Generator when it fails to generate an appropriate mask. 166 | 167 | ![Vignette](../../images/inferencers/vignette-default.jpg) 168 | 169 | #### Name 170 | - Vignette 171 | 172 | #### Implementation 173 | - [VignetteMaskGenerator](vignette_mask_generator.py) 174 | 175 | #### Recognized UI settings 176 | - Use minimal area (for close faces) 177 | 178 | #### Configuration Parameters (in JSON) 179 | - `sigma` (float, default: -1): The spread of the Gaussian effect. If not specified or set to -1, a default value of 120 will be used when `use_minimal_area` is set to True, and a default value of 180 will be used otherwise. 180 | - `keep_safe_area` (boolean, default: False): If set to True, a safe area within the ellipse around the face will be preserved entirely within the mask, preventing the fade-out effect from being applied to this area. 181 | 182 | --- 183 | 184 | ### 3.3 Ellipse 185 | This option draws an ellipse around the detected face region to generate a mask. 186 | 187 | ![Ellipse](../../images/inferencers/ellipse.jpg) 188 | 189 | #### Name 190 | - Ellipse 191 | 192 | #### Implementation 193 | - [EllipseMaskGenerator](ellipse_mask_generator.py) 194 | 195 | #### Recognized UI settings 196 | - Use minimal area (for close faces) 197 | 198 | #### Configuration Parameters (in JSON) 199 | - N/A 200 | 201 | --- 202 | 203 | ### 3.4 Rect 204 | This is a simplistic implementation that uses the detected face region as a direct mask. 205 | 206 | ![Rect](../../images/inferencers/face-area-rect.jpg) 207 | 208 | #### Name 209 | - Rect 210 | 211 | #### Implementation 212 | - [FaceAreaMaskGenerator](face_area_mask_generator.py) 213 | 214 | #### Recognized UI settings 215 | - N/A 216 | 217 | #### Configuration Parameters (in JSON) 218 | - N/A 219 | 220 | --- 221 | 222 | ### 3.5 NoMask 223 | This option generates a "mask" that is simply an all-white image of the same size as the input face image. It essentially does not mask any part of the image and can be used in scenarios where no masking is desired. 224 | 225 | ![NoMask](../../images/inferencers/no-mask.jpg) 226 | 227 | #### Implementation 228 | - [NoMaskGenerator](no_mask_generator.py) 229 | 230 | #### Name 231 | - NoMask 232 | 233 | #### Recognized UI settings 234 | - N/A 235 | 236 | #### Configuration Parameters (in JSON) 237 | - N/A 238 | 239 | #### Usage in Workflows 240 | - [blur_non_center_faces.json](../../workflows/examples/blur_non_center_faces.json) 241 | - [blur_young_people.json](../../workflows/examples/blur_young_people.json) 242 | -------------------------------------------------------------------------------- /face_editor/inferencers/anime_segmentation/README.md: -------------------------------------------------------------------------------- 1 | # Anime Segmentation components 2 | Component implementation using [Anime Segmentation](https://github.com/SkyTNT/anime-segmentation) model from the [Hugging Face Model Hub](https://huggingface.co/skytnt/anime-seg). 3 | 4 | To use the following components, please enable 'anime_segmentation' option under "Additional components" in the Face Editor section of the "Settings" tab. 5 | 6 | ## 1. Mask Generator 7 | This utilizes the [Anime Segmentation](https://github.com/SkyTNT/anime-segmentation) model to generate masks specifically designed for anime images. 8 | 9 | ![Example](../../../images//inferencers/anime_segmentation/mask.jpg) 10 | 11 | #### Name 12 | - AnimeSegmentation 13 | 14 | #### Implementation 15 | - [AnimeSegmentationMaskGenerator](mask_generator.py) 16 | 17 | #### Recognized UI settings 18 | - N/A 19 | 20 | #### Configuration Parameters (in JSON) 21 | - N/A 22 | 23 | -------------------------------------------------------------------------------- /face_editor/inferencers/anime_segmentation/installer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from face_editor.use_cases.installer import Installer 4 | 5 | 6 | class AnimeSegmentationInstaller(Installer): 7 | def name(self) -> str: 8 | return "AnimeSegmentation" 9 | 10 | def requirements(self) -> List[str]: 11 | return ["huggingface_hub", "onnxruntime"] 12 | -------------------------------------------------------------------------------- /face_editor/inferencers/anime_segmentation/mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import huggingface_hub 5 | import modules.shared as shared 6 | import numpy as np 7 | import onnxruntime as rt 8 | from face_editor.use_cases.mask_generator import MaskGenerator 9 | 10 | 11 | class AnimeSegmentationMaskGenerator(MaskGenerator): 12 | def name(self) -> str: 13 | return "AnimeSegmentation" 14 | 15 | def generate_mask( 16 | self, 17 | face_image: np.ndarray, 18 | face_area_on_image: Tuple[int, int, int, int], 19 | **kwargs, 20 | ) -> np.ndarray: 21 | device_id = shared.cmd_opts.device_id if shared.cmd_opts.device_id is not None else 0 22 | model_path = huggingface_hub.hf_hub_download("skytnt/anime-seg", "isnetis.onnx") 23 | model = rt.InferenceSession( 24 | model_path, 25 | providers=[ 26 | ( 27 | "CUDAExecutionProvider", 28 | { 29 | "device_id": device_id, 30 | }, 31 | ), 32 | "CPUExecutionProvider", 33 | ], 34 | ) 35 | 36 | mask = self.__get_mask(face_image, model) 37 | mask = (mask * 255).astype(np.uint8) 38 | return mask.repeat(3, axis=2) 39 | 40 | def __get_mask(self, face_image, model): 41 | face_image = (face_image / 255).astype(np.float32) 42 | image_input = cv2.resize(face_image, (1024, 1024)) 43 | image_input = np.transpose(image_input, (2, 0, 1)) 44 | image_input = image_input[np.newaxis, :] 45 | mask = model.run(None, {"img": image_input})[0][0] 46 | mask = np.transpose(mask, (1, 2, 0)) 47 | mask = cv2.resize(mask, (face_image.shape[0], face_image.shape[1]))[:, :, np.newaxis] 48 | return mask 49 | -------------------------------------------------------------------------------- /face_editor/inferencers/bisenet_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import cv2 4 | import modules.shared as shared 5 | import numpy as np 6 | import torch 7 | from face_editor.inferencers.vignette_mask_generator import VignetteMaskGenerator 8 | from face_editor.use_cases.mask_generator import MaskGenerator 9 | from facexlib.parsing import init_parsing_model 10 | from facexlib.utils.misc import img2tensor 11 | from torchvision.transforms.functional import normalize 12 | 13 | 14 | class BiSeNetMaskGenerator(MaskGenerator): 15 | def __init__(self) -> None: 16 | self.mask_model = init_parsing_model(device=shared.device) 17 | self.fallback_mask_generator = VignetteMaskGenerator() 18 | 19 | def name(self): 20 | return "BiSeNet" 21 | 22 | def generate_mask( 23 | self, 24 | face_image: np.ndarray, 25 | face_area_on_image: Tuple[int, int, int, int], 26 | affected_areas: List[str], 27 | mask_size: int, 28 | use_minimal_area: bool, 29 | fallback_ratio: float = 0.25, 30 | **kwargs, 31 | ) -> np.ndarray: 32 | original_face_image = face_image 33 | face_image = face_image.copy() 34 | face_image = face_image[:, :, ::-1] 35 | 36 | if use_minimal_area: 37 | face_image = MaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 38 | 39 | h, w, _ = face_image.shape 40 | 41 | if w != 512 or h != 512: 42 | rw = (int(w * (512 / w)) // 8) * 8 43 | rh = (int(h * (512 / h)) // 8) * 8 44 | face_image = cv2.resize(face_image, dsize=(rw, rh)) 45 | 46 | face_tensor = img2tensor(face_image.astype("float32") / 255.0, float32=True) 47 | normalize(face_tensor, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) 48 | face_tensor = torch.unsqueeze(face_tensor, 0).to(shared.device) 49 | 50 | with torch.no_grad(): 51 | face = self.mask_model(face_tensor)[0] 52 | face = face.squeeze(0).cpu().numpy().argmax(0) 53 | face = face.copy().astype(np.uint8) 54 | 55 | mask = self.__to_mask(face, affected_areas) 56 | if mask_size > 0: 57 | mask = cv2.dilate(mask, np.ones((5, 5), np.uint8), iterations=mask_size) 58 | 59 | if w != 512 or h != 512: 60 | mask = cv2.resize(mask, dsize=(w, h)) 61 | 62 | if MaskGenerator.calculate_mask_coverage(mask) < fallback_ratio: 63 | mask = self.fallback_mask_generator.generate_mask( 64 | original_face_image, face_area_on_image, use_minimal_area=True 65 | ) 66 | 67 | return mask 68 | 69 | def __to_mask(self, face: np.ndarray, affected_areas: List[str]) -> np.ndarray: 70 | keep_face = "Face" in affected_areas 71 | keep_neck = "Neck" in affected_areas 72 | keep_hair = "Hair" in affected_areas 73 | keep_hat = "Hat" in affected_areas 74 | 75 | mask = np.zeros((face.shape[0], face.shape[1], 3), dtype=np.uint8) 76 | num_of_class = np.max(face) 77 | for i in range(1, num_of_class + 1): 78 | index = np.where(face == i) 79 | if i < 14 and keep_face: 80 | mask[index[0], index[1], :] = [255, 255, 255] 81 | elif i == 14 and keep_neck: 82 | mask[index[0], index[1], :] = [255, 255, 255] 83 | elif i == 17 and keep_hair: 84 | mask[index[0], index[1], :] = [255, 255, 255] 85 | elif i == 18 and keep_hat: 86 | mask[index[0], index[1], :] = [255, 255, 255] 87 | return mask 88 | -------------------------------------------------------------------------------- /face_editor/inferencers/blur_processor.py: -------------------------------------------------------------------------------- 1 | from face_editor.entities.face import Face 2 | from face_editor.use_cases.face_processor import FaceProcessor 3 | from modules.processing import StableDiffusionProcessingImg2Img 4 | from PIL import Image, ImageFilter 5 | 6 | 7 | class BlurProcessor(FaceProcessor): 8 | def name(self) -> str: 9 | return "Blur" 10 | 11 | def process( 12 | self, 13 | face: Face, 14 | p: StableDiffusionProcessingImg2Img, 15 | radius=20, 16 | **kwargs, 17 | ) -> Image: 18 | return face.image.filter(filter=ImageFilter.GaussianBlur(radius)) 19 | -------------------------------------------------------------------------------- /face_editor/inferencers/debug_processor.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from face_editor.entities.face import Face 4 | from face_editor.use_cases.face_processor import FaceProcessor 5 | from modules.processing import StableDiffusionProcessingImg2Img 6 | from PIL import Image 7 | 8 | colors = [ 9 | (255, 0, 0), 10 | (0, 255, 0), 11 | (0, 0, 255), 12 | (255, 255, 0), 13 | (255, 0, 255), 14 | (0, 255, 255), 15 | (255, 255, 255), 16 | (128, 0, 0), 17 | (0, 128, 0), 18 | (128, 128, 0), 19 | (0, 0, 128), 20 | (0, 128, 128), 21 | ] 22 | 23 | 24 | def color_generator(colors): 25 | while True: 26 | for color in colors: 27 | yield color 28 | 29 | 30 | color_iter = color_generator(colors) 31 | 32 | 33 | class DebugProcessor(FaceProcessor): 34 | def name(self) -> str: 35 | return "Debug" 36 | 37 | def process( 38 | self, 39 | face: Face, 40 | p: StableDiffusionProcessingImg2Img, 41 | **kwargs, 42 | ) -> Image: 43 | image = np.array(face.image) 44 | overlay = image.copy() 45 | cv2.rectangle(overlay, (0, 0), (image.shape[1], image.shape[0]), next(color_iter), -1) 46 | l, t, r, b = face.face_area_on_image 47 | cv2.rectangle(overlay, (l, t), (r, b), (0, 0, 0), 10) 48 | if face.landmarks_on_image is not None: 49 | for landmark in face.landmarks_on_image: 50 | cv2.circle(overlay, (int(landmark.x), int(landmark.y)), 6, (0, 0, 0), 10) 51 | alpha = 0.3 52 | output = cv2.addWeighted(image, 1 - alpha, overlay, alpha, 0) 53 | return Image.fromarray(output) 54 | -------------------------------------------------------------------------------- /face_editor/inferencers/ellipse_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import numpy as np 4 | from face_editor.inferencers.face_area_mask_generator import FaceAreaMaskGenerator 5 | from face_editor.use_cases.mask_generator import MaskGenerator 6 | from PIL import Image, ImageDraw 7 | 8 | 9 | class EllipseMaskGenerator(MaskGenerator): 10 | def name(self): 11 | return "Ellipse" 12 | 13 | def generate_mask( 14 | self, 15 | face_image: np.ndarray, 16 | face_area_on_image: Tuple[int, int, int, int], 17 | use_minimal_area: bool, 18 | **kwargs, 19 | ) -> np.ndarray: 20 | if use_minimal_area: 21 | face_image = FaceAreaMaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 22 | 23 | height, width = face_image.shape[:2] 24 | img = Image.new("RGB", (width, height), (255, 255, 255)) 25 | mask = Image.new("L", (width, height)) 26 | draw_mask = ImageDraw.Draw(mask) 27 | draw_mask.ellipse(self.__get_box(face_area_on_image), fill=255) 28 | black_img = Image.new("RGB", (width, height)) 29 | face_image = Image.composite(img, black_img, mask) 30 | return np.array(face_image) 31 | 32 | def __get_box(self, face_area_on_image: Tuple[int, int, int, int]) -> List[int]: 33 | left, top, right, bottom = face_area_on_image 34 | factor = 1.2 35 | width = right - left 36 | height = bottom - top 37 | a_width = int(width * factor) 38 | a_height = int(height * factor) 39 | left = left - int((a_width - width) / 2) 40 | top = top - int((a_height - height) / 2) 41 | return [left, top, a_width, a_height] 42 | -------------------------------------------------------------------------------- /face_editor/inferencers/face_area_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import numpy as np 4 | from face_editor.use_cases.mask_generator import MaskGenerator 5 | 6 | 7 | class FaceAreaMaskGenerator(MaskGenerator): 8 | def name(self): 9 | return "Rect" 10 | 11 | def generate_mask( 12 | self, 13 | face_image: np.ndarray, 14 | face_area_on_image: Tuple[int, int, int, int], 15 | **kwargs, 16 | ) -> np.ndarray: 17 | height, width = face_image.shape[:2] 18 | img = np.ones((height, width, 3), np.uint8) * 255 19 | return MaskGenerator.mask_non_face_areas(img, face_area_on_image) 20 | -------------------------------------------------------------------------------- /face_editor/inferencers/img2img_face_processor.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from face_editor.entities.face import Face 4 | from face_editor.use_cases.face_processor import FaceProcessor 5 | from modules.processing import StableDiffusionProcessingImg2Img, process_images 6 | from PIL import Image 7 | 8 | 9 | class Img2ImgFaceProcessor(FaceProcessor): 10 | def name(self) -> str: 11 | return "img2img" 12 | 13 | def process( 14 | self, 15 | face: Face, 16 | p: StableDiffusionProcessingImg2Img, 17 | strength1: Union[float, int], 18 | pp: str = "", 19 | np: str = "", 20 | use_refiner_model_only=False, 21 | **kwargs, 22 | ) -> Image: 23 | p.init_images = [face.image] 24 | p.width = face.image.width 25 | p.height = face.image.height 26 | p.denoising_strength = strength1 27 | p.do_not_save_samples = True 28 | 29 | if len(pp) > 0: 30 | p.prompt = pp 31 | if len(np) > 0: 32 | p.negative_prompt = np 33 | 34 | if use_refiner_model_only: 35 | refiner_switch_at = p.refiner_switch_at 36 | p.refiner_switch_at = 0 37 | 38 | has_hr_checkpoint_name = ( 39 | hasattr(p, "enable_hr") 40 | and p.enable_hr 41 | and hasattr(p, "hr_checkpoint_name") 42 | and p.hr_checkpoint_name is not None 43 | and hasattr(p, "override_settings") 44 | ) 45 | if has_hr_checkpoint_name: 46 | backup_sd_model_checkpoint = p.override_settings.get("sd_model_checkpoint", None) 47 | p.override_settings["sd_model_checkpoint"] = p.hr_checkpoint_name 48 | if getattr(p, "overlay_images", []): 49 | overlay_images = p.overlay_images 50 | p.overlay_images = [] 51 | if hasattr(p, "image_mask"): 52 | image_mask = p.image_mask 53 | p.image_mask = None 54 | if hasattr(p, "mask"): 55 | mask = p.mask 56 | p.mask = None 57 | 58 | print(f"prompt for the {face.face_area.tag}: {p.prompt}") 59 | 60 | proc = process_images(p) 61 | if use_refiner_model_only: 62 | p.refiner_switch_at = refiner_switch_at 63 | if has_hr_checkpoint_name: 64 | p.override_settings["sd_model_checkpoint"] = backup_sd_model_checkpoint 65 | if getattr(p, "overlay_images", []): 66 | p.overlay_images = overlay_images 67 | if hasattr(p, "image_mask"): 68 | p.image_mask = image_mask 69 | if hasattr(p, "mask"): 70 | p.mask = mask 71 | 72 | return proc.images[0] 73 | -------------------------------------------------------------------------------- /face_editor/inferencers/insightface/README.md: -------------------------------------------------------------------------------- 1 | # InsightFace components 2 | Component implementation using [InsightFace](https://github.com/deepinsight/insightface/). 3 | 4 | To use the following components, please enable 'insightface' option under "Additional components" in the Face Editor section of the "Settings" tab. 5 | 6 | ## 1. Face Detector 7 | A Face Detector implemented using [the InsightFace Detection module](https://github.com/deepinsight/insightface/). 8 | 9 | #### Name 10 | - InsightFace 11 | 12 | #### Implementation 13 | - [InsightFaceDetector](detector.py) 14 | 15 | #### Recognized UI settings 16 | - N/A 17 | 18 | #### Configuration Parameters (in JSON) 19 | - `conf` (float, optional, default: 0.5): The confidence threshold for face detection. This specifies the minimum confidence for a face to be detected. The higher this value, the fewer faces will be detected, and the lower this value, the more faces will be detected. 20 | 21 | #### Returns 22 | - tag: "face" 23 | - attributes: 24 | - "gender": The detected gender of the face ("M" for male, "F" for female). 25 | - "age": The detected age of the face. 26 | - landmarks: 5 (both eyes, the nose, and both ends of the mouth) 27 | 28 | **Note** 29 | Please be aware that the accuracy of gender and age detection with the InsightFace Detector is at a level where it can just about be used for experimental purposes. 30 | 31 | 32 | #### Usage in Workflows 33 | - [insightface.json](../../../workflows/examples/insightface.json) 34 | - [blur_young_people.json](../../../workflows/examples/blur_young_people.json) 35 | 36 | --- 37 | 38 | ## 2. Mask Generator 39 | This component utilizes the facial landmark detection feature of InsightFace to generate masks. It identifies the 106 facial landmarks provided by the InsightFace's 'landmark_2d_106' model and uses them to construct the mask. 40 | 41 | ![Example](../../../images/inferencers/insightface/mask.jpg) 42 | 43 | #### Name 44 | - InsightFace 45 | 46 | #### Implementation 47 | - [InsightFaceMaskGenerator](mask_generator.py) 48 | 49 | #### Recognized UI settings 50 | - Use minimal area (for close faces) 51 | - Mask size 52 | 53 | #### Configuration Parameters (in JSON) 54 | - `use_convex_hull` (boolean, default: True): If set to True, the mask is created based on the convex hull (the smallest convex polygon that contains all the points) of the facial landmarks. This can help to create a more uniform and regular mask shape. If False, the mask is directly based on the face landmarks, possibly leading to a more irregular shape. 55 | - `dilate_size` (integer, default: -1): Determines the size of the morphological dilation and erosion processes. These operations can adjust the mask size and smooth its edges. If set to -1, the dilation size will be automatically set to 0 if `use_convex_hull` is True, or 40 if `use_convex_hull` is False. 56 | 57 | #### Usage in Workflows 58 | - [insightface.json](../../../workflows/examples/insightface.json) 59 | -------------------------------------------------------------------------------- /face_editor/inferencers/insightface/detector.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | from face_editor.entities.rect import Landmarks, Point, Rect 5 | from face_editor.use_cases.face_detector import FaceDetector 6 | from insightface.app import FaceAnalysis, common 7 | from PIL import Image 8 | 9 | 10 | class InsightFaceDetector(FaceDetector): 11 | def __init__(self): 12 | self.app = FaceAnalysis(allowed_modules=["detection", "genderage"]) 13 | self.app.prepare(ctx_id=-1) 14 | 15 | def name(self) -> str: 16 | return "InsightFace" 17 | 18 | def detect_faces(self, image: Image, conf: float = 0.5, **kwargs) -> List[Rect]: 19 | image_array = np.array(image) 20 | 21 | det_boxes, kpss = self.app.det_model.detect(image_array) 22 | 23 | rects = [] 24 | 25 | for box, kps in zip(det_boxes, kpss): 26 | eye1 = Point(*map(int, kps[0])) 27 | eye2 = Point(*map(int, kps[1])) 28 | nose = Point(*map(int, kps[2])) 29 | mouth2 = Point(*map(int, kps[3])) 30 | mouth1 = Point(*map(int, kps[4])) 31 | 32 | gender, age = self.app.models["genderage"].get(image_array, common.Face({"bbox": box})) 33 | gender = "M" if gender == 1 else "F" 34 | rect = Rect.from_ndarray( 35 | box, 36 | "face", 37 | landmarks=Landmarks(eye1, eye2, nose, mouth1, mouth2), 38 | attributes={"gender": gender, "age": age}, 39 | ) 40 | rects.append(rect) 41 | 42 | return rects 43 | -------------------------------------------------------------------------------- /face_editor/inferencers/insightface/installer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from face_editor.use_cases.installer import Installer 4 | 5 | 6 | class InsightFaceInstaller(Installer): 7 | def name(self) -> str: 8 | return "InsightFace" 9 | 10 | def requirements(self) -> List[str]: 11 | return ['"insightface>=0.7.3"', "onnxruntime"] 12 | 13 | def install(self) -> None: 14 | try: 15 | from face_editor.inferencers.insightface.detector import InsightFaceDetector 16 | 17 | InsightFaceDetector() 18 | except Exception: 19 | super().install() 20 | return None 21 | -------------------------------------------------------------------------------- /face_editor/inferencers/insightface/mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import numpy as np 5 | from face_editor.use_cases.mask_generator import MaskGenerator 6 | from insightface.app import FaceAnalysis, common 7 | 8 | 9 | class InsightFaceMaskGenerator(MaskGenerator): 10 | def __init__(self): 11 | self.app = FaceAnalysis(allowed_modules=["detection", "landmark_2d_106"]) 12 | self.app.prepare(ctx_id=-1) 13 | 14 | def name(self): 15 | return "InsightFace" 16 | 17 | def generate_mask( 18 | self, 19 | face_image: np.ndarray, 20 | face_area_on_image: Tuple[int, int, int, int], 21 | use_minimal_area: bool, 22 | mask_size: int, 23 | use_convex_hull: bool = True, 24 | dilate_size: int = -1, 25 | **kwargs, 26 | ) -> np.ndarray: 27 | if dilate_size == -1: 28 | dilate_size = 0 if use_convex_hull else 40 29 | 30 | face = common.Face({"bbox": np.array(face_area_on_image)}) 31 | landmarks = self.app.models["landmark_2d_106"].get(face_image, face) 32 | 33 | if use_minimal_area: 34 | face_image = MaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 35 | 36 | mask = np.zeros(face_image.shape[0:2], dtype=np.uint8) 37 | points = [(int(landmark[0]), int(landmark[1])) for landmark in landmarks] 38 | if use_convex_hull: 39 | points = cv2.convexHull(np.array(points)) 40 | cv2.drawContours(mask, [np.array(points)], -1, (255, 255, 255), -1) 41 | 42 | if dilate_size > 0: 43 | kernel = np.ones((dilate_size, dilate_size), np.uint8) 44 | mask = cv2.dilate(mask, kernel) 45 | mask = cv2.erode(mask, kernel) 46 | 47 | if mask_size > 0: 48 | mask = cv2.dilate(mask, np.ones((5, 5), np.uint8), iterations=mask_size) 49 | 50 | return cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) 51 | -------------------------------------------------------------------------------- /face_editor/inferencers/lbpcascade_animeface_detector.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Sequence 3 | 4 | import cv2 5 | import numpy as np 6 | from face_editor.entities.rect import Rect 7 | from face_editor.io.util import assets_dir 8 | from face_editor.use_cases.face_detector import FaceDetector 9 | from PIL import Image 10 | 11 | 12 | class LbpcascadeAnimefaceDetector(FaceDetector): 13 | def __init__(self) -> None: 14 | self.cascade_file = os.path.join(assets_dir, "lbpcascade_animeface.xml") 15 | 16 | def name(self): 17 | return "lbpcascade_animeface" 18 | 19 | def detect_faces(self, image: Image, min_neighbors: int = 5, **kwargs) -> List[Rect]: 20 | cascade = cv2.CascadeClassifier(self.cascade_file) 21 | gray = cv2.cvtColor(np.array(image), cv2.COLOR_BGR2GRAY) 22 | gray = cv2.equalizeHist(gray) 23 | xywhs = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=min_neighbors, minSize=(24, 24)) 24 | return self.__xywh_to_ltrb(xywhs) 25 | 26 | def __xywh_to_ltrb(self, xywhs: Sequence) -> List[Rect]: 27 | ltrbs = [] 28 | for xywh in xywhs: 29 | x, y, w, h = xywh 30 | ltrbs.append(Rect(x, y, x + w, y + h)) 31 | return ltrbs 32 | -------------------------------------------------------------------------------- /face_editor/inferencers/mediapipe/README.md: -------------------------------------------------------------------------------- 1 | # Mediapipe components 2 | Component implementation using [MediaPipe](https://developers.google.com/mediapipe). 3 | 4 | To use the following components, please enable 'mediapipe' option under "Additional components" in the Face Editor section of the "Settings" tab. 5 | 6 | ## 1. Face Detector 7 | A Face Detector implemented using [the face detection feature of MediaPipe](https://developers.google.com/mediapipe/solutions/vision/face_detector). 8 | 9 | #### Name 10 | - MediaPipe 11 | 12 | #### Implementation 13 | - [MediaPipeFaceDetector](face_detector.py) 14 | 15 | #### Recognized UI settings 16 | - N/A 17 | 18 | #### Configuration Parameters (in JSON) 19 | - `conf` (float, optional, default: 0.01): The confidence threshold for face detection. This specifies the minimum confidence for a face to be detected. The higher this value, the fewer faces will be detected, and the lower this value, the more faces will be detected. 20 | 21 | #### Returns 22 | - tag: "face" 23 | - attributes: N/A 24 | - landmarks: 5 (both eyes, the nose, and the center of the mouth) 25 | 26 | #### Usage in Workflows 27 | - [mediapipe.json](../../../workflows/examples/mediapipe.json) 28 | 29 | 30 | --- 31 | 32 | ## 2. Mask Generator 33 | This mask generator uses the MediaPipe Face Mesh model to generate masks. It identifies a set of facial landmarks and interpolates these to create a mask. 34 | 35 | ![Example](../../../images/inferencers/mediapipe/mask.jpg) 36 | 37 | #### Name 38 | - MediaPipe 39 | 40 | #### Implementation 41 | - [MediaPipeMaskGenerator](mask_generator.py) 42 | 43 | #### Recognized UI settings 44 | - Use minimal area (for close faces) 45 | - Mask size 46 | 47 | #### Configuration Parameters (in JSON) 48 | - `use_convex_hull` (boolean, default: True): If set to True, the mask is created based on the convex hull (the smallest convex polygon that contains all the points) of the facial landmarks. This can help to create a more uniform and regular mask shape. If False, the mask is directly based on the face landmarks, possibly leading to a more irregular shape. 49 | - `dilate_size` (integer, default: -1): Determines the size of the morphological dilation and erosion processes. These operations can adjust the mask size and smooth its edges. If set to -1, the dilation size will be automatically set to 0 if `use_convex_hull` is True, or 40 if `use_convex_hull` is False. 50 | - `conf` (float, default: 0.01): Confidence threshold for the MediaPipe Face Mesh model. Any landmarks detected with a confidence lower than this value will be ignored during mask generation. 51 | 52 | #### Usage in Workflows 53 | - [mediapipe.json](../../../workflows/examples/mediapipe.json) 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /face_editor/inferencers/mediapipe/face_detector.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import mediapipe as mp 4 | import numpy as np 5 | from face_editor.entities.rect import Landmarks, Point, Rect 6 | from face_editor.use_cases.face_detector import FaceDetector 7 | from PIL import Image 8 | 9 | 10 | class MediaPipeFaceDetector(FaceDetector): 11 | def name(self) -> str: 12 | return "MediaPipe" 13 | 14 | def detect_faces(self, image: Image, conf: float = 0.01, **kwargs) -> List[Rect]: 15 | face_detection = mp.solutions.face_detection.FaceDetection(min_detection_confidence=conf) 16 | results = face_detection.process(np.array(image.convert("RGB"))) 17 | 18 | width = image.width 19 | height = image.height 20 | rects: List[Rect] = [] 21 | 22 | if not results.detections: 23 | return rects 24 | 25 | for d in results.detections: 26 | relative_box = d.location_data.relative_bounding_box 27 | left = int(relative_box.xmin * width) 28 | top = int(relative_box.ymin * height) 29 | right = int(left + (relative_box.width * width)) 30 | bottom = int(top + (relative_box.height * height)) 31 | 32 | keypoints = d.location_data.relative_keypoints 33 | 34 | eye1 = Point(int(keypoints[0].x * width), int(keypoints[0].y * height)) 35 | eye2 = Point(int(keypoints[1].x * width), int(keypoints[1].y * height)) 36 | nose = Point(int(keypoints[2].x * width), int(keypoints[2].y * height)) 37 | mouth = Point(int(keypoints[3].x * width), int(keypoints[3].y * height)) 38 | 39 | rects.append(Rect(left, top, right, bottom, landmarks=Landmarks(eye1, eye2, nose, mouth, mouth))) 40 | 41 | return rects 42 | -------------------------------------------------------------------------------- /face_editor/inferencers/mediapipe/installer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from face_editor.use_cases.installer import Installer 4 | 5 | 6 | class MediaPipeInstaller(Installer): 7 | def name(self) -> str: 8 | return "MediaPipe" 9 | 10 | def requirements(self) -> List[str]: 11 | return ["mediapipe"] 12 | -------------------------------------------------------------------------------- /face_editor/inferencers/mediapipe/mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import mediapipe as mp 5 | import numpy as np 6 | from face_editor.use_cases.mask_generator import MaskGenerator 7 | 8 | 9 | class MediaPipeMaskGenerator(MaskGenerator): 10 | def name(self) -> str: 11 | return "MediaPipe" 12 | 13 | def generate_mask( 14 | self, 15 | face_image: np.ndarray, 16 | face_area_on_image: Tuple[int, int, int, int], 17 | use_minimal_area: bool, 18 | mask_size: int, 19 | use_convex_hull: bool = True, 20 | dilate_size: int = -1, 21 | conf: float = 0.01, 22 | **kwargs, 23 | ) -> np.ndarray: 24 | if dilate_size == -1: 25 | dilate_size = 0 if use_convex_hull else 40 26 | 27 | face_mesh = mp.solutions.face_mesh.FaceMesh(min_detection_confidence=conf) 28 | results = face_mesh.process(face_image) 29 | 30 | if not results.multi_face_landmarks: 31 | return np.zeros(face_image.shape, dtype=np.uint8) 32 | 33 | if use_minimal_area: 34 | face_image = MaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 35 | 36 | mask = np.zeros(face_image.shape[0:2], dtype=np.uint8) 37 | for face_landmarks in results.multi_face_landmarks: 38 | points = [] 39 | for i in range(0, len(face_landmarks.landmark)): 40 | pt = face_landmarks.landmark[i] 41 | x, y = int(pt.x * face_image.shape[1]), int(pt.y * face_image.shape[0]) 42 | points.append((x, y)) 43 | if use_convex_hull: 44 | points = cv2.convexHull(np.array(points)) 45 | cv2.drawContours(mask, [np.array(points)], -1, (255, 255, 255), -1) 46 | 47 | if dilate_size > 0: 48 | dilate_kernel = np.ones((dilate_size, dilate_size), np.uint8) 49 | mask = cv2.dilate(mask, dilate_kernel) 50 | mask = cv2.erode(mask, dilate_kernel) 51 | 52 | if mask_size > 0: 53 | mask = cv2.dilate(mask, np.ones((5, 5), np.uint8), iterations=mask_size) 54 | 55 | return cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) 56 | -------------------------------------------------------------------------------- /face_editor/inferencers/no_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import numpy as np 4 | from face_editor.use_cases.mask_generator import MaskGenerator 5 | 6 | 7 | class NoMaskGenerator(MaskGenerator): 8 | def name(self): 9 | return "NoMask" 10 | 11 | def generate_mask( 12 | self, 13 | face_image: np.ndarray, 14 | face_area_on_image: Tuple[int, int, int, int], 15 | **kwargs, 16 | ) -> np.ndarray: 17 | height, width = face_image.shape[:2] 18 | return np.ones((height, width, 3), np.uint8) * 255 19 | -------------------------------------------------------------------------------- /face_editor/inferencers/no_op_processor.py: -------------------------------------------------------------------------------- 1 | from face_editor.entities.face import Face 2 | from face_editor.use_cases.face_processor import FaceProcessor 3 | from modules.processing import StableDiffusionProcessingImg2Img 4 | from PIL import Image 5 | 6 | 7 | class NoOpProcessor(FaceProcessor): 8 | def name(self) -> str: 9 | return "NoOp" 10 | 11 | def process( 12 | self, 13 | face: Face, 14 | p: StableDiffusionProcessingImg2Img, 15 | **kwargs, 16 | ) -> Image: 17 | return None 18 | -------------------------------------------------------------------------------- /face_editor/inferencers/openmmlab/README.md: -------------------------------------------------------------------------------- 1 | # OpenMMLab components 2 | Component implementation using [OpenMMLab](https://openmmlab.com/). 3 | 4 | To use this, please enable 'openmmlab' option under "Additional components" in the Face Editor section of the "Settings" tab. 5 | 6 | ## 1. Mask Generator 7 | This component utilizes a [Occlusion-aware face segmentation model](https://huggingface.co/ototadana/occlusion-aware-face-segmentation) for mask generation. Because the model is trained by [High-Quality Synthetic Face Occlusion Segmentation Datasets](https://github.com/kennyvoo/face-occlusion-generation), if a face is covered by another object, such as a hand or mask, it can be excluded and the facial segment can be extracted. 8 | 9 | ![Example](../../../images/inferencers/openmmlab/mask.jpg) 10 | 11 | #### Name 12 | - MMSeg 13 | 14 | #### Implementation 15 | - [MMSegMaskGenerator](mmseg_mask_generator.py) 16 | 17 | #### Recognized UI settings 18 | - Use minimal area (for close faces) 19 | - Mask size 20 | 21 | #### Usage in Workflows 22 | - [openmmlab.json](../../../workflows/examples/openmmlab.json) 23 | -------------------------------------------------------------------------------- /face_editor/inferencers/openmmlab/installer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import launch 4 | from face_editor.use_cases.installer import Installer 5 | 6 | 7 | class OpenMMLabInstaller(Installer): 8 | def name(self) -> str: 9 | return "OpenMMLab" 10 | 11 | def install(self) -> None: 12 | launch.run_pip( 13 | 'install openmim "mmsegmentation>=1.0.0" huggingface_hub mmdet', 14 | "requirements for openmmlab inferencers of Face Editor", 15 | ) 16 | cmd = "mim" 17 | if os.name == "nt": 18 | cmd = os.path.join("venv", "Scripts", cmd) 19 | 20 | launch.run(f"{cmd} install mmengine") 21 | launch.run(f'{cmd} install "mmcv>=2.0.0"') 22 | -------------------------------------------------------------------------------- /face_editor/inferencers/openmmlab/mmseg_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import modules.shared as shared 5 | import numpy as np 6 | from face_editor.use_cases.mask_generator import MaskGenerator 7 | from huggingface_hub import hf_hub_download 8 | from mmseg.apis import inference_model, init_model 9 | 10 | 11 | class MMSegMaskGenerator(MaskGenerator): 12 | def __init__(self): 13 | checkpoint_file = hf_hub_download( 14 | repo_id="ototadana/occlusion-aware-face-segmentation", 15 | filename="deeplabv3plus_r101_512x512_face-occlusion-93ec6695.pth", 16 | ) 17 | config_file = hf_hub_download( 18 | repo_id="ototadana/occlusion-aware-face-segmentation", 19 | filename="deeplabv3plus_r101_512x512_face-occlusion.py", 20 | ) 21 | self.model = init_model(config_file, checkpoint_file, device=shared.device) 22 | 23 | def name(self) -> str: 24 | return "MMSeg" 25 | 26 | def generate_mask( 27 | self, 28 | face_image: np.ndarray, 29 | face_area_on_image: Tuple[int, int, int, int], 30 | mask_size: int, 31 | use_minimal_area: bool, 32 | **kwargs, 33 | ) -> np.ndarray: 34 | face_image = face_image.copy() 35 | face_image = face_image[:, :, ::-1] 36 | 37 | if use_minimal_area: 38 | face_image = MaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 39 | 40 | result = inference_model(self.model, face_image) 41 | pred_sem_seg = result.pred_sem_seg 42 | pred_sem_seg_data = pred_sem_seg.data.squeeze(0) 43 | pred_sem_seg_np = pred_sem_seg_data.cpu().numpy() 44 | pred_sem_seg_np = (pred_sem_seg_np * 255).astype(np.uint8) 45 | 46 | mask = cv2.cvtColor(pred_sem_seg_np, cv2.COLOR_BGR2RGB) 47 | if mask_size > 0: 48 | mask = cv2.dilate(mask, np.ones((5, 5), np.uint8), iterations=mask_size) 49 | 50 | return mask 51 | -------------------------------------------------------------------------------- /face_editor/inferencers/retinaface_detector.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import modules.shared as shared 4 | import torch 5 | from face_editor.entities.rect import Landmarks, Point, Rect 6 | from face_editor.use_cases.face_detector import FaceDetector 7 | from facexlib.detection import init_detection_model, retinaface 8 | from PIL import Image 9 | 10 | 11 | class RetinafaceDetector(FaceDetector): 12 | def __init__(self) -> None: 13 | if hasattr(retinaface, "device"): 14 | retinaface.device = shared.device 15 | self.detection_model = init_detection_model("retinaface_resnet50", device=shared.device) 16 | 17 | def name(self): 18 | return "RetinaFace" 19 | 20 | def detect_faces(self, image: Image, confidence: float, **kwargs) -> List[Rect]: 21 | with torch.no_grad(): 22 | boxes_landmarks = self.detection_model.detect_faces(image, confidence) 23 | 24 | faces = [] 25 | for box_landmark in boxes_landmarks: 26 | face_box = box_landmark[:5] 27 | landmark = box_landmark[5:] 28 | face = Rect.from_ndarray(face_box) 29 | 30 | eye1 = Point(int(landmark[0]), int(landmark[1])) 31 | eye2 = Point(int(landmark[2]), int(landmark[3])) 32 | nose = Point(int(landmark[4]), int(landmark[5])) 33 | mouth2 = Point(int(landmark[6]), int(landmark[7])) 34 | mouth1 = Point(int(landmark[8]), int(landmark[9])) 35 | 36 | face.landmarks = Landmarks(eye1, eye2, nose, mouth1, mouth2) 37 | faces.append(face) 38 | 39 | return faces 40 | -------------------------------------------------------------------------------- /face_editor/inferencers/rotate_face_processor.py: -------------------------------------------------------------------------------- 1 | from face_editor.entities.face import Face 2 | from face_editor.use_cases.face_processor import FaceProcessor 3 | from face_editor.use_cases.image_processing_util import rotate_image 4 | from modules.processing import StableDiffusionProcessingImg2Img 5 | from PIL import Image 6 | 7 | 8 | class RotateFaceProcessor(FaceProcessor): 9 | def name(self) -> str: 10 | return "Rotate" 11 | 12 | def process(self, face: Face, p: StableDiffusionProcessingImg2Img, angle: float = 0, **kwargs) -> Image: 13 | return rotate_image(face.image, angle) 14 | -------------------------------------------------------------------------------- /face_editor/inferencers/vignette_mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import numpy as np 5 | from face_editor.use_cases.mask_generator import MaskGenerator 6 | 7 | 8 | class VignetteMaskGenerator(MaskGenerator): 9 | def name(self): 10 | return "Vignette" 11 | 12 | def generate_mask( 13 | self, 14 | face_image: np.ndarray, 15 | face_area_on_image: Tuple[int, int, int, int], 16 | use_minimal_area: bool, 17 | sigma: float = -1, 18 | keep_safe_area: bool = False, 19 | **kwargs, 20 | ) -> np.ndarray: 21 | (left, top, right, bottom) = face_area_on_image 22 | w, h = right - left, bottom - top 23 | mask = np.zeros((face_image.shape[0], face_image.shape[1]), dtype=np.uint8) 24 | if use_minimal_area: 25 | sigma = 120 if sigma == -1 else sigma 26 | mask[top : top + h, left : left + w] = 255 27 | else: 28 | sigma = 180 if sigma == -1 else sigma 29 | h, w = face_image.shape[0], face_image.shape[1] 30 | mask[:, :] = 255 31 | 32 | Y = np.linspace(0, h, h, endpoint=False) 33 | X = np.linspace(0, w, w, endpoint=False) 34 | Y, X = np.meshgrid(Y, X) 35 | Y -= h / 2 36 | X -= w / 2 37 | 38 | gaussian = np.exp(-(X**2 + Y**2) / (2 * sigma**2)) 39 | gaussian_mask = np.uint8(255 * gaussian.T) 40 | if use_minimal_area: 41 | mask[top : top + h, left : left + w] = gaussian_mask 42 | else: 43 | mask[:, :] = gaussian_mask 44 | 45 | if keep_safe_area: 46 | mask = cv2.ellipse(mask, ((left + right) // 2, (top + bottom) // 2), (w // 2, h // 2), 0, 0, 360, 255, -1) 47 | 48 | mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) 49 | return mask 50 | -------------------------------------------------------------------------------- /face_editor/inferencers/yolo/README.md: -------------------------------------------------------------------------------- 1 | # YOLO components 2 | Component implementation using [YOLO](https://github.com/ultralytics/ultralytics). 3 | 4 | To use this, please enable 'yolo' option under "Additional components" in the Face Editor section of the "Settings" tab. 5 | 6 | ## 1. Face Detector 7 | This component utilizes an object detection model from YOLO for face detection. Though not exclusively designed for face detection, it can identify other objects of interest as well. Effectiveness can be enhanced by utilizing a model specifically trained on faces. 8 | 9 | #### Name 10 | - YOLO 11 | 12 | #### Implementation 13 | - [YoloDetector](detector.py) 14 | 15 | #### Recognized UI settings 16 | - N/A 17 | 18 | #### Configuration Parameters (in JSON) 19 | - `path` (string, default: "yolov8n.pt"): Path to the model file. If `repo_id` is specified, the model will be downloaded from Hugging Face Model Hub instead, using `repo_id` and `filename`. 20 | - `repo_id` (string, optional): The repository ID if the model is hosted on Hugging Face Model Hub. If this is specified, `path` will be ignored. 21 | - `filename` (string, optional): The filename of the model in the Hugging Face Model Hub repository. Use this in combination with `repo_id`. 22 | - `conf`: (float, optional, default: 0.5): The confidence threshold for object detection. 23 | 24 | #### Returns 25 | - tag: The class of the detected object (as trained in the YOLO model) 26 | - attributes: N/A 27 | - landmarks: N/A 28 | 29 | #### Usage in Workflows 30 | - [yolov8n.json](../../../workflows/examples/yolov8n.json) 31 | - [adetailer.json](../../../workflows/examples/adetailer.json) 32 | 33 | 34 | --- 35 | 36 | ## 2. Mask Generator 37 | This component utilizes a segmentation model from YOLO (You Only Look Once) for mask generation. 38 | 39 | #### Name 40 | - YOLO 41 | 42 | #### Implementation 43 | - [YoloMaskGenerator](mask_generator.py) 44 | 45 | #### Recognized UI settings 46 | - Use minimal area (for close faces) 47 | 48 | #### Configuration Parameters (in JSON) 49 | - `path` (string, default: "yolov8n-seg.pt"): Path to the model file. If `repo_id` is specified, the model will be downloaded from Hugging Face Model Hub instead, using `repo_id` and `filename`. 50 | - `repo_id` (string, optional): The repository ID if the model is hosted on Hugging Face Model Hub. If this is specified, `path` will be ignored. 51 | - `filename` (string, optional): The filename of the model in the Hugging Face Model Hub repository. Use this in combination with `repo_id`. 52 | - `conf` (float, default: 0.5): Confidence threshold for detections. Any detection with a confidence lower than this will be ignored. 53 | 54 | #### Usage in Workflows 55 | - [yolov8n.json](../../../workflows/examples/yolov8n.json) 56 | -------------------------------------------------------------------------------- /face_editor/inferencers/yolo/detector.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import modules.shared as shared 4 | from face_editor.entities.rect import Rect 5 | from face_editor.inferencers.yolo.inferencer import YoloInferencer 6 | from face_editor.use_cases.face_detector import FaceDetector 7 | from PIL import Image 8 | 9 | 10 | class YoloDetector(FaceDetector, YoloInferencer): 11 | def name(self) -> str: 12 | return "YOLO" 13 | 14 | def detect_faces( 15 | self, 16 | image: Image, 17 | path: str = "yolov8n.pt", 18 | repo_id: str = None, 19 | filename: str = None, 20 | conf: float = 0.5, 21 | **kwargs, 22 | ) -> List[Rect]: 23 | self.load_model(path, repo_id, filename) 24 | 25 | names = self.model.names 26 | output = self.model.predict(image, device=shared.device) 27 | 28 | faces = [] 29 | for detection in output: 30 | boxes = detection.boxes 31 | for box in boxes: 32 | tag = names[int(box.cls)] 33 | box_conf = float(box.conf[0]) 34 | if box_conf >= conf: 35 | l, t, r, b = ( 36 | int(box.xyxy[0][0]), 37 | int(box.xyxy[0][1]), 38 | int(box.xyxy[0][2]), 39 | int(box.xyxy[0][3]), 40 | ) 41 | faces.append(Rect(l, t, r, b, tag)) 42 | print(f"{tag} detected at ({l}, {t}, {r}, {b}), Confidence: {box_conf:.2f}") 43 | 44 | return faces 45 | -------------------------------------------------------------------------------- /face_editor/inferencers/yolo/inferencer.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from huggingface_hub import hf_hub_download 4 | from ultralytics import YOLO 5 | 6 | 7 | class YoloInferencer: 8 | def __init__(self, task: str = None): 9 | self.task = task 10 | self.model = None 11 | self.loaded_path: Optional[str] = None 12 | self.loaded_repo_id: Optional[str] = None 13 | self.loaded_file_name: Optional[str] = None 14 | 15 | def load_model(self, path: str = None, repo_id: str = None, filename: str = None): 16 | if self.model is None or path != self.loaded_path or repo_id != self.loaded_repo_id: 17 | if repo_id is not None: 18 | path = hf_hub_download(repo_id, filename) 19 | self.model = YOLO(path, task=self.task) 20 | self.loaded_path = path 21 | self.loaded_repo_id = repo_id 22 | self.loaded_file_name = filename 23 | -------------------------------------------------------------------------------- /face_editor/inferencers/yolo/installer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from face_editor.use_cases.installer import Installer 4 | 5 | 6 | class YoloDetectorInstaller(Installer): 7 | def name(self) -> str: 8 | return "YOLO" 9 | 10 | def requirements(self) -> List[str]: 11 | return ["huggingface_hub", "ultralytics"] 12 | -------------------------------------------------------------------------------- /face_editor/inferencers/yolo/mask_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import cv2 4 | import modules.shared as shared 5 | import numpy as np 6 | from face_editor.inferencers.yolo.inferencer import YoloInferencer 7 | from face_editor.use_cases.mask_generator import MaskGenerator 8 | 9 | 10 | class YoloMaskGenerator(MaskGenerator, YoloInferencer): 11 | def __init__(self): 12 | super().__init__("segment") 13 | 14 | def name(self) -> str: 15 | return "YOLO" 16 | 17 | def generate_mask( 18 | self, 19 | face_image: np.ndarray, 20 | face_area_on_image: Tuple[int, int, int, int], 21 | use_minimal_area: bool, 22 | tag: str = "face", 23 | path: str = "yolov8n-seg.pt", 24 | repo_id: str = None, 25 | filename: str = None, 26 | conf: float = 0.5, 27 | **kwargs, 28 | ) -> np.ndarray: 29 | self.load_model(path, repo_id, filename) 30 | 31 | names = self.model.names 32 | output = self.model.predict(face_image, device=shared.device) 33 | 34 | if use_minimal_area: 35 | face_image = MaskGenerator.mask_non_face_areas(face_image, face_area_on_image) 36 | 37 | combined_mask = np.zeros(face_image.shape[:2], np.uint8) 38 | 39 | for detection in output: 40 | boxes = detection.boxes 41 | for i, box in enumerate(boxes): 42 | box_tag = names[int(box.cls)] 43 | box_conf = float(box.conf[0]) 44 | if box_tag != tag or box_conf < conf: 45 | continue 46 | if detection.masks is None: 47 | print(f"This model may not support masks: {self.loaded_path}") 48 | continue 49 | mask = cv2.resize(detection.masks[i].data[0].cpu().numpy(), (512, 512)) 50 | combined_mask += (mask * 255).astype(np.uint8) 51 | 52 | return np.dstack([combined_mask] * 3) 53 | -------------------------------------------------------------------------------- /face_editor/io/util.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import inspect 3 | import os 4 | from typing import List, Type 5 | 6 | import modules.scripts as scripts 7 | from modules import shared 8 | 9 | 10 | def get_path(*p: str) -> str: 11 | dir = os.path.join(scripts.basedir(), *p) 12 | if not os.path.isdir(dir): 13 | dir = os.path.join(scripts.basedir(), "extensions", "sd-face-editor", *p) 14 | if not os.path.isdir(dir): 15 | raise RuntimeError(f"not found:{dir}") 16 | return dir 17 | 18 | 19 | workflows_dir = os.path.join(get_path("workflows")) 20 | assets_dir = os.path.join(get_path("assets")) 21 | inferencers_dir = os.path.join(get_path("face_editor", "inferencers")) 22 | 23 | 24 | def load_classes_from_file(file_path: str, base_class: Type) -> List[Type]: 25 | spec = importlib.util.spec_from_file_location("module.name", file_path) 26 | if spec is None or spec.loader is None: 27 | raise ImportError(f"Could not load the module from {file_path}") 28 | module = importlib.util.module_from_spec(spec) 29 | classes = [] 30 | 31 | try: 32 | spec.loader.exec_module(module) 33 | for name, cls in inspect.getmembers(module, inspect.isclass): 34 | if issubclass(cls, base_class) and cls is not base_class: 35 | classes.append(cls) 36 | except Exception as e: 37 | print(file_path, ":", e) 38 | 39 | return classes 40 | 41 | 42 | def load_classes_from_directory(base_class: Type, installer: bool = False) -> List[Type]: 43 | if not installer: 44 | all_classes = load_classes_from_directory_(base_class, inferencers_dir, False) 45 | else: 46 | all_classes = [] 47 | 48 | for component in shared.opts.data.get("face_editor_additional_components", []): 49 | all_classes.extend( 50 | load_classes_from_directory_(base_class, os.path.join(inferencers_dir, component), installer) 51 | ) 52 | 53 | return all_classes 54 | 55 | 56 | def load_classes_from_directory_(base_class: Type, dir: str, installer: bool) -> List[Type]: 57 | all_classes = [] 58 | for file in os.listdir(dir): 59 | if installer and "install" not in file: 60 | continue 61 | 62 | if file.endswith(".py") and file != os.path.basename(__file__): 63 | file_path = os.path.join(dir, file) 64 | try: 65 | classes = load_classes_from_file(file_path, base_class) 66 | if classes: 67 | all_classes.extend(classes) 68 | except Exception as e: 69 | print(f"Face Editor: Can't load {file_path}") 70 | print(str(e)) 71 | 72 | return all_classes 73 | -------------------------------------------------------------------------------- /face_editor/ui/param_value_parser.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from modules.scripts import script_callbacks 4 | 5 | 6 | class ParamValueParser: 7 | value_types: Dict[str, type] = {} 8 | 9 | @staticmethod 10 | def update(infotext: str, params: Dict[str, Any]): 11 | for key, value_type in ParamValueParser.value_types.items(): 12 | value = params.get(key, None) 13 | if value is None: 14 | return 15 | if value_type == list and not isinstance(value, list): 16 | params[key] = value.split(";") 17 | 18 | @staticmethod 19 | def add(key: str, value_type: type): 20 | if len(ParamValueParser.value_types) == 0: 21 | script_callbacks.on_infotext_pasted(ParamValueParser.update) 22 | ParamValueParser.value_types[key] = value_type 23 | -------------------------------------------------------------------------------- /face_editor/ui/ui_builder.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import gradio as gr 4 | from face_editor.entities.option import Option 5 | from face_editor.io.util import inferencers_dir 6 | from face_editor.ui import workflow_editor 7 | from face_editor.ui.param_value_parser import ParamValueParser 8 | from modules import script_callbacks, shared 9 | 10 | 11 | class UiBuilder: 12 | def __init__(self, for_extension: bool) -> None: 13 | self.infotext_fields: list = [] 14 | self.__for_extension = for_extension 15 | 16 | def build(self, is_img2img: bool): 17 | if self.__for_extension: 18 | with gr.Accordion("Face Editor", open=False, elem_id="sd-face-editor-extension"): 19 | with gr.Row(): 20 | enabled = gr.Checkbox(label="Enabled", value=False) 21 | workflow_selector = self.__create_workflow_selector() 22 | components = [enabled] + self.__build(workflow_selector) 23 | self.infotext_fields.append((enabled, Option.add_prefix("enabled"))) 24 | return components 25 | else: 26 | return self.__build(self.__create_workflow_selector()) 27 | 28 | def __create_workflow_selector(self) -> gr.Dropdown: 29 | return gr.Dropdown(choices=workflow_editor.get_files(), label="Workflow", value="default", show_label=True) 30 | 31 | def __build(self, workflow_selector: gr.Dropdown): 32 | use_minimal_area = gr.Checkbox( 33 | label="Use minimal area (for close faces)", value=Option.DEFAULT_USE_MINIMAL_AREA 34 | ) 35 | self.infotext_fields.append((use_minimal_area, Option.add_prefix("use_minimal_area"))) 36 | 37 | save_original_image = gr.Checkbox(label="Save original image", value=Option.DEFAULT_SAVE_ORIGINAL_IMAGE) 38 | self.infotext_fields.append((save_original_image, Option.add_prefix("save_original_image"))) 39 | 40 | with gr.Row(): 41 | show_original_image = gr.Checkbox(label="Show original image", value=Option.DEFAULT_SHOW_ORIGINAL_IMAGE) 42 | self.infotext_fields.append((show_original_image, Option.add_prefix("show_original_image"))) 43 | 44 | show_intermediate_steps = gr.Checkbox( 45 | label="Show intermediate steps", value=Option.DEFAULT_SHOW_INTERMEDIATE_STEPS 46 | ) 47 | self.infotext_fields.append((show_intermediate_steps, Option.add_prefix("show_intermediate_steps"))) 48 | 49 | prompt_for_face = gr.Textbox( 50 | show_label=False, 51 | placeholder="Prompt for face", 52 | label="Prompt for face", 53 | lines=2, 54 | ) 55 | self.infotext_fields.append((prompt_for_face, Option.add_prefix("prompt_for_face"))) 56 | 57 | affected_areas = gr.CheckboxGroup( 58 | label="Affected areas", choices=["Face", "Hair", "Hat", "Neck"], value=Option.DEFAULT_AFFECTED_AREAS 59 | ) 60 | affected_areas_key = Option.add_prefix("affected_areas") 61 | self.infotext_fields.append((affected_areas, affected_areas_key)) 62 | ParamValueParser.add(affected_areas_key, list) 63 | 64 | mask_size = gr.Slider(label="Mask size", minimum=0, maximum=64, step=1, value=Option.DEFAULT_MASK_SIZE) 65 | self.infotext_fields.append((mask_size, Option.add_prefix("mask_size"))) 66 | 67 | mask_blur = gr.Slider(label="Mask blur ", minimum=0, maximum=64, step=1, value=Option.DEFAULT_MASK_BLUR) 68 | self.infotext_fields.append((mask_blur, Option.add_prefix("mask_blur"))) 69 | 70 | with gr.Accordion("Advanced Options", open=False): 71 | with gr.Accordion("(1) Face Detection", open=False): 72 | max_face_count = gr.Slider( 73 | minimum=1, 74 | maximum=20, 75 | step=1, 76 | value=Option.DEFAULT_MAX_FACE_COUNT, 77 | label="Maximum number of faces to detect", 78 | ) 79 | self.infotext_fields.append((max_face_count, Option.add_prefix("max_face_count"))) 80 | 81 | confidence = gr.Slider( 82 | minimum=0.7, 83 | maximum=1.0, 84 | step=0.01, 85 | value=Option.DEFAULT_CONFIDENCE, 86 | label="Face detection confidence", 87 | ) 88 | self.infotext_fields.append((confidence, Option.add_prefix("confidence"))) 89 | 90 | with gr.Accordion("(2) Crop and Resize the Faces", open=False): 91 | face_margin = gr.Slider( 92 | minimum=1.0, maximum=2.0, step=0.1, value=Option.DEFAULT_FACE_MARGIN, label="Face margin" 93 | ) 94 | self.infotext_fields.append((face_margin, Option.add_prefix("face_margin"))) 95 | 96 | hide_face_size_option = shared.opts.data.get("face_editor_auto_face_size_by_model", False) 97 | face_size = gr.Slider( 98 | label="Size of the face when recreating", 99 | visible=not hide_face_size_option, 100 | minimum=64, 101 | maximum=2048, 102 | step=16, 103 | value=Option.DEFAULT_FACE_SIZE, 104 | ) 105 | self.infotext_fields.append((face_size, Option.add_prefix("face_size"))) 106 | 107 | ignore_larger_faces = gr.Checkbox( 108 | label="Ignore faces larger than specified size", value=Option.DEFAULT_IGNORE_LARGER_FACES 109 | ) 110 | self.infotext_fields.append((ignore_larger_faces, Option.add_prefix("ignore_larger_faces"))) 111 | 112 | upscalers = [upscaler.name for upscaler in shared.sd_upscalers] 113 | if Option.DEFAULT_UPSCALER not in upscalers: 114 | upscalers.append(Option.DEFAULT_UPSCALER) 115 | upscaler = gr.Dropdown( 116 | label="Upscaler", 117 | choices=[upscaler.name for upscaler in shared.sd_upscalers], 118 | value=Option.DEFAULT_UPSCALER, 119 | ) 120 | self.infotext_fields.append((upscaler, Option.add_prefix("upscaler"))) 121 | 122 | with gr.Accordion("(3) Recreate the Faces", open=False): 123 | strength1 = gr.Slider( 124 | minimum=0.1, 125 | maximum=0.8, 126 | step=0.05, 127 | value=Option.DEFAULT_STRENGTH1, 128 | label="Denoising strength for face images", 129 | ) 130 | self.infotext_fields.append((strength1, Option.add_prefix("strength1"))) 131 | 132 | tilt_adjustment_threshold = gr.Slider( 133 | label="Tilt adjustment threshold", 134 | minimum=0, 135 | maximum=180, 136 | step=10, 137 | value=Option.DEFAULT_TILT_ADJUSTMENT_THRESHOLD, 138 | ) 139 | self.infotext_fields.append((tilt_adjustment_threshold, Option.add_prefix("tilt_adjustment_threshold"))) 140 | 141 | apply_scripts_to_faces = gr.Checkbox( 142 | label="Apply scripts to faces", visible=False, value=Option.DEFAULT_APPLY_SCRIPTS_TO_FACES 143 | ) 144 | self.infotext_fields.append((apply_scripts_to_faces, Option.add_prefix("apply_scripts_to_faces"))) 145 | 146 | with gr.Accordion("(4) Paste the Faces", open=False): 147 | apply_inside_mask_only = gr.Checkbox( 148 | label="Apply inside mask only ", value=Option.DEFAULT_APPLY_INSIDE_MASK_ONLY 149 | ) 150 | self.infotext_fields.append((apply_inside_mask_only, Option.add_prefix("apply_inside_mask_only"))) 151 | 152 | with gr.Accordion("(5) Blend the entire image", open=False): 153 | strength2 = gr.Slider( 154 | minimum=0.0, 155 | maximum=1.0, 156 | step=0.05, 157 | value=Option.DEFAULT_STRENGTH2, 158 | label="Denoising strength for the entire image ", 159 | ) 160 | self.infotext_fields.append((strength2, Option.add_prefix("strength2"))) 161 | 162 | with gr.Accordion("Workflow Editor", open=False): 163 | workflow = workflow_editor.build(workflow_selector) 164 | self.infotext_fields.append((workflow, Option.add_prefix("workflow"))) 165 | 166 | return [ 167 | face_margin, 168 | confidence, 169 | strength1, 170 | strength2, 171 | max_face_count, 172 | mask_size, 173 | mask_blur, 174 | prompt_for_face, 175 | apply_inside_mask_only, 176 | save_original_image, 177 | show_intermediate_steps, 178 | apply_scripts_to_faces, 179 | face_size, 180 | use_minimal_area, 181 | ignore_larger_faces, 182 | affected_areas, 183 | show_original_image, 184 | workflow, 185 | upscaler, 186 | tilt_adjustment_threshold, 187 | ] 188 | 189 | 190 | def on_ui_settings(): 191 | section = ("face_editor", "Face Editor") 192 | 193 | shared.opts.add_option( 194 | "face_editor_search_subdirectories", 195 | shared.OptionInfo(False, "Search workflows in subdirectories", gr.Checkbox, section=section), 196 | ) 197 | 198 | additional_components = [] 199 | with os.scandir(inferencers_dir) as entries: 200 | for entry in entries: 201 | if entry.is_dir() and entry.name[0].isalnum(): 202 | additional_components.append(entry.name) 203 | 204 | shared.opts.add_option( 205 | "face_editor_additional_components", 206 | shared.OptionInfo( 207 | [], "Additional components", gr.CheckboxGroup, {"choices": additional_components}, section=section 208 | ), 209 | ) 210 | 211 | shared.opts.add_option( 212 | "face_editor_save_original_on_detection_fail", 213 | shared.OptionInfo(True, "Save original image if face detection fails", gr.Checkbox, section=section), 214 | ) 215 | 216 | shared.opts.add_option( 217 | "face_editor_correct_tilt", 218 | shared.OptionInfo(False, "Adjust tilt for detected faces", gr.Checkbox, section=section), 219 | ) 220 | 221 | shared.opts.add_option( 222 | "face_editor_auto_face_size_by_model", 223 | shared.OptionInfo(False, "Auto face size adjustment by model", gr.Checkbox, section=section), 224 | ) 225 | 226 | shared.opts.add_option( 227 | "face_editor_script_index", 228 | shared.OptionInfo( 229 | 99, 230 | "The position in postprocess at which this script will be executed; " 231 | "0 means it will be executed before any scripts, 99 means it will probably be executed last.", 232 | gr.Slider, 233 | {"minimum": 0, "maximum": 99, "step": 1}, 234 | section=section, 235 | ), 236 | ) 237 | 238 | 239 | script_callbacks.on_ui_settings(on_ui_settings) 240 | -------------------------------------------------------------------------------- /face_editor/ui/workflow_editor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Any, Dict, List 4 | 5 | import gradio as gr 6 | from face_editor.io.util import workflows_dir 7 | from face_editor.use_cases.workflow_manager import WorkflowManager 8 | from modules import shared 9 | from pydantic import ValidationError 10 | 11 | 12 | def load_workflow(file: str) -> str: 13 | if file is not None: 14 | filepath = os.path.join(workflows_dir, file + ".json") 15 | if os.path.isfile(filepath): 16 | return open(filepath).read() 17 | return "" 18 | 19 | 20 | def get_filename(file: str) -> str: 21 | if file == "default": 22 | return "" 23 | return file 24 | 25 | 26 | def sync_selection(file: str) -> str: 27 | return file 28 | 29 | 30 | def save_workflow(name: str, workflow: str) -> str: 31 | if name is None or len(name) == 0: 32 | return "" 33 | 34 | with open(os.path.join(workflows_dir, name + ".json"), "w") as file: 35 | file.write(workflow) 36 | return f"Saved to {name}.json" 37 | 38 | 39 | def get_files() -> List[str]: 40 | search_subdirectories = shared.opts.data.get("face_editor_search_subdirectories", False) 41 | files = [] 42 | for root, _, filenames in os.walk(workflows_dir): 43 | if not search_subdirectories and not os.path.samefile(root, workflows_dir): 44 | continue 45 | for filename in filenames: 46 | if filename.endswith(".json"): 47 | relative_path, _ = os.path.splitext(os.path.relpath(os.path.join(root, filename), workflows_dir)) 48 | files.append(relative_path) 49 | return files 50 | 51 | 52 | def refresh_files(workflow: str, file: str) -> dict: 53 | files = get_files() 54 | kwargs: Dict[str, Any] = {"choices": files} 55 | if workflow: 56 | for file in files: 57 | if load_workflow(file) == workflow: 58 | kwargs["value"] = file 59 | break 60 | 61 | return gr.update(**kwargs) 62 | 63 | 64 | def validate_workflow(workflow: str) -> str: 65 | try: 66 | json.loads(workflow) 67 | WorkflowManager.get(workflow) 68 | return "No errors found in the Workflow." 69 | except json.JSONDecodeError as e: 70 | return f"Error in JSON: {str(e)}" 71 | except ValidationError as e: 72 | errors = e.errors() 73 | if len(errors) == 0: 74 | return f"{str(e)}" 75 | err = errors[-1] 76 | return f"{' -> '.join(str(er) for er in err['loc'])} {err['msg']}\n--\n{str(e)}" 77 | except Exception as e: 78 | return f"{str(e)}" 79 | 80 | 81 | def build(workflow_selector: gr.Dropdown): 82 | with gr.Blocks(title="Workflow"): 83 | with gr.Row(): 84 | filename_dropdown = gr.Dropdown( 85 | choices=get_files(), 86 | label="Choose a Workflow", 87 | value="default", 88 | scale=2, 89 | min_width=400, 90 | show_label=False, 91 | ) 92 | refresh_button = gr.Button(value="🔄", scale=0, size="sm", elem_classes="tool") 93 | with gr.Row(): 94 | filename_input = gr.Textbox(scale=2, show_label=False, placeholder="Save as") 95 | save_button = gr.Button(value="💾", scale=0, size="sm", elem_classes="tool") 96 | 97 | workflow_editor = gr.Code(language="json", label="Workflow", value=load_workflow("default")) 98 | with gr.Row(): 99 | json_status = gr.Textbox(scale=2, show_label=False) 100 | validate_button = gr.Button(value="✅", scale=0, size="sm", elem_classes="tool") 101 | 102 | filename_dropdown.input(load_workflow, inputs=[filename_dropdown], outputs=[workflow_editor]) 103 | filename_dropdown.change(get_filename, inputs=[filename_dropdown], outputs=[filename_input]) 104 | filename_dropdown.input(sync_selection, inputs=[filename_dropdown], outputs=[workflow_selector]) 105 | 106 | workflow_selector.input(load_workflow, inputs=[workflow_selector], outputs=[workflow_editor]) 107 | workflow_selector.input(get_filename, inputs=[workflow_selector], outputs=[filename_input]) 108 | workflow_selector.input(sync_selection, inputs=[workflow_selector], outputs=[filename_dropdown]) 109 | 110 | save_button.click(validate_workflow, inputs=[workflow_editor], outputs=[json_status]) 111 | save_button.click(save_workflow, inputs=[filename_input, workflow_editor]) 112 | 113 | refresh_button.click(refresh_files, inputs=[workflow_editor, filename_dropdown], outputs=[filename_dropdown]) 114 | refresh_button.click(refresh_files, inputs=[workflow_editor, filename_dropdown], outputs=[workflow_selector]) 115 | 116 | validate_button.click(validate_workflow, inputs=[workflow_editor], outputs=[json_status]) 117 | 118 | return workflow_editor 119 | -------------------------------------------------------------------------------- /face_editor/use_cases/face_detector.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from face_editor.entities.rect import Rect 5 | from PIL import Image 6 | 7 | 8 | class FaceDetector(ABC): 9 | @abstractmethod 10 | def name(self) -> str: 11 | pass 12 | 13 | @abstractmethod 14 | def detect_faces(self, image: Image, **kwargs) -> List[Rect]: 15 | pass 16 | -------------------------------------------------------------------------------- /face_editor/use_cases/face_processor.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from face_editor.entities.face import Face 4 | from modules.processing import StableDiffusionProcessingImg2Img 5 | from PIL import Image 6 | 7 | 8 | class FaceProcessor(ABC): 9 | @abstractmethod 10 | def name(self) -> str: 11 | pass 12 | 13 | @abstractmethod 14 | def process(self, face: Face, p: StableDiffusionProcessingImg2Img, **kwargs) -> Image: 15 | pass 16 | -------------------------------------------------------------------------------- /face_editor/use_cases/image_processing_util.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from PIL import Image 4 | 5 | 6 | def rotate_image(image: Image, angle: float) -> Image: 7 | if angle == 0: 8 | return image 9 | return Image.fromarray(rotate_array(np.array(image), angle)) 10 | 11 | 12 | def rotate_array(image: np.ndarray, angle: float) -> np.ndarray: 13 | if angle == 0: 14 | return image 15 | 16 | h, w = image.shape[:2] 17 | center = (w // 2, h // 2) 18 | 19 | M = cv2.getRotationMatrix2D(center, angle, 1.0) 20 | return cv2.warpAffine(image, M, (w, h)) 21 | -------------------------------------------------------------------------------- /face_editor/use_cases/image_processor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from operator import attrgetter 4 | from typing import Any, List, Tuple 5 | 6 | import cv2 7 | import modules.images as images 8 | import modules.scripts as scripts 9 | import modules.shared as shared 10 | import numpy as np 11 | from face_editor.entities.definitions import Rule 12 | from face_editor.entities.face import Face 13 | from face_editor.entities.option import Option 14 | from face_editor.entities.rect import Rect 15 | from face_editor.use_cases.mask_generator import MaskGenerator 16 | from face_editor.use_cases.registry import face_processors 17 | from face_editor.use_cases.workflow_manager import WorkflowManager 18 | from modules.processing import ( 19 | Processed, 20 | StableDiffusionProcessing, 21 | StableDiffusionProcessingImg2Img, 22 | create_infotext, 23 | process_images, 24 | ) 25 | from PIL import Image 26 | 27 | os.makedirs(os.path.join(tempfile.gettempdir(), "gradio"), exist_ok=True) 28 | 29 | 30 | class ImageProcessor: 31 | def __init__(self, workflow: WorkflowManager) -> None: 32 | self.workflow = workflow 33 | 34 | def proc_images(self, o: StableDiffusionProcessing, res: Processed, option: Option): 35 | edited_images, all_seeds, all_prompts, all_negative_prompts, infotexts = [], [], [], [], [] 36 | seed_index = 0 37 | subseed_index = 0 38 | 39 | self.__extend_infos(res.all_prompts, len(res.images)) 40 | self.__extend_infos(res.all_negative_prompts, len(res.images)) 41 | self.__extend_infos(res.all_seeds, len(res.images)) 42 | self.__extend_infos(res.infotexts, len(res.images)) 43 | 44 | for i, image in enumerate(res.images): 45 | if i < res.index_of_first_image: 46 | continue 47 | 48 | p = StableDiffusionProcessingImg2Img(init_images=[image]) 49 | self.__init_processing(p, o, image) 50 | 51 | if seed_index < len(res.all_seeds): 52 | p.seed = res.all_seeds[seed_index] 53 | seed_index += 1 54 | if subseed_index < len(res.all_subseeds): 55 | p.subseed = res.all_subseeds[subseed_index] 56 | subseed_index += 1 57 | 58 | if type(p.prompt) == list: 59 | p.prompt = p.prompt[i] 60 | 61 | proc = self.proc_image(p, option, image, res.infotexts[i], (res.width, res.height)) 62 | edited_images.extend(proc.images) 63 | all_seeds.extend(proc.all_seeds) 64 | all_prompts.extend(proc.all_prompts) 65 | all_negative_prompts.extend(proc.all_negative_prompts) 66 | infotexts.extend(proc.infotexts) 67 | 68 | if res.index_of_first_image == 1: 69 | edited_images.insert(0, images.image_grid(edited_images)) 70 | infotexts.insert(0, infotexts[0]) 71 | 72 | if option.show_original_image: 73 | res.images.extend(edited_images) 74 | res.infotexts.extend(infotexts) 75 | res.all_seeds.extend(all_seeds) 76 | res.all_prompts.extend(all_prompts) 77 | res.all_negative_prompts.extend(all_negative_prompts) 78 | else: 79 | res.images = edited_images 80 | res.infotexts = infotexts 81 | res.all_seeds = all_seeds 82 | res.all_prompts = all_prompts 83 | res.all_negative_prompts = all_negative_prompts 84 | 85 | return res 86 | 87 | def __init_processing(self, p: StableDiffusionProcessingImg2Img, o: StableDiffusionProcessing, image): 88 | sample = p.sample 89 | p.__dict__.update(o.__dict__) 90 | p.sampler = None 91 | p.c = None 92 | p.uc = None 93 | p.cached_c = [None, None] 94 | p.cached_uc = [None, None] 95 | p.init_images = [image] 96 | p.width, p.height = image.size 97 | p.sample = sample 98 | 99 | def proc_image( 100 | self, 101 | p: StableDiffusionProcessingImg2Img, 102 | option: Option, 103 | pre_proc_image: Image = None, 104 | pre_proc_infotext: Any = None, 105 | original_size: Tuple[int, int] = None, 106 | ) -> Processed: 107 | if shared.opts.data.get("face_editor_auto_face_size_by_model", False): 108 | option.face_size = 1024 if getattr(shared.sd_model, "is_sdxl", False) else 512 109 | params = option.to_dict() 110 | 111 | if hasattr(p.init_images[0], "mode") and p.init_images[0].mode != "RGB": 112 | p.init_images[0] = p.init_images[0].convert("RGB") 113 | 114 | entire_image = np.array(p.init_images[0]) 115 | faces = self.__crop_face(p.init_images[0], option) 116 | faces = faces[: option.max_face_count] 117 | faces = sorted(faces, key=attrgetter("center")) 118 | entire_mask_image = np.zeros_like(entire_image) 119 | 120 | entire_width = (p.width // 8) * 8 121 | entire_height = (p.height // 8) * 8 122 | entire_prompt = p.prompt 123 | entire_all_prompts = p.all_prompts 124 | original_denoising_strength = p.denoising_strength 125 | p.batch_size = 1 126 | p.n_iter = 1 127 | 128 | if shared.state.job_count == -1: 129 | shared.state.job_count = len(faces) + 1 130 | 131 | output_images = [] 132 | if option.show_intermediate_steps: 133 | output_images.append(self.__show_detected_faces(np.copy(entire_image), faces, p)) 134 | 135 | print(f"number of faces: {len(faces)}. ") 136 | if ( 137 | len(faces) == 0 138 | and pre_proc_image is not None 139 | and ( 140 | option.save_original_image 141 | or not shared.opts.data.get("face_editor_save_original_on_detection_fail", False) 142 | ) 143 | ): 144 | return Processed( 145 | p, 146 | images_list=[pre_proc_image], 147 | all_prompts=[p.prompt], 148 | all_seeds=[p.seed], 149 | infotexts=[pre_proc_infotext], 150 | ) 151 | 152 | wildcards_script = self.__get_wildcards_script(p) 153 | face_prompts = self.__get_face_prompts(len(faces), option.prompt_for_face, entire_prompt) 154 | 155 | if not option.apply_scripts_to_faces: 156 | p.scripts = None 157 | 158 | for i, face in enumerate(faces): 159 | if shared.state.interrupted: 160 | break 161 | 162 | p.prompt = face_prompts[i] 163 | if wildcards_script is not None: 164 | p.prompt = self.__apply_wildcards(wildcards_script, p.prompt, i) 165 | 166 | rule = self.workflow.select_rule(faces, i, entire_width, entire_height) 167 | 168 | if rule is None or len(rule.then) == 0: 169 | continue 170 | jobs = rule.then 171 | 172 | if option.show_intermediate_steps: 173 | original_face = np.array(face.image.copy()) 174 | 175 | proc_image = self.workflow.process(jobs, face, p, option) 176 | if proc_image is None: 177 | continue 178 | 179 | if proc_image.mode != "RGB": 180 | proc_image = proc_image.convert("RGB") 181 | 182 | face_image = np.array(proc_image) 183 | mask_image = self.workflow.generate_mask(jobs, face_image, face, option) 184 | 185 | if option.show_intermediate_steps: 186 | feature = self.__get_feature(p.prompt, entire_prompt) 187 | self.__add_debug_image( 188 | output_images, face, original_face, face_image, mask_image, rule, option, feature 189 | ) 190 | 191 | face_image = cv2.resize(face_image, dsize=(face.width, face.height), interpolation=cv2.INTER_AREA) 192 | mask_image = cv2.resize(mask_image, dsize=(face.width, face.height), interpolation=cv2.INTER_AREA) 193 | 194 | if option.use_minimal_area: 195 | l, t, r, b = face.face_area.to_tuple() 196 | face_image = face_image[t - face.top : b - face.top, l - face.left : r - face.left] 197 | mask_image = mask_image[t - face.top : b - face.top, l - face.left : r - face.left] 198 | face.top = t 199 | face.left = l 200 | face.bottom = b 201 | face.right = r 202 | 203 | if option.apply_inside_mask_only: 204 | face_background = entire_image[ 205 | face.top : face.bottom, 206 | face.left : face.right, 207 | ] 208 | face_fg = (face_image * (mask_image / 255.0)).astype("uint8") 209 | face_bg = (face_background * (1 - (mask_image / 255.0))).astype("uint8") 210 | face_image = face_fg + face_bg 211 | 212 | entire_image[ 213 | face.top : face.bottom, 214 | face.left : face.right, 215 | ] = face_image 216 | entire_mask_image[ 217 | face.top : face.bottom, 218 | face.left : face.right, 219 | ] = mask_image 220 | 221 | p.prompt = entire_prompt 222 | p.all_prompts = entire_all_prompts 223 | p.width = entire_width 224 | p.height = entire_height 225 | simple_composite_image = Image.fromarray(entire_image) 226 | p.init_images = [simple_composite_image] 227 | p.mask_blur = option.mask_blur 228 | p.inpainting_mask_invert = 1 229 | p.inpainting_fill = 1 230 | p.image_mask = Image.fromarray(entire_mask_image) 231 | p.do_not_save_samples = True 232 | 233 | p.extra_generation_params.update(params) 234 | 235 | if option.strength2 > 0: 236 | p.denoising_strength = option.strength2 237 | if p.scripts is None: 238 | p.scripts = scripts.ScriptRunner() 239 | proc = process_images(p) 240 | p.init_images = proc.images 241 | 242 | if original_size is not None: 243 | p.width, p.height = original_size 244 | p.denoising_strength = original_denoising_strength 245 | proc = self.__save_images(p, pre_proc_infotext if len(faces) == 0 else None) 246 | 247 | if option.show_intermediate_steps: 248 | output_images.append(simple_composite_image) 249 | if option.strength2 > 0: 250 | output_images.append(Image.fromarray(self.__to_masked_image(entire_mask_image, entire_image))) 251 | output_images.append(proc.images[0]) 252 | proc.images = output_images 253 | 254 | self.__extend_infos(proc.all_prompts, len(proc.images)) 255 | self.__extend_infos(proc.all_seeds, len(proc.images)) 256 | self.__extend_infos(proc.infotexts, len(proc.images)) 257 | 258 | return proc 259 | 260 | def __add_debug_image( 261 | self, 262 | output_images: list, 263 | face: Face, 264 | original_face: np.ndarray, 265 | face_image: np.ndarray, 266 | mask_image: np.ndarray, 267 | rule: Rule, 268 | option: Option, 269 | feature: str, 270 | ): 271 | h, w = original_face.shape[:2] 272 | debug_image = np.zeros_like(original_face) 273 | 274 | tag = f"{face.face_area.tag} ({face.face_area.width}x{face.face_area.height})" 275 | attributes = str(face.face_area.attributes) if face.face_area.attributes else "" 276 | if not attributes: 277 | attributes = option.upscaler if option.upscaler != Option.DEFAULT_UPSCALER else "" 278 | 279 | def resize(img: np.ndarray): 280 | return cv2.resize(img, (w // 2, h // 2)) 281 | 282 | debug_image[0 : h // 2, 0 : w // 2] = resize( 283 | self.__add_comment(self.__add_comment(original_face, attributes), tag, True) 284 | ) 285 | 286 | criteria = rule.when.criteria if rule.when is not None and rule.when.criteria is not None else "" 287 | debug_image[0 : h // 2, w // 2 :] = resize( 288 | self.__add_comment(self.__add_comment(face_image, feature), criteria, True) 289 | ) 290 | 291 | coverage = MaskGenerator.calculate_mask_coverage(mask_image) * 100 292 | mask_info = f"size:{option.mask_size}, blur:{option.mask_blur}, cov:{coverage:.0f}%" 293 | debug_image[h // 2 :, 0 : w // 2] = resize( 294 | self.__add_comment(self.__to_masked_image(mask_image, face_image), mask_info) 295 | ) 296 | 297 | face_fg = (face_image * (mask_image / 255.0)).astype("uint8") 298 | face_bg = (original_face * (1 - (mask_image / 255.0))).astype("uint8") 299 | debug_image[h // 2 :, w // 2 :] = resize(face_fg + face_bg) 300 | 301 | output_images.append(Image.fromarray(debug_image)) 302 | 303 | def __show_detected_faces(self, entire_image: np.ndarray, faces: List[Face], p: StableDiffusionProcessingImg2Img): 304 | processor = face_processors.get("debug") 305 | for face in faces: 306 | face_image = np.array(processor.process(face, p)) 307 | face_image = cv2.resize(face_image, dsize=(face.width, face.height), interpolation=cv2.INTER_AREA) 308 | entire_image[ 309 | face.top : face.bottom, 310 | face.left : face.right, 311 | ] = face_image 312 | return Image.fromarray(self.__add_comment(entire_image, f"{len(faces)}")) 313 | 314 | def __get_wildcards_script(self, p: StableDiffusionProcessingImg2Img): 315 | if p.scripts is None: 316 | return None 317 | 318 | for script in p.scripts.alwayson_scripts: 319 | if script.filename.endswith("stable-diffusion-webui-wildcards/scripts/wildcards.py"): 320 | return script 321 | 322 | return None 323 | 324 | def __get_feature(self, prompt: str, entire_prompt: str) -> str: 325 | if prompt == "" or prompt == entire_prompt: 326 | return "" 327 | return prompt.replace(entire_prompt, "") 328 | 329 | def __add_comment(self, image: np.ndarray, comment: str, top: bool = False) -> np.ndarray: 330 | image = np.copy(image) 331 | h, _, _ = image.shape 332 | pos = (10, 48) if top else (10, h - 16) 333 | cv2.putText( 334 | image, 335 | text=comment, 336 | org=pos, 337 | fontFace=cv2.FONT_HERSHEY_SIMPLEX, 338 | fontScale=1.2, 339 | color=(0, 0, 0), 340 | thickness=10, 341 | ) 342 | cv2.putText( 343 | image, 344 | text=comment, 345 | org=pos, 346 | fontFace=cv2.FONT_HERSHEY_SIMPLEX, 347 | fontScale=1.2, 348 | color=(255, 255, 255), 349 | thickness=2, 350 | ) 351 | return image 352 | 353 | def __apply_wildcards(self, wildcards_script: scripts.Script, prompt: str, seed: int) -> str: 354 | if "__" in prompt: 355 | wp = StableDiffusionProcessing() 356 | wp.all_prompts = [prompt] 357 | wp.all_seeds = [0 if shared.opts.wildcards_same_seed else seed] 358 | wildcards_script.process(wp) 359 | return wp.all_prompts[0] 360 | return prompt 361 | 362 | def __get_face_prompts(self, length: int, prompt_for_face: str, entire_prompt: str) -> List[str]: 363 | if len(prompt_for_face) == 0: 364 | return [entire_prompt] * length 365 | prompts = [] 366 | p = prompt_for_face.split("||") 367 | for i in range(length): 368 | if i >= len(p): 369 | i = 0 370 | prompts.append(self.__edit_face_prompt(p[i], p[0], entire_prompt)) 371 | return prompts 372 | 373 | def __edit_face_prompt(self, prompt: str, default_prompt: str, entire_prompt: str) -> str: 374 | if len(prompt) == 0: 375 | return default_prompt 376 | 377 | return prompt.strip().replace("@@", entire_prompt) 378 | 379 | def __save_images(self, p: StableDiffusionProcessingImg2Img, pre_proc_infotext: Any) -> Processed: 380 | if pre_proc_infotext is not None: 381 | infotext = pre_proc_infotext 382 | else: 383 | if p.all_prompts is None or len(p.all_prompts) == 0: 384 | p.all_prompts = [p.prompt] 385 | if p.all_negative_prompts is None or len(p.all_negative_prompts) == 0: 386 | p.all_negative_prompts = [p.negative_prompt] 387 | if p.all_seeds is None or len(p.all_seeds) == 0: 388 | p.all_seeds = [p.seed] 389 | if p.all_subseeds is None or len(p.all_subseeds) == 0: 390 | p.all_subseeds = [p.subseed] 391 | infotext = create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, {}, 0, 0) 392 | images.save_image( 393 | p.init_images[0], p.outpath_samples, "", p.seed, p.prompt, shared.opts.samples_format, info=infotext, p=p 394 | ) 395 | return Processed( 396 | p, 397 | images_list=p.init_images, 398 | seed=p.seed, 399 | info=infotext, 400 | subseed=p.subseed, 401 | index_of_first_image=0, 402 | infotexts=[infotext], 403 | ) 404 | 405 | def __extend_infos(self, infos: list, image_count: int) -> None: 406 | infos.extend([infos[0]] * (image_count - len(infos))) 407 | 408 | def __to_masked_image(self, mask_image: np.ndarray, image: np.ndarray) -> np.ndarray: 409 | gray_mask = mask_image / 255.0 410 | return (image * gray_mask).astype("uint8") 411 | 412 | def __crop_face(self, image: Image, option: Option) -> List[Face]: 413 | face_areas = self.workflow.detect_faces(image, option) 414 | return self.__crop( 415 | image, face_areas, option.face_margin, option.face_size, option.ignore_larger_faces, option.upscaler 416 | ) 417 | 418 | def __crop( 419 | self, 420 | image: Image, 421 | face_areas: List[Rect], 422 | face_margin: float, 423 | face_size: int, 424 | ignore_larger_faces: bool, 425 | upscaler: str, 426 | ) -> List[Face]: 427 | image = np.array(image, dtype=np.uint8) 428 | 429 | areas: List[Face] = [] 430 | for face_area in face_areas: 431 | face = Face(image, face_area, face_margin, face_size, upscaler) 432 | if ignore_larger_faces and face.width > face_size: 433 | continue 434 | areas.append(face) 435 | 436 | return sorted(areas, key=attrgetter("height"), reverse=True) 437 | -------------------------------------------------------------------------------- /face_editor/use_cases/installer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | import launch 5 | 6 | 7 | class Installer(ABC): 8 | @abstractmethod 9 | def name(self) -> str: 10 | pass 11 | 12 | def requirements(self) -> List[str]: 13 | return [] 14 | 15 | def install(self) -> None: 16 | requirements_to_install = [req for req in self.requirements() if not launch.is_installed(req)] 17 | if len(requirements_to_install) > 0: 18 | launch.run_pip( 19 | f"install {' '.join(requirements_to_install)}", 20 | f"requirements for {self.name()}: {requirements_to_install}", 21 | ) 22 | -------------------------------------------------------------------------------- /face_editor/use_cases/mask_generator.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Tuple 3 | 4 | import cv2 5 | import numpy as np 6 | 7 | 8 | class MaskGenerator(ABC): 9 | @abstractmethod 10 | def name(self) -> str: 11 | pass 12 | 13 | @abstractmethod 14 | def generate_mask( 15 | self, 16 | face_image: np.ndarray, 17 | face_area_on_image: Tuple[int, int, int, int], 18 | **kwargs, 19 | ) -> np.ndarray: 20 | pass 21 | 22 | @staticmethod 23 | def mask_non_face_areas(image: np.ndarray, face_area_on_image: Tuple[int, int, int, int]) -> np.ndarray: 24 | left, top, right, bottom = face_area_on_image 25 | image = image.copy() 26 | image[:top, :] = 0 27 | image[bottom:, :] = 0 28 | image[:, :left] = 0 29 | image[:, right:] = 0 30 | return image 31 | 32 | @staticmethod 33 | def calculate_mask_coverage(mask: np.ndarray): 34 | gray_mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) 35 | non_black_pixels = np.count_nonzero(gray_mask) 36 | total_pixels = gray_mask.size 37 | return non_black_pixels / total_pixels 38 | -------------------------------------------------------------------------------- /face_editor/use_cases/query_matcher.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Dict 3 | 4 | from lark import Lark, Tree 5 | 6 | query_grammar = """ 7 | ?start: expression 8 | 9 | ?expression: and_expression ("|" and_expression)* 10 | ?and_expression: sub_expression ("&" sub_expression)* 11 | ?sub_expression: condition | "(" expression ")" 12 | 13 | condition: field OPERATOR value 14 | 15 | field: CNAME 16 | value: CNAME | NUMBER 17 | 18 | %import common.CNAME 19 | %import common.NUMBER 20 | 21 | OPERATOR: "=" | "<" | ">" | "<=" | ">=" | "!=" | "~=" | "*=" | "=*" | "~*" | "*~" 22 | """ 23 | 24 | query_parser = Lark(query_grammar, start="start") 25 | 26 | 27 | def starts_with(a, b): 28 | return a.startswith(b) 29 | 30 | 31 | def ends_with(a, b): 32 | return a.endswith(b) 33 | 34 | 35 | def contains(a, b): 36 | return b in a 37 | 38 | 39 | def not_contains(a, b): 40 | return b not in a 41 | 42 | 43 | operator_mapping = { 44 | ">": operator.gt, 45 | "<": operator.lt, 46 | ">=": operator.ge, 47 | "<=": operator.le, 48 | "=": operator.eq, 49 | "!=": operator.ne, 50 | "~=": contains, 51 | "*=": starts_with, 52 | "=*": ends_with, 53 | "~*": not_contains, 54 | } 55 | 56 | 57 | def split_condition(condition_parts): 58 | field_name = condition_parts[0].children[0].value 59 | operator = condition_parts[1].value 60 | value = condition_parts[2].children[0].value 61 | return field_name, operator, value 62 | 63 | 64 | def evaluate_condition(condition, attributes): 65 | field_name, operator, value = split_condition(condition.children) 66 | if field_name in attributes: 67 | attr_value = attributes[field_name] 68 | try: 69 | attr_value = float(attr_value) 70 | value = float(value) 71 | except ValueError: 72 | attr_value = str(attr_value).lower() 73 | value = str(value).lower() 74 | return operator_mapping[operator](attr_value, value) 75 | else: 76 | return False 77 | 78 | 79 | def evaluate_and_expression(and_expression, attributes): 80 | return all(evaluate_expression(child, attributes) for child in and_expression.children) 81 | 82 | 83 | def evaluate_expression(expression, attributes): 84 | if isinstance(expression, Tree) and expression.data == "condition": 85 | return evaluate_condition(expression, attributes) 86 | elif isinstance(expression, Tree) and expression.data == "and_expression": 87 | return evaluate_and_expression(expression, attributes) 88 | else: 89 | return any(evaluate_expression(child, attributes) for child in expression.children) 90 | 91 | 92 | def evaluate(query: str, attributes: Dict[str, str]) -> bool: 93 | tree = query_parser.parse(query) 94 | return evaluate_expression(tree, attributes) 95 | 96 | 97 | def validate(query: str): 98 | return evaluate(query, {}) 99 | -------------------------------------------------------------------------------- /face_editor/use_cases/registry.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Dict 3 | 4 | from face_editor.io.util import load_classes_from_directory 5 | from face_editor.use_cases.face_detector import FaceDetector 6 | from face_editor.use_cases.face_processor import FaceProcessor 7 | from face_editor.use_cases.mask_generator import MaskGenerator 8 | 9 | 10 | def create(all_classes, type: str) -> Dict: 11 | d = {} 12 | for cls in all_classes: 13 | try: 14 | c = cls() 15 | d[c.name().lower()] = c 16 | except Exception as e: 17 | print(traceback.format_exc()) 18 | print(f"Face Editor: {cls}, Error: {e}") 19 | return d 20 | 21 | 22 | def load_face_detector() -> Dict[str, FaceDetector]: 23 | return create(load_classes_from_directory(FaceDetector), "FaceDetector") 24 | 25 | 26 | def load_face_processor() -> Dict[str, FaceProcessor]: 27 | return create(load_classes_from_directory(FaceProcessor), "FaceProcessor") 28 | 29 | 30 | def load_mask_generator() -> Dict[str, MaskGenerator]: 31 | return create(load_classes_from_directory(MaskGenerator), "MaskGenerator") 32 | 33 | 34 | face_detectors = load_face_detector() 35 | face_processors = load_face_processor() 36 | mask_generators = load_mask_generator() 37 | face_detector_names = list(face_detectors.keys()) 38 | face_processor_names = list(face_processors.keys()) 39 | mask_generator_names = list(mask_generators.keys()) 40 | -------------------------------------------------------------------------------- /face_editor/use_cases/workflow_manager.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import cv2 4 | import numpy as np 5 | from face_editor.entities.definitions import Condition, Job, Rule, Workflow 6 | from face_editor.entities.face import Face 7 | from face_editor.entities.option import Option 8 | from face_editor.entities.rect import Rect 9 | from face_editor.use_cases import query_matcher, registry 10 | from face_editor.use_cases.image_processing_util import rotate_array, rotate_image 11 | from modules import shared 12 | from modules.processing import StableDiffusionProcessingImg2Img 13 | from PIL import Image 14 | 15 | 16 | class WorkflowManager: 17 | @classmethod 18 | def get(cls, workflow: str) -> "WorkflowManager": 19 | manager = cls(Workflow.parse_raw(workflow)) 20 | 21 | for face_detector in manager.workflow.face_detector: 22 | if face_detector.name not in registry.face_detector_names: 23 | raise KeyError(f"face_detector `{face_detector.name}` does not exist") 24 | 25 | for rule in manager.workflow.rules: 26 | for job in rule.then: 27 | if job.face_processor.name not in registry.face_processor_names: 28 | raise KeyError(f"face_processor `{job.face_processor.name}` does not exist") 29 | if job.mask_generator.name not in registry.mask_generator_names: 30 | raise KeyError(f"mask_generator `{job.mask_generator.name}` does not exist") 31 | if rule.when is not None and rule.when.tag is not None and "?" in rule.when.tag: 32 | _, query = cls.__parse_tag(rule.when.tag) 33 | if len(query) > 0: 34 | query_matcher.validate(query) 35 | 36 | return manager 37 | 38 | def __init__(self, workflow: Workflow) -> None: 39 | self.workflow = workflow 40 | self.correct_tilt = shared.opts.data.get("face_editor_correct_tilt", False) 41 | 42 | def detect_faces(self, image: Image, option: Option) -> List[Rect]: 43 | results = [] 44 | 45 | for fd in self.workflow.face_detector: 46 | face_detector = registry.face_detectors[fd.name] 47 | params = fd.params.copy() 48 | params["confidence"] = option.confidence 49 | results.extend(face_detector.detect_faces(image, **params)) 50 | 51 | return results 52 | 53 | def select_rule(self, faces: List[Face], index: int, width: int, height: int) -> Rule: 54 | face = faces[index] 55 | if face.face_area is None: 56 | return None 57 | 58 | for rule in self.workflow.rules: 59 | if rule.when is None: 60 | return rule 61 | 62 | if self.__is_tag_match(rule.when, face): 63 | tag_matched_faces = [f for f in faces if self.__is_tag_match(rule.when, f)] 64 | if self.__is_criteria_match(rule.when, tag_matched_faces, face, width, height): 65 | return rule 66 | 67 | return None 68 | 69 | @classmethod 70 | def __parse_tag(cls, tag: str) -> Tuple[str, str]: 71 | parts = tag.split("?", 1) 72 | return parts[0], parts[1] if len(parts) > 1 else "" 73 | 74 | def __is_tag_match(self, condition: Condition, face: Face) -> bool: 75 | if condition.tag is None or len(condition.tag) == 0: 76 | return True 77 | 78 | condition_tag = condition.tag.lower() 79 | if condition_tag == "any": 80 | return True 81 | 82 | tag, query = self.__parse_tag(condition_tag) 83 | face_tag = face.face_area.tag.lower() if face.face_area.tag is not None else "" 84 | if tag != face_tag: 85 | return False 86 | if len(query) == 0: 87 | return True 88 | return query_matcher.evaluate(query, face.face_area.attributes) 89 | 90 | def __is_criteria_match(self, condition: Condition, faces: List[Face], face: Face, width: int, height: int) -> bool: 91 | if not condition.has_criteria(): 92 | return True 93 | 94 | criteria = condition.get_criteria() 95 | indices = condition.get_indices() 96 | 97 | if criteria == "all": 98 | return True 99 | 100 | if criteria in {"left", "leftmost"}: 101 | return self.__is_left(indices, faces, face) 102 | if criteria in {"center", "center_horizontal", "middle_horizontal"}: 103 | return self.__is_center(indices, faces, face, width) 104 | if criteria in {"right", "rightmost"}: 105 | return self.__is_right(indices, faces, face) 106 | if criteria in {"top", "upper", "upmost"}: 107 | return self.__is_top(indices, faces, face) 108 | if criteria in {"middle", "center_vertical", "middle_vertical"}: 109 | return self.__is_middle(indices, faces, face, height) 110 | if criteria in {"bottom", "lower", "downmost"}: 111 | return self.__is_bottom(indices, faces, face) 112 | if criteria in {"small", "tiny", "smaller"}: 113 | return self.__is_small(indices, faces, face) 114 | if criteria in {"large", "big", "bigger"}: 115 | return self.__is_large(indices, faces, face) 116 | return False 117 | 118 | def __is_left(self, indices: List[int], faces: List[Face], face: Face) -> bool: 119 | sorted_faces = sorted(faces, key=lambda f: f.face_area.left) 120 | return sorted_faces.index(face) in indices 121 | 122 | def __is_center(self, indices: List[int], faces: List[Face], face: Face, width: int) -> bool: 123 | sorted_faces = sorted(faces, key=lambda f: abs((f.face_area.center - width / 2))) 124 | return sorted_faces.index(face) in indices 125 | 126 | def __is_right(self, indices: List[int], faces: List[Face], face: Face) -> bool: 127 | sorted_faces = sorted(faces, key=lambda f: f.face_area.right, reverse=True) 128 | return sorted_faces.index(face) in indices 129 | 130 | def __is_top(self, indices: List[int], faces: List[Face], face: Face) -> bool: 131 | sorted_faces = sorted(faces, key=lambda f: f.face_area.top) 132 | return sorted_faces.index(face) in indices 133 | 134 | def __is_middle(self, indices: List[int], faces: List[Face], face: Face, height: int) -> bool: 135 | sorted_faces = sorted(faces, key=lambda f: abs(f.face_area.middle - height / 2)) 136 | return sorted_faces.index(face) in indices 137 | 138 | def __is_bottom(self, indices: List[int], faces: List[Face], face: Face) -> bool: 139 | sorted_faces = sorted(faces, key=lambda f: f.face_area.bottom, reverse=True) 140 | return sorted_faces.index(face) in indices 141 | 142 | def __is_small(self, indices: List[int], faces: List[Face], face: Face) -> bool: 143 | sorted_faces = sorted(faces, key=lambda f: f.face_area.size) 144 | return sorted_faces.index(face) in indices 145 | 146 | def __is_large(self, indices: List[int], faces: List[Face], face: Face) -> bool: 147 | sorted_faces = sorted(faces, key=lambda f: f.face_area.size, reverse=True) 148 | return sorted_faces.index(face) in indices 149 | 150 | def process(self, jobs: List[Job], face: Face, p: StableDiffusionProcessingImg2Img, option: Option) -> Image: 151 | for job in jobs: 152 | fp = job.face_processor 153 | face_processor = registry.face_processors[fp.name] 154 | params = fp.params.copy() 155 | params["strength1"] = option.strength1 156 | 157 | angle = face.get_angle() 158 | correct_tilt = self.__correct_tilt(option, angle) 159 | face.image = rotate_image(face.image, angle) if correct_tilt else face.image 160 | 161 | image = face_processor.process(face, p, **params) 162 | 163 | face.image = rotate_image(image, -angle) if correct_tilt else image 164 | return face.image 165 | 166 | def __correct_tilt(self, option: Option, angle: float) -> bool: 167 | if self.correct_tilt: 168 | return True 169 | angle = abs(angle) 170 | if angle > 180: 171 | angle = 360 - angle 172 | return angle > option.tilt_adjustment_threshold 173 | 174 | def generate_mask(self, jobs: List[Job], face_image: np.ndarray, face: Face, option: Option) -> np.ndarray: 175 | mask = None 176 | for job in jobs: 177 | mg = job.mask_generator 178 | mask_generator = registry.mask_generators[mg.name] 179 | params = mg.params.copy() 180 | params["mask_size"] = option.mask_size 181 | params["use_minimal_area"] = option.use_minimal_area 182 | params["affected_areas"] = option.affected_areas 183 | params["tag"] = face.face_area.tag 184 | 185 | angle = face.get_angle() 186 | correct_tilt = self.__correct_tilt(option, angle) 187 | image = rotate_array(face_image, angle) if correct_tilt else face_image 188 | face_area_on_image = face.rotate_face_area_on_image(angle) if correct_tilt else face.face_area_on_image 189 | m = mask_generator.generate_mask(image, face_area_on_image, **params) 190 | m = rotate_array(m, -angle) if correct_tilt else m 191 | 192 | if mask is None: 193 | mask = m 194 | else: 195 | mask = mask + m 196 | 197 | assert mask is not None 198 | if option.mask_blur > 0: 199 | mask = cv2.blur(mask, (option.mask_blur, option.mask_blur)) 200 | 201 | return mask 202 | -------------------------------------------------------------------------------- /images/deno-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/deno-4.jpg -------------------------------------------------------------------------------- /images/deno-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/deno-6.jpg -------------------------------------------------------------------------------- /images/deno-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/deno-8.jpg -------------------------------------------------------------------------------- /images/face-margin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/face-margin.jpg -------------------------------------------------------------------------------- /images/image-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/image-01.jpg -------------------------------------------------------------------------------- /images/inferencers/anime_segmentation/mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/anime_segmentation/mask.jpg -------------------------------------------------------------------------------- /images/inferencers/bisenet-face-hair-hat-neck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/bisenet-face-hair-hat-neck.jpg -------------------------------------------------------------------------------- /images/inferencers/bisenet-face-hair-hat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/bisenet-face-hair-hat.jpg -------------------------------------------------------------------------------- /images/inferencers/bisenet-face-hair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/bisenet-face-hair.jpg -------------------------------------------------------------------------------- /images/inferencers/bisenet-face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/bisenet-face.jpg -------------------------------------------------------------------------------- /images/inferencers/blur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/blur.jpg -------------------------------------------------------------------------------- /images/inferencers/ellipse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/ellipse.jpg -------------------------------------------------------------------------------- /images/inferencers/face-area-rect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/face-area-rect.jpg -------------------------------------------------------------------------------- /images/inferencers/insightface/mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/insightface/mask.jpg -------------------------------------------------------------------------------- /images/inferencers/mediapipe/mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/mediapipe/mask.jpg -------------------------------------------------------------------------------- /images/inferencers/no-mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/no-mask.jpg -------------------------------------------------------------------------------- /images/inferencers/openmmlab/mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/openmmlab/mask.jpg -------------------------------------------------------------------------------- /images/inferencers/vignette-default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/vignette-default.jpg -------------------------------------------------------------------------------- /images/inferencers/vignette-use-minimal-area.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/inferencers/vignette-use-minimal-area.jpg -------------------------------------------------------------------------------- /images/mask-00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/mask-00.jpg -------------------------------------------------------------------------------- /images/mask-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/mask-10.jpg -------------------------------------------------------------------------------- /images/mask-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/mask-20.jpg -------------------------------------------------------------------------------- /images/screen-shot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/screen-shot.jpg -------------------------------------------------------------------------------- /images/setup-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/setup-01.png -------------------------------------------------------------------------------- /images/step-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-1.jpg -------------------------------------------------------------------------------- /images/step-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-2.jpg -------------------------------------------------------------------------------- /images/step-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-3.jpg -------------------------------------------------------------------------------- /images/step-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-4.jpg -------------------------------------------------------------------------------- /images/step-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-5.jpg -------------------------------------------------------------------------------- /images/step-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/step-6.jpg -------------------------------------------------------------------------------- /images/tips-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-01.png -------------------------------------------------------------------------------- /images/tips-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-02.jpg -------------------------------------------------------------------------------- /images/tips-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-02.png -------------------------------------------------------------------------------- /images/tips-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-03.jpg -------------------------------------------------------------------------------- /images/tips-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-03.png -------------------------------------------------------------------------------- /images/tips-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-04.png -------------------------------------------------------------------------------- /images/tips-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-05.jpg -------------------------------------------------------------------------------- /images/tips-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-06.png -------------------------------------------------------------------------------- /images/tips-07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-07.jpg -------------------------------------------------------------------------------- /images/tips-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/tips-08.png -------------------------------------------------------------------------------- /images/usage-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/usage-01.png -------------------------------------------------------------------------------- /images/usage-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/usage-02.png -------------------------------------------------------------------------------- /images/v1.0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/v1.0.0.jpg -------------------------------------------------------------------------------- /images/workflow-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-01.jpg -------------------------------------------------------------------------------- /images/workflow-editor-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-editor-01.png -------------------------------------------------------------------------------- /images/workflow-editor-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-editor-02.png -------------------------------------------------------------------------------- /images/workflow-editor-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-editor-03.png -------------------------------------------------------------------------------- /images/workflow-editor-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-editor-04.png -------------------------------------------------------------------------------- /images/workflow-editor-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ototadana/sd-face-editor/fd5fd91b955a42a01acb0ced46df044eecacb756/images/workflow-editor-05.png -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | try: 2 | from face_editor.io.util import load_classes_from_directory 3 | except Exception: 4 | import os 5 | import sys 6 | 7 | sys.path.append(os.path.dirname(__file__)) 8 | from face_editor.io.util import load_classes_from_directory 9 | 10 | import traceback 11 | 12 | import launch 13 | from modules import shared 14 | 15 | from face_editor.use_cases.installer import Installer 16 | 17 | if not shared.opts: 18 | from modules import shared_init 19 | 20 | shared_init.initialize() 21 | 22 | if shared.opts.data.get("face_editor_additional_components", None) is not None: 23 | for cls in load_classes_from_directory(Installer, True): 24 | try: 25 | cls().install() 26 | except Exception as e: 27 | print(traceback.format_exc()) 28 | print(f"Face Editor: {e}") 29 | 30 | launch.run_pip( 31 | "install lark", 32 | "requirements for Face Editor", 33 | ) 34 | -------------------------------------------------------------------------------- /scripts/face_editor.py: -------------------------------------------------------------------------------- 1 | import modules.scripts as scripts 2 | import modules.shared as shared 3 | from modules.processing import StableDiffusionProcessing, StableDiffusionProcessingImg2Img, process_images 4 | 5 | from face_editor.entities.option import Option 6 | from face_editor.ui.ui_builder import UiBuilder 7 | from face_editor.use_cases.image_processor import ImageProcessor 8 | from face_editor.use_cases.workflow_manager import WorkflowManager 9 | 10 | 11 | class Script(scripts.Script): 12 | def title(self): 13 | return "Face Editor" 14 | 15 | def show(self, is_img2img): 16 | return True 17 | 18 | def ui(self, is_img2img): 19 | builder = UiBuilder(False) 20 | components = builder.build(is_img2img) 21 | self.infotext_fields = builder.infotext_fields 22 | return components 23 | 24 | def run(self, o: StableDiffusionProcessing, *args): 25 | option = Option(*args) 26 | processor = ImageProcessor(WorkflowManager.get(option.workflow)) 27 | 28 | if ( 29 | isinstance(o, StableDiffusionProcessingImg2Img) 30 | and o.n_iter == 1 31 | and o.batch_size == 1 32 | and not option.apply_scripts_to_faces 33 | ): 34 | return processor.proc_image(o, option) 35 | else: 36 | shared.state.job_count = o.n_iter * 3 37 | if not option.save_original_image: 38 | o.do_not_save_samples = True 39 | res = process_images(o) 40 | o.do_not_save_samples = False 41 | 42 | return processor.proc_images(o, res, option) 43 | -------------------------------------------------------------------------------- /scripts/face_editor_extension.py: -------------------------------------------------------------------------------- 1 | import modules.scripts as scripts 2 | from modules import shared 3 | 4 | from face_editor.entities.option import Option 5 | from face_editor.ui.ui_builder import UiBuilder 6 | from face_editor.use_cases.image_processor import ImageProcessor 7 | from face_editor.use_cases.workflow_manager import WorkflowManager 8 | 9 | 10 | class FaceEditorExtension(scripts.Script): 11 | def __init__(self) -> None: 12 | super().__init__() 13 | self.__is_running = False 14 | 15 | def title(self): 16 | return "Face Editor EX" 17 | 18 | def show(self, is_img2img): 19 | return scripts.AlwaysVisible 20 | 21 | def ui(self, is_img2img): 22 | builder = UiBuilder(True) 23 | components = builder.build(is_img2img) 24 | self.infotext_fields = builder.infotext_fields 25 | return components 26 | 27 | def before_process_batch(self, p, enabled: bool, *args, **kwargs): 28 | if not enabled or self.__is_running: 29 | return 30 | option = Option(*args) 31 | if not option.save_original_image: 32 | p.do_not_save_samples = True 33 | 34 | if p.scripts is not None and hasattr(p.scripts, "alwayson_scripts"): 35 | script_index = shared.opts.data.get("face_editor_script_index", 99) 36 | for i, e in enumerate(p.scripts.alwayson_scripts): 37 | if e == self: 38 | p.scripts.alwayson_scripts.insert(script_index, p.scripts.alwayson_scripts.pop(i)) 39 | break 40 | 41 | def postprocess(self, o, res, enabled, *args): 42 | if not enabled or self.__is_running: 43 | return 44 | 45 | option = Option(*args) 46 | if isinstance(enabled, dict): 47 | option.update_by_dict(enabled) 48 | 49 | try: 50 | self.__is_running = True 51 | 52 | o.do_not_save_samples = False 53 | ImageProcessor(WorkflowManager.get(option.workflow)).proc_images(o, res, option) 54 | 55 | finally: 56 | self.__is_running = False 57 | -------------------------------------------------------------------------------- /workflows/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "RetinaFace", 3 | "rules": { 4 | "then": { 5 | "face_processor": "img2img", 6 | "mask_generator": { 7 | "name": "BiSeNet", 8 | "params": { 9 | "fallback_ratio": 0.1 10 | } 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /workflows/examples/README.md: -------------------------------------------------------------------------------- 1 | # Example Workflows 2 | 3 | - This folder contains workflow definitions that can be used as a reference when you create your own workflows. 4 | - To access these example workflow definitions from the workflow list in the Workflow Editor, the "Search workflows in subdirectories" option must be enabled. This option is located in the Face Editor section of the "Settings" tab. 5 | - These workflow definitions can be used as they are, or you can customize them and save them under a different name for personal use. 6 | - Please note that some workflows require specific "Additional components" to be enabled in the Face Editor section of the "Settings" tab for them to function correctly. 7 | 8 | --- 9 | 10 | ### Example 1: Basic Workflow - MediaPipe 11 | 12 | This workflow uses the MediaPipe face detector and applies the 'img2img' face processor and 'MediaPipe' mask generator to all detected faces. 13 | 14 | [View the workflow definition](mediapipe.json) 15 | 16 | Please note that to use this workflow, the 'mediapipe' option under "Additional components" in the Face Editor section of the "Settings" tab needs to be enabled. 17 | 18 | This is a good starting point for creating more complex workflows. You can customize this by changing the face detector or face processor, adding parameters, or adding conditions to apply different processing to different faces. 19 | 20 | --- 21 | 22 | ### Example 2: Basic Workflow - YOLO Example 23 | 24 | This workflow uses the YOLO face detector and applies the 'img2img' face processor and 'YOLO' mask generator to all detected faces. 25 | 26 | [View the workflow definition](yolov8n.json) 27 | 28 | Please note that to use this workflow, the 'yolo' option under "Additional components" in the Face Editor section of the "Settings" tab needs to be enabled. Also, you need to use a model trained for face detection, as the `yolov8n.pt` model specified here does not support face detection. 29 | 30 | Like the MediaPipe workflow, this is a good starting point for creating more complex workflows. You can customize it in the same ways. 31 | 32 | --- 33 | 34 | ### Example 3: High Accuracy Face Detection Workflow - Bingsu/adetailer Example 35 | 36 | This workflow uses the YOLO face detector, but it employs a model that is actually capable of face detection. From our testing, the accuracy of this model in detecting faces is outstanding. For more details on the model, please check [Bingsu/adetailer](https://huggingface.co/Bingsu/adetailer). 37 | 38 | [View the workflow definition](adetailer.json) 39 | 40 | Please note that to use this workflow, the 'yolo' option under "Additional components" in the Face Editor section of the "Settings" tab needs to be enabled. 41 | 42 | --- 43 | 44 | ### Example 4: Anime Face Detection Workflow - lbpcascade_animeface Example 45 | 46 | This workflow uses the `lbpcascade_animeface` face detector, which is specially designed for detecting anime faces. The source of this detector is the widely known [lbpcascade_animeface](https://github.com/nagadomi/lbpcascade_animeface) model. 47 | 48 | [View the workflow definition](lbpcascade_animeface.json) 49 | 50 | --- 51 | 52 | ### Example 5: Simple Face Blurring 53 | 54 | This workflow is straightforward and has a single, simple task - blurring detected faces in an image. It uses the `RetinaFace` method for face detection, which is a reliable and high-performance detection algorithm. 55 | 56 | [View the workflow definition](blur.json) 57 | 58 | The `Blur` face processor is employed here, which, as the name suggests, applies a blur effect to the detected faces. For masking, the workflow uses the `NoMask` mask generator. This is a special mask generator that doesn't mask anything - it simply allows the entire face to pass through to the face processor. 59 | 60 | As a result, the entire area of each detected face gets blurred. This can be useful in situations where you need to anonymize faces in images for privacy reasons. 61 | 62 | --- 63 | 64 | ### Example 6: Different Processing Based on Face Position 65 | 66 | This workflow employs the `RetinaFace` face detector and applies different processing depending on the position of the detected faces in the image. 67 | 68 | [View the workflow definition](blur_non_center_faces.json) 69 | 70 | For faces located in the center of the image (as specified by the `"criteria": "center"` condition), the `img2img` face processor and `BiSeNet` mask generator are used. This means that faces in the center of the image will be subject to advanced masking and img2img transformations. 71 | 72 | On the other hand, for faces not located in the center, the `Blur` face processor and `NoMask` mask generator are applied, effectively blurring these faces. 73 | 74 | This workflow could be handy in situations where you want to emphasize the subject in the middle of the photo, or to anonymize faces in the background for privacy reasons. 75 | 76 | --- 77 | 78 | ### Example 7: Basic Workflow - InsightFace 79 | 80 | This workflow utilizes the InsightFace face detector and applies the 'img2img' face processor and 'InsightFace' mask generator to all detected faces. 81 | 82 | [View the workflow definition](insightface.json) 83 | 84 | Please note that to use this workflow, the 'insightface' option under "Additional components" in the Face Editor section of the "Settings" tab needs to be enabled. 85 | 86 | --- 87 | 88 | ### Example 8: Blurring Young People's Faces 89 | 90 | This workflow employs the InsightFace face detector and adjusts the processing of detected faces based on their ages. 91 | 92 | [View the workflow definition](blur_young_people.json) 93 | 94 | For faces that are under 30 years old (as specified by the `"tag": "face?age<30"` condition), the Blur face processor and NoMask mask generator are applied. This results in blurring the faces of younger individuals. 95 | 96 | For all other faces (i.e., those aged 30 or over), the img2img face processor and BiSeNet mask generator are used. This leads to advanced masking and img2img transformations. 97 | 98 | This workflow can be beneficial in scenarios where you need to blur the faces of young people for anonymization in image processing. It also serves as an example of applying different processing based on age. 99 | 100 | Please note that the accuracy of age detection depends on the face detector's performance. There might be variations or inaccuracies in the detected age. 101 | 102 | -------------------------------------------------------------------------------- /workflows/examples/adetailer.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": [ 3 | { 4 | "name": "YOLO", 5 | "params": { 6 | "repo_id": "Bingsu/adetailer", 7 | "filename": "face_yolov8n.pt" 8 | } 9 | } 10 | ], 11 | "rules": [ 12 | { 13 | "then": { 14 | "face_processor": "img2img", 15 | "mask_generator": "BiSeNet" 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /workflows/examples/blur.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "RetinaFace", 3 | "rules": { 4 | "then": { 5 | "face_processor": "Blur", 6 | "mask_generator": "NoMask" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /workflows/examples/blur_non_center_faces.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "RetinaFace", 3 | "rules": [ 4 | { 5 | "when": { 6 | "criteria": "center" 7 | }, 8 | "then": { 9 | "face_processor": "img2img", 10 | "mask_generator": "BiSeNet" 11 | } 12 | }, 13 | { 14 | "then": { 15 | "face_processor": "Blur", 16 | "mask_generator": "NoMask" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /workflows/examples/blur_young_people.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "InsightFace", 3 | "rules": [ 4 | { 5 | "when": { 6 | "tag": "face?age<30" 7 | }, 8 | "then": { 9 | "face_processor": "Blur", 10 | "mask_generator": "NoMask" 11 | } 12 | }, 13 | { 14 | "then": { 15 | "face_processor": "img2img", 16 | "mask_generator": "BiSeNet" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /workflows/examples/insightface.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "InsightFace", 3 | "rules": { 4 | "then": { 5 | "face_processor": "img2img", 6 | "mask_generator": "InsightFace" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /workflows/examples/lbpcascade_animeface.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "lbpcascade_animeface", 3 | "rules": { 4 | "then": { 5 | "face_processor": "img2img", 6 | "mask_generator": { 7 | "name": "BiSeNet", 8 | "params": { 9 | "fallback_ratio": 0.3 10 | } 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /workflows/examples/mediapipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "MediaPipe", 3 | "rules": { 4 | "then": { 5 | "face_processor": "img2img", 6 | "mask_generator": "MediaPipe" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /workflows/examples/openmmlab.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": "RetinaFace", 3 | "rules": { 4 | "then": { 5 | "face_processor": "img2img", 6 | "mask_generator": "MMSeg" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /workflows/examples/yolov8n.json: -------------------------------------------------------------------------------- 1 | { 2 | "face_detector": { 3 | "name": "YOLO", 4 | "params": { 5 | "path": "yolov8n.pt" 6 | } 7 | }, 8 | "rules": { 9 | "then": { 10 | "face_processor": "img2img", 11 | "mask_generator": "YOLO" 12 | } 13 | } 14 | } --------------------------------------------------------------------------------