├── .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 | 
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 | 
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 | 
23 | 2. Then enter the prompts as usual and click the "Generate" button to modify the faces in the generated images.
24 | 
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 | 
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 | 
37 |
38 | This setting modifies the mask area as illustrated below:
39 |
40 | 
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 | 
49 |
50 | ---
51 | ### Change facial expression
52 | Use **"Prompt for face"** option if you want to change the facial expression.
53 |
54 | 
55 |
56 | #### Individual instructions for multiple faces
57 | 
58 |
59 | Faces can be individually directed with prompts separated by `||` (two vertical lines).
60 |
61 | 
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 | 
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 | 
103 |
104 |
105 | ### Step 3. Recreate the Faces
106 |
107 | Run img2img with the image to create a new face image.
108 |
109 | 
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 | 
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 | 
124 |
125 |
126 | ### Completed
127 |
128 | Show sample image
129 |
130 | 
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 | 
168 |
169 | **size: 10**
170 | 
171 |
172 | **size: 20**
173 | 
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 | 
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 | 
225 |
226 | **strength: 0.6**
227 | 
228 |
229 | **strength: 0.8**
230 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
134 |
135 | #### Affected areas - Face, Hair
136 | 
137 |
138 | #### Affected areas - Face, Hair, Hat
139 | 
140 |
141 | #### Affected areas - Face, Hair, Hat, Neck
142 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------