90 Commits
Author SHA1 Message Date
Gourieff | 古仁 a12c5b19dc UPD: Comfy Registry 2026-09-21 12:09:43 +07:00
Gourieff | 古仁 ea86b52e0f VersionUP
0.7.1 Beta3
2026-09-21 12:08:12 +07:00
Gourieff | 古仁 02a3518aa7 UPD: DLSS5 third-party note 2026-09-21 12:07:06 +07:00
Gourieff | 古仁 3746860aa4 UPD: Comfy Registry 2026-09-20 13:48:13 +07:00
Gourieff | 古仁 f92ebeb653 VersionUP
0.7.1 Beta2
2026-09-20 13:47:33 +07:00
Gourieff | 古仁 f6776fded7 UPD: DLSS5 auto_mask tooltip 2026-09-20 13:45:32 +07:00
Gourieff | 古仁 7b1233edc0 UPD: Comfy Registry 2026-09-20 13:11:43 +07:00
Gourieff | 古仁 b3a0ac8cf5 UPD: What's new sec -> DLSS5 requirements 2026-09-19 02:07:46 +07:00
Gourieff | 古仁 de7e09db0f UPD: Comfy Registry 2026-09-18 19:59:34 +07:00
Gourieff | 古仁 60b310dd7f UPD: Usage sec 2026-09-18 17:49:46 +07:00
Gourieff | 古仁 4351dc4272 FIX: trailing comma 2026-09-18 17:45:16 +07:00
Gourieff | 古仁 5f68562b6d UPD: What's new sec 2026-09-18 15:14:20 +07:00
Gourieff | 古仁 b9f8683da5 UPD: DLSS5FrameEnhancer Description 2026-09-18 14:25:23 +07:00
Gourieff | 古仁 78c8844f26 VersionUP
0.7.1 Beta1
2026-09-17 13:30:42 +07:00
Gourieff | 古仁 b01880c37f Merge branch 'dlss5-support' into evolve 2026-09-17 13:06:35 +07:00
Gourieff | 古仁 50fdb4993f UPD: Comfy Registry 2026-09-17 13:01:47 +07:00
Gourieff | 古仁 8fe1c0ec6d VersionUP
0.7.0 (Beta passed)
2026-09-17 13:00:49 +07:00
Gourieff | 古仁 619eb16a1e UPD: dlssnr dll README 2026-09-17 12:46:49 +07:00
Gourieff | 古仁 4dab1f1b0a ADD: DLSS5 Enhancer Node 2026-09-17 03:04:22 +07:00
Gourieff | 古仁 a51e88ca24 UPD: Comfy Registry 2026-09-04 19:39:15 +07:00
Gourieff | 古仁 99e23652a2 VersionUP
0.7.0 Beta1
2026-08-30 15:13:57 +07:00
Gourieff | 古仁 0aa14fabb4 UPD: Desktop friendly install.bat
https://codeberg.org/Gourieff/comfyui-reactor-node/issues/52
2026-08-30 15:11:22 +07:00
Gourieff | 古仁 ecd2b3c16d FIX: Typo
Issue #245
2026-08-30 15:03:36 +07:00
Gourieff | 古仁 6ad6b35a4d UPD: Comfy Registry 2026-05-13 00:24:34 +07:00
Gourieff | 古仁 2e34bf8356 VersionUP
0.7.0 Alpha2
2026-05-13 00:20:10 +07:00
Gourieff | 古仁 be42ca37ee FIX: Gender detection face index logic
Issue #234
2026-05-13 00:07:42 +07:00
Gourieff | 古仁 a7628ccdff UPD: Comfy Registry 2026-04-25 15:44:33 +07:00
Gourieff | 古仁 59040c1557 VersionUP
0.7.0 Alpha1
2026-04-25 15:34:34 +07:00
Gourieff | 古仁 e0cd862c3d Merge branch 'cleanup_and_refactor' into evolve 2026-04-25 13:37:08 +07:00
Gourieff | 古仁 fc1691dda0 UPD: Comfy Registry 2026-04-25 13:32:43 +07:00
Gourieff | 古仁 33d0feff2b VersionUP
0.6.2 (Beta passed)
2026-04-25 13:32:10 +07:00
Gourieff | 古仁 5881d3ee2f FIX: Utils Face object link 2026-04-23 23:38:58 +07:00
Gourieff | 古仁 febdf3027a DEL: Monkey-patch
Insightface patcher is no need any more
2026-04-23 23:15:05 +07:00
Gourieff | 古仁 74146c26e0 ADD: New node "Face Similarity" 2026-04-23 17:34:36 +07:00
Gourieff | 古仁 55787ee169 ADD: Hyperswap Class 2026-04-23 16:07:20 +07:00
Gourieff | 古仁 ca24587deb UPD: landmark_3d_68 Math 2026-04-23 13:01:17 +07:00
Gourieff | 古仁 11db21f666 FIX: SFW Score correction
Still safe
2026-04-23 12:54:24 +07:00
Gourieff | 古仁 1356f18bf7 UPD: New ReActor Core
- No Insightface required
- Numpy 1.x 2.x friendly
2026-04-23 01:11:47 +07:00
Gourieff | 古仁 b60036fc85 FIX: HyperSwap CPU Float norm
Issue #183
Contributor: @Buumcode
2026-04-22 15:55:38 +07:00
Gourieff | 古仁 a2a61fd5e7 UPD: Comfy Registry 2025-12-08 15:23:23 +07:00
Gourieff | 古仁 d93450bd80 UPD: What's new sec 2025-10-07 23:23:45 +07:00
Gourieff | 古仁 67ce1186f8 FIX: Typo 2025-10-07 22:34:56 +07:00
Gourieff | 古仁 e008d3c5ae UPD: Profile url 2025-10-07 22:32:36 +07:00
Gourieff | 古仁 b26892f342 UPD: What's new sec 2025-10-07 22:28:31 +07:00
Gourieff | 古仁 b51d34e05a UPD: NSFW filter Score correction 2025-10-07 17:19:14 +07:00
Gourieff | 古仁 4ea6a541ec UPD: What's new sec 2025-10-07 02:42:56 +07:00
Gourieff | 古仁 18fc8a303f Merge branch 'hyperswap' into evolve 2025-10-07 02:40:03 +07:00
Art Gourieff 43bafc227e Merge pull request #177 from pervkajan-rgb/main
Fix multiple face index
2025-10-07 02:37:57 +07:00
Gourieff | 古仁 85763bc0b7 FIX: get_face_gender ValueError 2025-10-07 02:21:27 +07:00
Gourieff | 古仁 b8d49816be UPD: What's New, Models links + VersionUP
0.6.2 Beta1
2025-10-06 23:45:35 +07:00
Gourieff | 古仁 95ed7b248f UPD: HyperSwap models support
FR #143
Thanks @Buumcode for contribution
2025-10-06 22:15:27 +07:00
Gourieff | 古仁 d8e3b90c82 VersionUP + UPD: Comfy Registry
0.6.2 Alpha4
2025-10-03 01:43:46 +07:00
Gourieff | 古仁 13d836015b FIX: MaskHelper - move result back to CPU
To avoid "RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu" might be thrown by the next node.
2025-10-03 01:41:22 +07:00
pervkajan-rgb fd5f9e1de2 Fix multiple face index 2025-09-30 16:18:34 +03:00
Gourieff | 古仁 a2ad854a09 VersionUP + UPD: Comfy Registry
0.6.2 Alpha3
2025-09-23 01:09:25 +07:00
Gourieff | 古仁 a708f71a2f HOTFIX: ReActorPlusOpt init missed var 2025-09-23 01:04:52 +07:00
Gourieff | 古仁 6ee22a8ecb VersionUP + UPD: Comfy Registry
0.6.2 Alpha2
2025-09-22 19:35:28 +07:00
Gourieff | 古仁 9d5dba9801 HOTFIX: get_face_gender ValueError 2025-09-22 19:31:32 +07:00
Gourieff | 古仁 dd44fffd9e UPD: Comfy Registry 2025-09-21 16:55:13 +07:00
Gourieff | 古仁 4eb66b3a8f UPD: Comfy Registry 2025-09-21 15:23:01 +07:00
Gourieff | 古仁 e7dd3bbbc9 HOTFIX: index error 2025-09-21 14:47:10 +07:00
Gourieff | 古仁 e9d34b3a91 UPD: Comfy Registry 2025-09-21 13:26:26 +07:00
Gourieff | 古仁 c98dc50515 UPD: Insightface instructions
FR #163
Issue #172
2025-09-20 15:42:34 +07:00
Gourieff | 古仁 14595e4350 UPD: Face Model Name string
FACE_MODEL_NAME output for "Load Face Model" node
FR #134
2025-09-20 14:36:44 +07:00
Gourieff | 古仁 1ee4e21d21 UPD: web.archive URLs to old ReActor repo 2025-09-15 15:52:04 +07:00
Gourieff | 古仁 0a5f8ebffd UPD: Restore Face Advanced Node
Thanks @Buumcode for implementation of "Restore Face Filter"
2025-09-14 23:54:06 +07:00
Gourieff | 古仁 d218c52501 VersionUP
0.6.2 Alpha1
2025-09-14 04:20:03 +07:00
Gourieff | 古仁 ec743a7de4 UPD: Face Restore Filter -Stage2- 2025-09-14 03:09:16 +07:00
Gourieff | 古仁 8e41e6c900 UPD: Face Restore Filter
FR #159
2025-09-05 15:24:21 +07:00
Gourieff | 古仁 5c7884118b UPD: Comfy Registry 2025-09-05 12:51:32 +07:00
Gourieff | 古仁 0e8b1edd09 VersionUP
0.6.1 (Beta passed)
2025-09-05 12:49:39 +07:00
Gourieff | 古仁 d60458f212 FIX: original_image output 2025-08-04 01:47:36 +07:00
Gourieff | 古仁 9b17e4cea5 UPD: Comfy Registry 2025-07-09 20:50:55 +07:00
Gourieff | 古仁 1465ec6a66 FIX: Typo 2025-07-09 20:41:21 +07:00
Gourieff | 古仁 560211e3ab FIX: Docs 2025-07-09 20:37:25 +07:00
Gourieff | 古仁 aa318b5073 UPD: Docs 2025-07-09 20:27:25 +07:00
Gourieff | 古仁 6244ee6cdc VersionUP
0.6.1 Beta3
2025-07-01 23:41:50 +07:00
Gourieff | 古仁 5230d0f09b UPD: Gender det better logic for many faces 2025-07-01 23:38:15 +07:00
Gourieff | 古仁 392bbfd0af UPD: Inswapper dl URLs 2025-07-01 15:09:20 +07:00
Gourieff | 古仁 48a3ad27f9 UPD: Comfy Registry 2025-05-26 23:30:21 +07:00
Gourieff | 古仁 da1d284141 VersionUP
0.6.1 Beta2
2025-05-26 23:26:59 +07:00
Gourieff | 古仁 be2e8d549a UPD: Less annoying msgs 2025-05-26 23:22:51 +07:00
Gourieff | 古仁 701259b66e FIX: Numpy array error if Tensor is on GPU
Issue #127
2025-05-26 22:58:49 +07:00
Gourieff | 古仁 bc439d97a1 HOTFIX: " -> '
Issue #125
2025-05-22 17:09:17 +07:00
Gourieff | 古仁 3626434689 UPD: Comfy Registry 2025-05-21 14:36:55 +07:00
Gourieff | 古仁 2041f67349 UPD: What's New section 2025-05-21 14:33:36 +07:00
Gourieff | 古仁 483d56cc80 VersionUP
0.6.1 Beta1
2025-05-21 14:16:48 +07:00
Gourieff | 古仁 3d65ba0765 Merge branch 'main' into evolve 2025-05-21 14:14:05 +07:00
Gourieff | 古仁 f6b4a0ebce UPDs and FIXs
- #25 Fix
- ComfyUI native ProgressBar for different steps
- ORIGINAL_IMAGE output for main nodes
- no tmp file for nsfw detector
- nsfw detector little speed up
2025-05-21 12:47:35 +07:00
Gourieff | 古仁 0addca8a40 UPD: MaskHelper SpeedUp - Try1 2025-05-20 14:38:18 +07:00
32 changed files with 2637 additions and 902 deletions
+1
View File
@@ -3,3 +3,4 @@ __pycache__/
.vscode/
example
input
*.dll
+127 -105
View File
@@ -2,16 +2,8 @@
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
![Version](https://img.shields.io/badge/node_version-0.6.0-brightgreen?style=for-the-badge&labelColor=darkgreen)
![Version](https://img.shields.io/badge/node_version-0.7.1_beta3-green?style=for-the-badge&labelColor=darkgreen)
<!--<sup>
<font color=brightred>
## !!! [Important Update](#latestupdate) !!!<br>Don't forget to add the Node again in existing workflows
</font>
</sup>-->
<a href="https://boosty.to/artgourieff" target="_blank">
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Support Me on Boosty"/>
<br>
@@ -20,6 +12,9 @@
</sup>
</a>
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
<hr>
[![Commit activity](https://img.shields.io/github/commit-activity/t/Gourieff/ComfyUI-ReActor/main?cacheSeconds=0)](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
@@ -34,7 +29,7 @@
</div>
### The Fast and Simple Face Swap Extension Nodes for ComfyUI, based on [blocked ReActor](https://github.com/Gourieff/comfyui-reactor-node) - now it has a nudity detector to avoid using this software with 18+ content
### The Fast and Simple Face Swap Extension Nodes for ComfyUI, based on [blocked ReActor](https://web.archive.org/web/20241230084620/https://github.com/Gourieff/comfyui-reactor-node) - now it has a nudity detector to avoid using this software with 18+ content
> By using this Node you accept and assume [responsibility](#disclaimer)
@@ -51,6 +46,64 @@
## What's new in the latest update
### 0.7.1 <sub><sup>BETA1</sup></sub>
- **New `DLSS5 Frame Enhancer` node**. NVIDIA's latest DLSS 5 technology for enhancing image quality (both overall frame or faces by mask). <br><u>See [Installation Instructions](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>The integration operates in an isolated Host mode, fully eliminating conflicts with ComfyUI's CUDA context.<br>Huge thanks to the author of the [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) project for the C++ wrapper (neuroframe_engine.dll and neuroframe_caller.dll), which formed the base of the computational bridge for this node.<br>Also thanks Gemini 3.1 Pro (via [Google Gemini](https://gemini.google.com/app)) for the contribution.</pre><u>Requirements:</u><br>
-- Windows 10/11<br>
-- NVIDIA display driver >= 616.x<br>
-- NVIDIA RTX 40/50-series GPU<br>
(compatibility with older RTX series is unconfirmed)
### 0.7.0
- 💥 **Big Update! ☢ New ReActor Core!**<br>✅ No `Insightface` required!<br>✅ No `C++ Build Tools` required!<br>✅ Instalation process is much easier now!<br>✅ `Numpy 2.x` friendly as well as `1.x`!<br>⚠ <u>A swap result is slightly different now</u>. Hard to say if it’s "better" or "worse" — it’s just a bit different. But if we look at the numbers (cosine similarity of face embeddings), the accuracy is actually a little higher than with Insightface.<br>More info you can find here: https://t.me/reactor_faceswap/55
- New Node "Face Similarity" to check face likeness after you make a swap
- HyperSwap CPU Float normalization fix (thanks @Buumcode, issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
- Fixed: gender detection issue and face index logic (issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
- Comfy Desktop friendly `install.bat`
- Other fixes and improvements
<details>
<summary><a>Previous versions</a></summary>
### 0.6.2
- Added support of HyperSwap models by FaceFusion Labs (thanks [@Buumcode](https://github.com/Buumcode) for contribution)<br>You can download them [here](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>and put them into the `ComfyUI\models\hyperswap` directory
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
</center>
[Comparison grid](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) of Inswapper vs Reswapper vs HyperSwap
- Face restoration process affects only swapped faces
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
</center>
- New Node "Restore Face Advanced" with Face Restore Filter, thanks https://github.com/Buumcode for implementation of "Restore Face Filter"<br>This node helps you apply the restoration process to the face(s) you need
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
</center>
- Added FACE_MODEL_NAME output for "Load Face Model" node
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
</center>
- Fixes and improvements
### 0.6.1
- Gender detection better logic for many faces and many indexes
- MaskHelper node 2x speed up - not perfect yet but 1.5x-2x faster then before
- ComfyUI native ProgressBar for different steps
- ORIGINAL_IMAGE output for main nodes
- Different fixes and improvements (https://github.com/Gourieff/ComfyUI-ReActor/issues/25 fix; no tmp file for NSFW detector; NSFW detector little speed up)
### 0.6.0
- New Node `ReActorSetWeight` - you can now set the strength of face swap for `source_image` or `face_model` from 0% to 100% (in 12.5% step)
@@ -61,9 +114,6 @@
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
</center>
<details>
<summary><a>Previous versions</a></summary>
### 0.5.2
- ReSwapper models support. Although Inswapper still has the best similarity, but ReSwapper is evolving - thanks @somanchiu https://github.com/somanchiu/ReSwapper for the ReSwapper models and the ReSwapper project! This is a good step for the Community in the Inswapper's alternative creation!
@@ -104,7 +154,7 @@ Just put them into the "models/reswapper" directory.
- Sorting facemodels alphabetically
- A lot of fixes and improvements
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
- Spandrel lib support for GFPGAN
@@ -152,7 +202,7 @@ Use this Node to gain the best results of the face swapping process:
- Little speed boost when analyzing target images (unfortunately it is still quite slow in compare to swapping and restoring...)
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
- GPEN-BFR-512 and RestoreFormer_Plus_Plus face restoration models support
@@ -175,12 +225,12 @@ Result example (the new face was created from 4 faces of different actresses):
Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
- CUDA 12 Support - don't forget to run (Windows) `install.bat` or (Linux/MacOS) `install.py` for ComfyUI's Python enclosure or try to install ORT-GPU for CU12 manually (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
- Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173 fix
- Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173) fix
- Separate Node for the Face Restoration postprocessing (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), can be found inside ReActor's menu (RestoreFace Node)
- Separate Node for the Face Restoration postprocessing (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), can be found inside ReActor's menu (RestoreFace Node)
- (Windows) Installation can be done for Python from the System's PATH
- Different fixes and improvements
@@ -188,7 +238,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
- Input "input_image" goes first now, it gives a correct bypass and also it is right to have the main input first;
- You can now save face models as "safetensors" files (`ComfyUI\models\reactor\faces`) and load them into ReActor implementing different scenarios and keeping super lightweight face models of the faces you use:
@@ -209,47 +259,25 @@ Thanks to everyone who finds bugs, suggests new features and supports this proje
## Installation
<details>
<summary>SD WebUI: <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/">AUTOMATIC1111</a> or <a href="https://github.com/vladmandic/automatic">SD.Next</a></summary>
### Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows
1. Close (stop) your SD-WebUI/Comfy Server if it's running
2. (For Windows Users):
- Install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community version - you need this step to build Insightface)
- OR only [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and select "Desktop Development with C++" under "Workloads -> Desktop & Mobile"
- OR if you don't want to install VS or VS C++ BT - follow [this steps (sec. I)](#insightfacebuild)
3. Go to the `extensions\sd-webui-comfyui\ComfyUI\custom_nodes`
4. Open Console or Terminal and run `git clone https://github.com/Gourieff/ComfyUI-ReActor`
5. Go to the SD WebUI root folder, open Console or Terminal and run (Windows users)`.\venv\Scripts\activate` or (Linux/MacOS)`venv/bin/activate`
6. `python -m pip install -U pip`
7. `cd extensions\sd-webui-comfyui\ComfyUI\custom_nodes\ComfyUI-ReActor`
8. `python install.py`
9. Please, wait until the installation process will be finished
10. (From the version 0.3.0) Download additional facerestorers models from the link below and put them into the `extensions\sd-webui-comfyui\ComfyUI\models\facerestore_models` directory:<br>
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models
11. Run SD WebUI and check console for the message that ReActor Node is running:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/console_status_running.jpg?raw=true" alt="console_status_running" width="759"/>
12. Go to the ComfyUI tab and find there ReActor Node inside the menu `ReActor` or by using a search:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/webui-demo.png?raw=true" alt="webui-demo" width="100%"/>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/search-demo.png?raw=true" alt="webui-demo" width="1043"/>
</details>
<details>
<summary>Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows</summary>
1. Do the following:
- Install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community version - you need this step to build Insightface)
- OR only [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and select "Desktop Development with C++" under "Workloads -> Desktop & Mobile"
- OR if you don't want to install VS or VS C++ BT - follow [this steps (sec. I)](#insightfacebuild)
2. Choose between two options:
1. Choose between two options:
- (ComfyUI Manager) Open ComfyUI Manager, click "Install Custom Nodes", type "ReActor" in the "Search" field and then click "Install". After ComfyUI will complete the process - please restart the Server.
- (Manually) Go to `ComfyUI\custom_nodes`, open Console and run `git clone https://github.com/Gourieff/ComfyUI-ReActor`
3. Go to `ComfyUI\custom_nodes\ComfyUI-ReActor` and run `install.bat`
4. If you don't have the "face_yolov8m.pt" Ultralytics model - you can download it from the [Assets](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) and put it into the "ComfyUI\models\ultralytics\bbox" directory<br>As well as one or both of "Sams" models from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - download (if you don't have them) and put into the "ComfyUI\models\sams" directory
5. Run ComfyUI and find there ReActor Nodes inside the menu `ReActor` or by using a search
2. Go to `ComfyUI\custom_nodes\ComfyUI-ReActor` and run `install.bat`
3. Download required models from the Section below
4. Run ComfyUI and find there ReActor Nodes inside the menu `ReActor` or by using a search
</details>
## Models
- buffalo_l: downloaded on first launch into `ComfyUI\models\insightface\models\buffalo_l`, or you can download manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
- inswapper_128: downloaded during installation into `ComfyUI\models\insightface`, or you can download manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models put them into `ComfyUI\models\reswapper`
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) put them into `ComfyUI\models\hyperswap`
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models put any you like into `ComfyUI\models\facerestore_models`
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt put into `ComfyUI\models\ultralytics\bbox`
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams put into `ComfyUI\models\sams`
## Usage
@@ -257,11 +285,12 @@ You can find ReActor Nodes inside the menu `ReActor` or by using a search (just
List of Nodes:
- ••• Main Nodes •••
- ReActorFaceSwap (Main Node)
- ReActorFaceSwapOpt (Main Node with the additional Options input)
- ReActorOptions (Options for ReActorFaceSwapOpt)
- ReActorFaceBoost (Face Booster Node)
- ReActorMaskHelper (Masking Helper)
- ReActorFaceSwap (Main Node)
- ReActorFaceSwapOpt (Main Node with the additional Options input)
- ReActorOptions (Options for ReActorFaceSwapOpt)
- ReActorFaceBoost (Face Booster Node)
- ReActorMaskHelper (Masking Helper)
- ReActorSetWeight (Set Face Swap Weight)
- ••• Operations with Face Models •••
- ReActorSaveFaceModel (Save Face Model)
- ReActorLoadFaceModel (Load Face Model)
@@ -269,8 +298,12 @@ List of Nodes:
- ReActorMakeFaceModelBatch (Make Face Model Batch)
- ••• Additional Nodes •••
- ReActorRestoreFace (Face Restoration)
- ReActorRestoreFaceAdvanced (Restore Face Advanced)
- ReActorFaceSimilarity (Face Similarity)
- ReActorImageDublicator (Dublicate one Image to Images List)
- ImageRGBA2RGB (Convert RGBA to RGB)
- ReActorUnload (Unload ReActor models from VRAM)
- DLSS5FrameEnhancer (Enhance frame quality with NVIDIA DLSS 5)
Connect all required slots and run the query.
@@ -282,6 +315,10 @@ Connect all required slots and run the query.
- Supported Nodes: "Load Image" or any other nodes providing images as an output;
- `face_model` - is the input for the "Load Face Model" Node or another ReActor node to provide a face model file (face embedding) you created earlier via the "Save Face Model" Node;
- Supported Nodes: "Load Face Model", "Build Blended Face Model";
- `options` - to connect ReActorOptions;
- Supported Nodes: "ReActorOptions";
- `face_boost` - to connect ReActorFaceBoost;
- Supported Nodes: "ReActorFaceBoost";
### Main Node Outputs
@@ -289,6 +326,7 @@ Connect all required slots and run the query.
- Supported Nodes: any nodes which have images as an input;
- `FACE_MODEL` - is an output providing a source face's model being built during the swapping process;
- Supported Nodes: "Save Face Model", "ReActor", "Make Face Model Batch";
- `ORIGINAL_IMAGE` - `input_image` bypass;
### Face Restoration
@@ -317,59 +355,43 @@ Since version 0.4.0 you can save face models as "safetensors" files (stored in `
To make new models appear in the list of the "Load Face Model" Node - just refresh the page of your ComfyUI web application.<br>
(I recommend you to use ComfyUI Manager - otherwise you workflow can be lost after you refresh the page if you didn't save it before that).
### Masking Helper
Face Masking feature is available since version 0.5.0, just add the "ReActorMaskHelper" Node to the workflow and connect it as shown below:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-01.jpg?raw=true" alt="0.5.0-whatsnew-01" width="100%"/>
If you don't have the "face_yolov8m.pt" Ultralytics model - you can download it from the [Assets](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) and put it into the "ComfyUI\models\ultralytics\bbox" directory
<br>
As well as ["sam_vit_b_01ec64.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_b_01ec64.pth) or ["sam_vit_l_0b3195.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_l_0b3195.pth) (better occlusion) - download (if you don't have it) and put it into the "ComfyUI\models\sams" directory;
Use this Node to gain the best results of the face swapping process:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-02.jpg?raw=true" alt="0.5.0-whatsnew-02" width="100%"/>
### Face Swap Weigth
You can set the strength of face swap for `source_image` or `face_model` from 0% to 100% (in 12.5% step) with `ReActorSetWeight` node
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
</center>
## Troubleshooting
<a name="insightfacebuild">
### **I. (For Windows users) If you still cannot build Insightface for some reasons or just don't want to install Visual Studio or VS C++ Build Tools - do the following:**
1. (ComfyUI Portable) From the root folder check the version of Python:<br>run CMD and type `python_embeded\python.exe -V`
2. Download prebuilt Insightface package [for Python 3.10](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp310-cp310-win_amd64.whl) or [for Python 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (if in the previous step you see 3.11) or [for Python 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (if in the previous step you see 3.12) and put into the stable-diffusion-webui (A1111 or SD.Next) root folder (where you have "webui-user.bat" file) or into ComfyUI root folder if you use ComfyUI Portable
3. From the root folder run:
- (SD WebUI) CMD and `.\venv\Scripts\activate`
- (ComfyUI Portable) run CMD
4. Then update your PIP:
- (SD WebUI) `python -m pip install -U pip`
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
5. Then install Insightface:
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (for 3.10) or `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (for 3.11) or `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (for 3.10) or `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (for 3.11) or `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
6. Enjoy!
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
This error may occur if there's smth wrong with the model file `inswapper_128.onnx`
Try to download it manually from [here](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx)
Try to download it manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
and put it to the `ComfyUI\models\insightface` replacing existing one
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
This means that input points have been changed with the latest update<br>
Remove the current ReActor Node from your workflow and add it again
### **IV. ControlNet Aux Node IMPORT failed error when using with ReActor Node**
1. Close ComfyUI if it runs
2. Go to the ComfyUI root folder, open CMD there and run:
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
3. That's it!
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
### **V. "ModuleNotFoundError: No module named 'basicsr'" or "subprocess-exited-with-error" during future-0.18.3 installation**
- Download https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
- Put it to ComfyUI root And run:
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
- Then:
python_embeded\python.exe -m pip install basicsr
### **VI. "fatal: fetch-pack: invalid index-pack output" when you try to `git clone` the repository"**
### **III. "fatal: fetch-pack: invalid index-pack output" when you try to `git clone` the repository"**
Try to clone with `--depth=1` (last commit only):
@@ -468,8 +490,8 @@ SHA256:4c06341c33c2ca1f86781dab0e829f88ad5b64be9fba56e56bc9ebdefc619e43
| [codeformer-v0.1.0.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/codeformer-v0.1.0.pth) | [sczhou](https://github.com/sczhou/CodeFormer) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [GFPGANv1.3.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.3.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) | ![license](https://img.shields.io/badge/license-Apache_2.0-green.svg) |
| [GFPGANv1.4.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.4.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) | ![license](https://img.shields.io/badge/license-Apache_2.0-green.svg) |
| [inswapper_128.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128_fp16.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128_fp16.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
[BasicSR](https://github.com/XPixelGroup/BasicSR) - [@XPixelGroup](https://github.com/XPixelGroup) <br>
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
+138 -116
View File
@@ -2,15 +2,7 @@
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_RU.png?raw=true" alt="logo" width="180px"/>
![Version](https://img.shields.io/badge/версия_нода-0.6.0-brightgreen?style=for-the-badge&labelColor=darkgreen)
<!--<sup>
<font color=brightred>
## !!! [Важные изменения](#latestupdate) !!!<br>Не забудьте добавить Нод заново в существующие воркфлоу
</font>
</sup>-->
![Version](https://img.shields.io/badge/версия_нода-0.7.1_beta3-green?style=for-the-badge&labelColor=darkgreen)
<a href="https://boosty.to/artgourieff" target="_blank">
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
@@ -20,6 +12,9 @@
</sup>
</a>
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
<hr>
[![Commit activity](https://img.shields.io/github/commit-activity/t/Gourieff/ComfyUI-ReActor/main?cacheSeconds=0)](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
@@ -34,7 +29,7 @@
</div>
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://github.com/Gourieff/comfyui-reactor-node) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://web.archive.org/web/20241126185020/https://github.com/Gourieff/comfyui-reactor-node/blob/main/README_RU.md) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
> Используя данное ПО, вы понимаете и принимаете [ответственность](#disclaimer)
@@ -51,6 +46,64 @@
## Что нового в последнем обновлении
### 0.7.1 <sub><sup>BETA1</sup></sub>
- **Новый узел `DLSS5 Frame Enhancer`**. Новейшая технология DLSS 5 от NVIDIA для улучшения качества изображения (кадра целиком или лиц по маске).<br><u>Следуйте [инструкции по установке](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>Интеграция работает в изолированном Host-режиме, что полностью исключает конфликты с CUDA-контекстом ComfyUI.<br>Огромная благодарность автору проекта [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) за C++ обертку (neuroframe_engine.dll и neuroframe_caller.dll), которая легла в основу вычислительного моста для данного узла.<br>Также благодарность Gemini 3.1 Pro (через [Google Gemini](https://gemini.google.com/app)) за ассистирование и помощь.</pre><u>Требования:</u><br>
-- Windows 10/11<br>
-- NVIDIA display driver >= 616.x<br>
-- NVIDIA RTX 40/50-series GPU<br>
(совместимость с более старыми сериями RTX не подтверждена)
### 0.7.0
- 💥 **Важное обновление! ☢ Новое ядро РеАктора!**<br>✅ Библиотека `Insightface` больше не требуется!<br>✅ `C++ Build Tools` больше не требуются!<br>✅ Более простой процесс установки!<br>✅ Поддержка `Numpy 2.x` и `1.x`!<br>⚠ <u>Результат Свапа теперь слегка отличается</u>. Сложно сказать, хуже или лучше — результат просто чуть-чуть другой. По показателям схожести (оценка косинусного сходства векторов эмбеддингов лиц) результат стал немного лучше (хоть и незначительно), чем с Insightface.<br>Подробнее здесь: https://t.me/reactor_faceswap/55
- Новый узел "Face Similarity" для проверки схожести лиц после свапа
- Исправление "HyperSwap CPU Float normalization" (спасибо @Buumcode, Issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
- Исправлено: проблема определения пола и логика индексов лиц (Issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
- Улучшен `install.bat` для поддержки запуска установки в Comfy Desktop
- Прочие улучшение и исправления
<details>
<summary><a>Предыдущие версии</a></summary>
### 0.6.2
- Добавлена поддержка моделей HyperSwap от команды FaceFusion Labs (спасибо [@Buumcode](https://github.com/Buumcode) за вариант реализации)<br>Модели можно скачать [отсюда](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>и положить в папку `ComfyUI/models/hyperswap`
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
</center>
[Сравнение](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) моделей Inswapper, Reswapper, HyperSwap
- Теперь восстановление лиц затрагивает только заменённые лица
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01-ru.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
</center>
- Новый узел "Restore Face Advanced" с фильтром по лицам, спасибо https://github.com/Buumcode за реализацию "Фильтра восстановления лиц"<br>Этот узел помогает применить восстановление именно к нужному лицу или лицам
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
</center>
- Добавлен выход FACE_MODEL_NAME для узла "Load Face Model"
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
</center>
- Исправления и улучшения
### 0.6.1
- Улучшенная логика работы с индексами множества лиц при определении пола
- MaskHelper нод теперь почти вдвое быстрее - пока не идеально, но лучше, чем было ранее
- Нативный ProgressBar ComfyUI для разных шагов
- Добавлен выход ORIGINAL_IMAGE для основных нодов
- Разные исправления и улучшения (https://github.com/Gourieff/ComfyUI-ReActor/issues/25 фикс; временные файлы для NSFW детектора больше не создаются; NSFW детектор стал работать немного быстрее)
### 0.6.0
- Новый нод `ReActorSetWeight` - теперь можно установить силу замены лица для `source_image` или `face_model` от 0% до 100% (с шагом 12.5%)
@@ -61,9 +114,6 @@
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
</center>
<details>
<summary><a>Предыдущие версии</a></summary>
### 0.5.2
- Поддержка моделей ReSwapper. Несмотря на то, что Inswapper по-прежнему даёт лучшее сходство, но ReSwapper развивается - спасибо @somanchiu https://github.com/somanchiu/ReSwapper за эти модели и проект ReSwapper! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
@@ -103,7 +153,7 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
- Сортировка моделей лиц по алфавиту
- Множество исправлений и улучшений
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
- Поддержка библиотеки Spandrel при работе с GFPGAN
@@ -151,7 +201,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
- Небольшое улучшение скорости анализа целевых изображений (input)
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
- Добавлена поддержка GPEN-BFR-512 и RestoreFormer_Plus_Plus моделей восстановления лиц
@@ -174,12 +224,12 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
Базовый воркфлоу [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
- Поддержка CUDA 12 - не забудьте запустить (Windows) `install.bat` или (Linux/MacOS) `install.py` для используемого Python окружения или попробуйте установить ORT-GPU для CU12 вручную (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
- Исправление Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173
- Исправление Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173)
- Отдельный Нод для восстаноления лиц (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), располагается внутри меню ReActor (нод RestoreFace)
- Отдельный Нод для восстаноления лиц (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), располагается внутри меню ReActor (нод RestoreFace)
- (Windows) Установка зависимостей теперь может быть выполнена в Python из PATH ОС
- Разные исправления и улучшения
@@ -187,7 +237,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
- Вход "input_image" теперь идёт первым, это даёт возможность корректного байпаса, а также это правильно с точки зрения расположения входов (главный вход - первый);
- Теперь можно сохранять модели лиц в качестве файлов "safetensors" (`ComfyUI\models\reactor\faces`) и загружать их в ReActor, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
@@ -210,48 +260,24 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
## Установка
<details>
<summary>SD WebUI: <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/">AUTOMATIC1111</a> или <a href="https://github.com/vladmandic/automatic">SD.Next</a></summary>
### Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows
1. Закройте (остановите) SD-WebUI Сервер, если запущен
2. (Для пользователей Windows):
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
3. Перейдите в `extensions\sd-webui-comfyui\ComfyUI\custom_nodes`
4. Откройте Консоль или Терминал и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
5. Перейдите в корневую директорию SD WebUI, откройте Консоль или Терминал и выполните (для пользователей Windows)`.\venv\Scripts\activate` или (для пользователей Linux/MacOS)`venv/bin/activate`
6. `python -m pip install -U pip`
7. `cd extensions\sd-webui-comfyui\ComfyUI\custom_nodes\ComfyUI-ReActor`
8. `python install.py`
9. Пожалуйста, дождитесь полного завершения установки
10. (Начиная с версии 0.3.0) Скачайте дополнительные модели восстановления лиц (по ссылке ниже) и сохраните их в папку `extensions\sd-webui-comfyui\ComfyUI\models\facerestore_models`:<br>
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models
11. Запустите SD WebUI и проверьте консоль на сообщение, что ReActor Node работает:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/console_status_running.jpg?raw=true" alt="console_status_running" width="759"/>
12. Перейдите во вкладку ComfyUI и найдите там ReActor Node внутри меню `ReActor` или через поиск:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/webui-demo.png?raw=true" alt="webui-demo" width="100%"/>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/search-demo.png?raw=true" alt="webui-demo" width="1043"/>
</details>
<details>
<summary>Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows</summary>
1. Сделайте следующее:
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
2. Выберите из двух вариантов:
1. Выберите из двух вариантов:
- (ComfyUI Manager) Откройте ComfyUI Manager, нажвите "Install Custom Nodes", введите "ReActor" в поле "Search" и далее нажмите "Install". После того, как ComfyUI завершит установку, перезагрузите сервер.
- (Вручную) Перейдите в `ComfyUI\custom_nodes`, откройте Консоль и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
3. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
4. Если модель "face_yolov8m.pt" у вас отсутствует - можете скачать её [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положить в папку "ComfyUI\models\ultralytics\bbox"<br>
То же самое и с "Sams" моделями, скачайте одну или обе [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - и положите в папку "ComfyUI\models\sams"
5. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
2. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
3. Скачайте необходимые модели из Раздела "Модели" ниже
4. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
</details>
## Модели
- buffalo_l: скачиваются при первом запуске в `ComfyUI\models\insightface\models\buffalo_l`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
- inswapper_128: скачивается при установке в `ComfyUI\models\insightface`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models скачать в `ComfyUI\models\reswapper`
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) скачать в `ComfyUI\models\hyperswap`
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models скачать любые предпочитаемые в `ComfyUI\models\facerestore_models`
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt скачать в `ComfyUI\models\ultralytics\bbox`
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams скачать в `ComfyUI\models\sams`
<a name="usage">
@@ -260,25 +286,30 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
Вы можете найти ноды ReActor внутри меню `ReActor` или через поиск (достаточно ввести "ReActor" в поисковой строке)
Список нодов:
- ••• Main Nodes •••
- ReActorFaceSwap (Основной нод)
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
- ReActorOptions (Опции для ReActorFaceSwapOpt)
- ReActorFaceBoost (Нод Face Booster)
- ReActorMaskHelper (Masking Helper)
- ••• Operations with Face Models •••
- ReActorSaveFaceModel (Save Face Model)
- ReActorLoadFaceModel (Load Face Model)
- ReActorBuildFaceModel (Build Blended Face Model)
- ReActorMakeFaceModelBatch (Make Face Model Batch)
- ••• Additional Nodes •••
- ReActorRestoreFace (Face Restoration)
- ReActorImageDublicator (Dublicate one Image to Images List)
- ImageRGBA2RGB (Convert RGBA to RGB)
- ••• Основные •••
- ReActorFaceSwap (Основной нод)
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
- ReActorOptions (Опции для ReActorFaceSwapOpt)
- ReActorFaceBoost (Face Booster)
- ReActorMaskHelper (Masking Helper)
- ReActorSetWeight (Задать замены лица)
- ••• Работа с моделями лиц •••
- ReActorSaveFaceModel (Сохранить модель лица)
- ReActorLoadFaceModel (Загрузить модель лица)
- ReActorBuildFaceModel (Построить смешанную модель лица)
- ReActorMakeFaceModelBatch (Создать пачку моделей лиц)
- ••• Дополнительные •••
- ReActorRestoreFace (Восстановление лиц)
- ReActorRestoreFaceAdvanced (Восстановление лиц продвинутое)
- ReActorFaceSimilarity (Оценка схожести лиц)
- ReActorImageDublicator (Сделать из одного изображения несколько дубликатов)
- ImageRGBA2RGB (Конвертировать RGBA в RGB)
- ReActorUnload (Выгрузить модели РеАктора из VRAM)
- DLSS5FrameEnhancer (Улучшение детализации кадра с NVIDIA DLSS 5)
Соедините все необходимые слоты (slots) и запустите очередь (query).
### Входы основного Нода
### Входы основного Узла
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
@@ -286,13 +317,18 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
- Поддерживаемые ноды: "Load Image" или любые другие ноды с выходом Image(s);
- `face_model` - это вход для выхода с нода "Load Face Model" или другого нода ReActor для загрузки модели лица (face model или face embedding), которое вы создали ранее через нод "Save Face Model";
- Поддерживаемые ноды: "Load Face Model", "Build Blended Face Model";
- `options` - для соединения с ReActorOptions;
- Поддерживаемые ноды: "ReActorOptions";
- `face_boost` - для соединения с ReActorFaceBoost;
- Поддерживаемые ноды: "ReActorFaceBoost";
### Выходы основного Нода
### Выходы основного Узла
- `IMAGE` - выход с готовым изображением (результатом);
- Поддерживаемые ноды: любые ноды с изображением на входе;
- `FACE_MODEL` - выход, предоставляющий модель лица, построенную в ходе замены;
- Поддерживаемые ноды: "Save Face Model", "ReActor", "Make Face Model Batch";
- `ORIGINAL_IMAGE` - `input_image` байпас;
### Восстановление лиц
@@ -320,61 +356,47 @@ ReActor заменит только то лицо, которое удовлет
Чтобы новые модели появились в списке моделей нода "Load Face Model" - обновите страницу of с ComfyUI.<br>
(Рекомендую использовать ComfyUI Manager - иначе ваше воркфлоу может быть потеряно после перезагрузки страницы, если вы не сохранили его).
### Masking Helper
Нод доступен с версии 0.5.0, просто добавьте "ReActorMaskHelper" в рабочий процесс и соедините как показано ниже:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-01.jpg?raw=true" alt="0.5.0-whatsnew-01" width="100%"/>
Если модель "face_yolov8m.pt" отсутствует - скачайте [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положите в папку "ComfyUI\models\ultralytics\bbox"
<br>
Также и ["sam_vit_b_01ec64.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_b_01ec64.pth) или ["sam_vit_l_0b3195.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_l_0b3195.pth) (лучше окклюзия) - скачайте (если не качали ранее) и положите в папку "ComfyUI\models\sams";
Используйте этот нод для улучшенного результата при замене лиц:
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-02.jpg?raw=true" alt="0.5.0-whatsnew-02" width="100%"/>
### Сила замены лица
Для входов `source_image` or `face_model` можно задать силу замены лица от 0% до 100% (с шагом 12.5%) с помощью нода `ReActorSetWeight`
<center>
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
</center>
<a name="troubleshooting">
## Устранение проблем
<a name="insightfacebuild">
### **I. (Для пользователей Windows) Если вы до сих пор не можете установить пакет Insightface по каким-то причинам или же просто не желаете устанавливать Visual Studio или VS C++ Build Tools - сделайте следующее:**
1. (ComfyUI Portable) Находясь в корневой директории, проверьте версию Python:<br>запустите CMD и выполните `python_embeded\python.exe -V`<br>Вы должны увидеть версию или 3.10, или 3.11, или 3.12
2. Скачайте готовый пакет Insightface [для версии 3.10](https://github.com/Gourieff/sd-webui-reactor/raw/main/example/insightface-0.7.3-cp310-cp310-win_amd64.whl) или [для 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (если на предыдущем шаге вы увидели 3.11) или [для 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (если на предыдущем шаге вы увидели 3.12) и сохраните его в корневую директорию stable-diffusion-webui (A1111 или SD.Next) - туда, где лежит файл "webui-user.bat" -ИЛИ- в корневую директорию ComfyUI, если вы используете ComfyUI Portable
3. Из корневой директории запустите:
- (SD WebUI) CMD и `.\venv\Scripts\activate`
- (ComfyUI Portable) CMD
4. Обновите PIP:
- (SD WebUI) `python -m pip install -U pip`
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
5. Затем установите Insightface:
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
6. Готово!
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
Скачайте вручную по ссылке [отсюда](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx)
Скачайте вручную по ссылке [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
### **IV. ControlNet Aux Node IMPORT failed - при использовании совместно с нодом ReActor**
1. Закройте или остановите ComfyUI сервер, если он запущен
2. Перейдите в корневую папку ComfyUI, откройте консоль CMD и выполните следующее:
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
3. Готово!
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
### **V. "ModuleNotFoundError: No module named 'basicsr'" или "subprocess-exited-with-error" при установке пакета future-0.18.3**
- Скачайте https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
- Скопируйте файл в корневую папку ComfyUI и выполните в консоли:
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
- Затем:
python_embeded\python.exe -m pip install basicsr
### **VI. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
### **III. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
@@ -477,8 +499,8 @@ SHA256:4c06341c33c2ca1f86781dab0e829f88ad5b64be9fba56e56bc9ebdefc619e43
| [codeformer-v0.1.0.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/codeformer-v0.1.0.pth) | [sczhou](https://github.com/sczhou/CodeFormer) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [GFPGANv1.3.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.3.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) | ![license](https://img.shields.io/badge/license-Apache_2.0-green.svg) |
| [GFPGANv1.4.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.4.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) | ![license](https://img.shields.io/badge/license-Apache_2.0-green.svg) |
| [inswapper_128.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128_fp16.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
| [inswapper_128_fp16.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) | ![license](https://img.shields.io/badge/license-non_commercial-red) |
[BasicSR](https://github.com/XPixelGroup/BasicSR) - [@XPixelGroup](https://github.com/XPixelGroup) <br>
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
+3 -31
View File
@@ -1,39 +1,11 @@
import sys
import os
# Добавляем путь расширения, чтобы Питон видел наши папки (r_facelib, scripts и т.д.)
repo_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, repo_dir)
original_modules = sys.modules.copy()
if repo_dir not in sys.path:
sys.path.insert(0, repo_dir)
# Place aside existing modules if using a1111 web ui
modules_used = [
"modules",
"modules.images",
"modules.processing",
"modules.scripts_postprocessing",
"modules.scripts",
"modules.shared",
]
original_webui_modules = {}
for module in modules_used:
if module in sys.modules:
original_webui_modules[module] = sys.modules.pop(module)
# Proceed with node setup
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Clean up imports
# Remove repo directory from path
sys.path.remove(repo_dir)
# Remove any new modules
modules_to_remove = []
for module in sys.modules:
if module not in original_modules and not module.startswith("google.protobuf") and not module.startswith("onnx") and not module.startswith("cv2"):
modules_to_remove.append(module)
for module in modules_to_remove:
del sys.modules[module]
# Restore original modules
sys.modules.update(original_webui_modules)
+10 -6
View File
@@ -1,22 +1,25 @@
@echo off
setlocal enabledelayedexpansion
:: Try to use embedded python first
if exist ..\..\..\python_embeded\python.exe (
:: Use the embedded python
:: Try to use the Desktop version's venv python first
if exist ..\..\.venv\Scripts\python.exe (
:: Use the ComfyUI Desktop venv python
set PYTHON=..\..\.venv\Scripts\python.exe
) else if exist ..\..\..\python_embeded\python.exe (
:: Use the embedded python (portable version)
set PYTHON=..\..\..\python_embeded\python.exe
) else (
:: Embedded python not found, check for python in the PATH
:: Neither found, check for python in the PATH
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
set PYTHON_VERSION=%%F
)
if errorlevel 1 (
echo I couldn't find an embedded version of Python, nor one in the Windows PATH. Please install manually.
echo I couldn't find a venv python ^(Desktop^), an embedded python ^(Portable^), nor one in the Windows PATH. Please install manually.
pause
exit /b 1
) else (
:: Use python from the PATH (if it's the right version and the user agrees)
echo I couldn't find an embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
echo I couldn't find a venv or embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
echo Would you like to proceed with the install using that version? (Y/N^)
set /p USE_PYTHON=
if /i "!USE_PYTHON!"=="Y" (
@@ -30,6 +33,7 @@ if exist ..\..\..\python_embeded\python.exe (
)
:: Install the package
echo Using Python: %PYTHON%
echo Installing...
%PYTHON% install.py
echo Done^!
+615 -206
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,13 +1,13 @@
[project]
name = "comfyui-reactor"
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension Node for ComfyUI, based on ReActor SD-WebUI Face Swap Extension"
version = "0.6.0"
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
version = "0.7.1-b3"
license = { file = "LICENSE" }
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.3", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
dependencies = ["onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
[project.urls]
Repository = "https://github.com/Gourieff/ComfyUI-ReActor"
# Used by Comfy Registry https://comfyregistry.org
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "gourieff"
+8
View File
@@ -0,0 +1,8 @@
## Third-party DLLs
1. Download [neuroframe_dlls.zip](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/neuroframe_dlls.zip) and place `neuroframe_caller.dll`\* and `neuroframe_engine.dll`* here (in `r_dlssnr/dll`)
2. Place your `nvngx_dlssnr.dll`** here (in `r_dlssnr/dll`)
<sub>* Author [Merserk](https://github.com/Merserk), [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-Merserk.txt)
<br>
** Public distribution of this file is prohibited by NVIDIA, [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-NVIDIA-DLSS.txt)</sub>
+147
View File
@@ -0,0 +1,147 @@
import ctypes
import threading
from dataclasses import dataclass
from typing import Any
import numpy as np
# --- КОНСТАНТЫ ---
BRIDGE_ABI_VERSION = 6
MEMORY_HOST = 0
MEMORY_CUDA = 1
MEMORY_NONE = 2
class NeuralBridgeError(Exception):
pass
# --- C-СТРУКТУРЫ ДЛЯ ВЗАИМОДЕЙСТВИЯ С DLL ---
class RenderParameters(ctypes.Structure):
_fields_ = [
("struct_size", ctypes.c_uint32),
("abi_version", ctypes.c_uint32),
("style", ctypes.c_int32),
("intensity", ctypes.c_float),
("tone", ctypes.c_float),
("structure", ctypes.c_float),
("skin", ctypes.c_float),
("automask", ctypes.c_int32),
("reset", ctypes.c_int32),
("color_strength", ctypes.c_float),
("tone_preservation", ctypes.c_float),
("mask_memory_type", ctypes.c_uint32),
("mask_width", ctypes.c_uint32),
("mask_height", ctypes.c_uint32),
("mask_stride", ctypes.c_uint32),
("mask_plane", ctypes.c_uint64), # Из-за uint64 здесь будет 4 байта системного отступа
("face_skin_protection", ctypes.c_float),
("grain_preservation", ctypes.c_float),
("nr_passes", ctypes.c_int32),
("shimmer_suppression", ctypes.c_float),
("prefer_nvof", ctypes.c_int32),
]
class DLSSStandaloneManager:
def __init__(self, dll_dir: str):
self._lock = threading.RLock()
self._library = None
self.dll_dir = dll_dir
def initialize(self, ordinal: int):
with self._lock:
if self._library is not None:
return True
import os
if hasattr(os, 'add_dll_directory'):
os.add_dll_directory(self.dll_dir)
engine_path = os.path.join(self.dll_dir, "neuroframe_engine.dll")
if not os.path.exists(engine_path):
raise NeuralBridgeError(f"Missing DLL: {engine_path}")
loader = getattr(ctypes, "WinDLL", ctypes.CDLL)
try:
self._library = loader(engine_path)
except OSError as exc:
raise NeuralBridgeError(f"DLL load failed: {exc}")
# Сигнатура инициализации
self._library.dlss5nr_init.argtypes = [
ctypes.c_int, ctypes.c_wchar_p, ctypes.c_char_p, ctypes.c_int
]
self._library.dlss5nr_init.restype = ctypes.c_int
# Сигнатура HOST-рендера (process_v6 вместо process_cuda_v6)
c_float_p = ctypes.POINTER(ctypes.c_float)
self._library.dlss5nr_process_v6.argtypes = [
c_float_p, c_float_p, ctypes.c_int, ctypes.c_int,
ctypes.POINTER(RenderParameters), ctypes.c_char_p, ctypes.c_int
]
self._library.dlss5nr_process_v6.restype = ctypes.c_int
try:
self._library.dlss5nr_frame_abi_version.argtypes = []
self._library.dlss5nr_frame_abi_version.restype = ctypes.c_uint32
self.actual_abi = self._library.dlss5nr_frame_abi_version()
except Exception:
self.actual_abi = BRIDGE_ABI_VERSION
error = ctypes.create_string_buffer(4096)
ok = self._library.dlss5nr_init(ordinal, self.dll_dir, error, len(error))
if not ok:
err_msg = error.value.decode('utf-8', errors='ignore')
raise NeuralBridgeError(f"Bridge initialization failed: {err_msg}")
return True
def process_host(self, source: np.ndarray, destination: np.ndarray, settings: dict, reset: bool, mask: np.ndarray = None):
with self._lock:
error = ctypes.create_string_buffer(4096)
params = RenderParameters()
params.struct_size = ctypes.sizeof(RenderParameters)
params.abi_version = getattr(self, "actual_abi", BRIDGE_ABI_VERSION)
params.style = int(settings.get("style"))
params.intensity = float(settings.get("intensity"))
params.tone = float(settings.get("local_tone"))
params.structure = float(settings.get("local_structure"))
params.skin = float(settings.get("skin_structure"))
params.automask = int(bool(settings.get("auto_mask")))
params.reset = int(bool(reset))
params.color_strength = float(settings.get("color_strength"))
params.tone_preservation = float(settings.get("tone_preservation"))
params.face_skin_protection = float(settings.get("face_skin_protection"))
params.grain_preservation = float(settings.get("grain_preservation"))
params.nr_passes = int(settings.get("nr_passes"))
params.shimmer_suppression = float(settings.get("shimmer_suppression", 0.0))
params.prefer_nvof = int(bool(settings.get("prefer_nvof", False)))
# Обработка маски через HOST память
params.mask_memory_type = MEMORY_NONE
if mask is not None:
params.mask_memory_type = MEMORY_HOST
params.mask_width = int(mask.shape[1])
params.mask_height = int(mask.shape[0])
params.mask_stride = int(mask.strides[0])
params.mask_plane = int(mask.ctypes.data) # Передаем указатель RAM
c_float_p = ctypes.POINTER(ctypes.c_float)
# Вызываем HOST функцию (DLL сама разберется с видеокартой)
ok = self._library.dlss5nr_process_v6(
source.ctypes.data_as(c_float_p),
destination.ctypes.data_as(c_float_p),
source.shape[1],
source.shape[0],
ctypes.byref(params),
error,
len(error)
)
if not ok:
err_msg = error.value.decode('utf-8', errors='ignore')
raise NeuralBridgeError(f"DLSS-5 process failed: {err_msg}")
+113
View File
@@ -0,0 +1,113 @@
import torch
import numpy as np
import os
import comfy.model_management as model_management
from scripts.reactor_logger import logger
from .dlss5_core import DLSSStandaloneManager
from r_modules.shared import state
from reactor_utils import (
batch_tensor_to_pil,
progress_bar,
progress_bar_reset
)
class DLSS5FrameEnhancer:
def __init__(self):
self.device = model_management.get_torch_device()
self.manager = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"style": (["Default", "Nature", "Cinematic"],),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
"local_tone": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 0.0"}),
"local_structure": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
"skin_structure": ("FLOAT", {"default": 0.5, "min": -1.0, "max": 2.0, "step": 0.05, "tooltip": "-1..2, def: 0.5"}),
"color_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
"tone_preservation": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
"face_skin_protection": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
"grain_preservation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
"nr_passes": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "0..4, def: 1"}),
"auto_mask": ("BOOLEAN", {"default": False, "label_off": "OFF", "label_on": "ON", "tooltip": "AI Automatic masking of complex areas to prevent over-detailing"}),
},
"optional": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("enhanced_image",)
FUNCTION = "enhance"
CATEGORY = "🌌 ReActor"
DESCRIPTION = (
"Requirements:\n"
"- NVIDIA display driver >= 616.x\n"
"- NVIDIA RTX 40/50-series GPU\n"
"(compatibility with older RTX series is unconfirmed)\n"
"- DLLs: neuroframe_caller.dll, neuroframe_engine.dll, nvngx_dlssnr.dll\nin custom_nodes/ComfyUI-ReActor/r_dlssnr/dll\nor custom_nodes/comfyui-reactor-node/r_dlssnr/dll\n"
"(see README.md in dll folder for instructions)"
)
def load_bridge(self):
if self.manager is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
dll_dir = os.path.join(current_dir, "dll")
self.manager = DLSSStandaloneManager(dll_dir)
ordinal = getattr(self.device, 'index', 0) if self.device.index is not None else 0
self.manager.initialize(ordinal)
logger.status(f"DLSS-5 Bridge initialized on GPU {ordinal}")
def enhance(self, image, style, intensity, local_tone, local_structure,
skin_structure, color_strength, tone_preservation,
face_skin_protection, grain_preservation, nr_passes, auto_mask, mask=None):
self.load_bridge()
style_map = {"Default": 0, "Nature": 1, "Cinematic": 2}
settings = {
"style": style_map[style], "intensity": intensity, "local_tone": local_tone,
"local_structure": local_structure, "skin_structure": skin_structure,
"color_strength": color_strength, "tone_preservation": tone_preservation,
"face_skin_protection": face_skin_protection, "grain_preservation": grain_preservation,
"nr_passes": nr_passes, "auto_mask": auto_mask,
"shimmer_suppression": 0.0, "prefer_nvof": False
}
enhanced_batch = []
pil_images = batch_tensor_to_pil(image)
pbar = progress_bar(len(pil_images))
for i in range(len(image)):
if state.interrupted or model_management.processing_interrupted():
logger.status("Interrupted by User")
break
img_np = np.ascontiguousarray(image[i].cpu().numpy().astype(np.float32))
dest_np = np.ascontiguousarray(np.zeros_like(img_np))
mask_np = None
if mask is not None:
mask_np = np.ascontiguousarray(mask[i].cpu().numpy().astype(np.float32))
# Вызываем Host-обработчик (без CUDA конфликтов)
self.manager.process_host(
source=img_np,
destination=dest_np,
settings=settings,
reset=True,
mask=mask_np
)
out_tensor = torch.from_numpy(dest_np).to(self.device)
enhanced_batch.append(out_tensor)
pbar.update(1)
progress_bar_reset(pbar)
return (torch.stack(enhanced_batch),)
+1 -1
View File
@@ -6,7 +6,7 @@ import torch.nn.functional as F
from PIL import Image
from torchvision.models._utils import IntermediateLayerGetter as IntermediateLayerGetter
from modules import shared
from r_modules import shared
from r_facelib.detection.align_trans import get_reference_facial_points, warp_and_crop_face
from r_facelib.detection.retinaface.retinaface_net import FPN, SSH, MobileNetV1, make_bbox_head, make_class_head, make_landmark_head
@@ -1,13 +1,15 @@
class StableDiffusionProcessing:
class Processing:
def __init__(self, init_imgs):
self.init_images = init_imgs
self.width = init_imgs[0].width
self.height = init_imgs[0].height
self.extra_generation_params = {}
self.bbox = []
self.swapped_indexes = []
class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
class ProcessingImg2Img(Processing):
def __init__(self, init_img):
super().__init__(init_img)
View File
+103
View File
@@ -0,0 +1,103 @@
import os
import zipfile
from reactor_utils import download # Твоя функция скачивания из utils
from .face_objects import Face
from .inswap import SCRFD, ArcFaceONNX, Attribute, Landmark
from scripts.reactor_logger import logger
class ReActorFaceAnalysis:
"""
Главный класс-оркестратор.
Берет картинку, находит лица, определяет пол/возраст и вычисляет эмбеддинги.
"""
def __init__(self, name="buffalo_l", root="./models/insightface", providers=None):
self.name = name
self.root = root
self.providers = providers or ["CPUExecutionProvider"]
self.models = {}
model_dir = os.path.join(root, "models", name)
os.makedirs(model_dir, exist_ok=True)
det_file = os.path.join(model_dir, "det_10g.onnx")
rec_file = os.path.join(model_dir, "w600k_r50.onnx")
attr_file = os.path.join(model_dir, "genderage.onnx")
lmk2d_file = os.path.join(model_dir, "2d106det.onnx")
lmk3d_file = os.path.join(model_dir, "1k3d68.onnx")
# Если файлов нет - качаем архив
if not (os.path.exists(det_file) and os.path.exists(rec_file) and os.path.exists(attr_file)):
zip_url = "https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/buffalo_l.zip"
zip_path = os.path.join(model_dir, f"{name}.zip")
logger.status(f"Downloading {name} models archive...")
download(zip_url, zip_path, f"{name}.zip")
logger.status(f"Extracting {name} models...")
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(model_dir)
logger.status("Extraction completed!")
except zipfile.BadZipFile:
logger.error("Downloaded zip file is corrupted. Please try again.")
finally:
# В любом случае пытаемся удалить архив, чтобы не занимать место
if os.path.exists(zip_path):
os.remove(zip_path)
# Инициализируем только те модели, которые физически есть в папке
if os.path.exists(det_file):
self.models["detection"] = SCRFD(det_file, providers=self.providers)
if os.path.exists(rec_file):
self.models["recognition"] = ArcFaceONNX(rec_file, providers=self.providers)
if os.path.exists(attr_file):
self.models["attribute"] = Attribute(attr_file, providers=self.providers)
if os.path.exists(lmk2d_file):
self.models["landmark_2d"] = Landmark(lmk2d_file, providers=self.providers)
if os.path.exists(lmk3d_file):
self.models["landmark_3d"] = Landmark(lmk3d_file, providers=self.providers)
if "detection" not in self.models:
raise FileNotFoundError(
f"Detection model (det_10g.onnx) not found at {det_file}. "
"Please ensure the buffalo_l models are downloaded and extracted properly."
)
def prepare(self, ctx_id=0, det_size=(640, 640), det_thresh=0.5):
self.det_size = det_size
self.det_thresh = det_thresh
def get(self, img, max_num=0):
bboxes, kpss = self.models["detection"].detect(
img,
det_thresh=self.det_thresh,
input_size=self.det_size,
max_num=max_num
)
if bboxes.shape[0] == 0:
return []
ret = []
for i in range(bboxes.shape[0]):
bbox = bboxes[i, 0:4]
det_score = bboxes[i, 4]
kps = kpss[i] if kpss is not None else None
face = Face(bbox=bbox, kps=kps, det_score=det_score)
if "attribute" in self.models:
self.models["attribute"].get(img, face)
if "recognition" in self.models:
self.models["recognition"].get(img, face)
if "landmark_2d" in self.models:
self.models["landmark_2d"].get(img, face)
if "landmark_3d" in self.models:
self.models["landmark_3d"].get(img, face)
ret.append(face)
return ret
+82
View File
@@ -0,0 +1,82 @@
import numpy as np
import onnxruntime as ort
import os
class Face(dict):
"""
Класс-хранилище данных о лице.
Наследуется от dict для полной обратной совместимости с кодом,
который ожидает доступ по ключу (например, face['bbox']).
"""
def __init__(self, d=None, **kwargs):
if d is None:
d = {}
if kwargs:
d.update(**kwargs)
for k, v in d.items():
setattr(self, k, v)
# Инициализируем родительский словарь
super().__init__(d)
def __setattr__(self, name, value):
# Если это массив, делаем копию, чтобы избежать багов с мутацией по ссылке
if isinstance(value, (list, tuple)):
value = [x for x in value]
elif isinstance(value, np.ndarray):
value = value.copy()
super().__setattr__(name, value)
super().__setitem__(name, value)
def __setitem__(self, key, value):
super().__setitem__(key, value)
super().__setattr__(key, value)
@property
def sex(self):
"""Возвращает 'M' или 'F' на основе числового значения gender"""
gender = self.get('gender', None)
if gender is None:
return None
return 'M' if gender == 1 else 'F'
@property
def normed_embedding(self):
"""Автоматически нормализует эмбеддинг для ArcFace / INSwapper"""
embedding = self.get('embedding', None)
if embedding is None:
return None
norm = np.linalg.norm(embedding)
if norm == 0:
return embedding
return embedding / norm
class BaseONNXModel:
"""
Базовый класс для всех моделей (Детектор, ArcFace, INSwapper).
Берет на себя рутину по открытию сессий и чтению входов/выходов.
"""
def __init__(self, model_file, providers=None):
self.model_file = model_file
self.providers = providers or ["CPUExecutionProvider"]
if not os.path.exists(self.model_file):
raise FileNotFoundError(f"Model file not found: {self.model_file}")
self.session = ort.InferenceSession(self.model_file, providers=self.providers)
# Получаем параметры входов
self.inputs = self.session.get_inputs()
self.input_names = [inp.name for inp in self.inputs]
# Обычно нас интересует шейп первого входа (например, батч, каналы, высота, ширина)
self.input_shape = self.inputs[0].shape
# Получаем параметры выходов
self.outputs = self.session.get_outputs()
self.output_names = [out.name for out in self.outputs]
def forward(self, *args, **kwargs):
"""Этот метод будет переопределен в классах-наследниках"""
raise NotImplementedError("Forward method must be implemented by subclasses.")
+181
View File
@@ -0,0 +1,181 @@
import cv2
import numpy as np
from .face_objects import BaseONNXModel
class HyperSwapper(BaseONNXModel):
"""Класс для работы с моделями семейства Hyperswap"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
# Функция для получения 5 ключевых точек из объекта Face
def get_landmarks_5(self, face):
if hasattr(face, 'landmark_5') and face.landmark_5 is not None:
return face.landmark_5
elif hasattr(face, 'kps') and face.kps is not None:
return face.kps
elif hasattr(face, 'landmark') and face.landmark is not None:
if face.landmark.shape[0] >= 68:
idxs = [36, 45, 30, 48, 54]
return face.landmark[idxs]
return None
# Функция для вычисления аффинного преобразования
def get_affine_transform(self, src_pts, dst_pts):
M, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
return M
# Создаём градиентную маску овальной формы без обрезки
def create_gradient_mask(self, crop_size=256):
# 1. Создаём пустую маску (все пиксели = 0)
mask = np.zeros((crop_size, crop_size), dtype=np.float32)
# 2. Определяем центр и размеры эллипса
center = (crop_size // 2, crop_size // 2)
axes = (int(crop_size * 0.35), int(crop_size * 0.4))
# 3. Рисуем эллипс (заполняем белым цветом, значение=1.0)
cv2.ellipse(
mask, # Массив для рисования
center, # Центр эллипса
axes, # Полуоси (ширина, высота)
angle=0, # Угол поворота
startAngle=0, # Начальный угол дуги
endAngle=360, # Конечный угол дуги (360 = полный эллипс)
color=1.0, # Значение для заполнения (белый = 1.0)
thickness=-1 # -1 = заполнить всю область эллипса
)
# 4. Применяем размытие для плавных краёв
blur_ksize = 15 # Нечётное число, чтобы ядро было симметричным
mask = cv2.GaussianBlur(mask, (blur_ksize, blur_ksize), 0)
# 5. Ограничим значения в диапазоне [0, 1]
mask = np.clip(mask, 0, 1)
return mask
def paste_back(self, target_img, swapped_face, M, crop_size=256):
# 1. Создание мягкой маски (Эрозия + Размытие)
mask = self.create_gradient_mask(crop_size)
# Преобразуем в трехканальную маску
mask_3c = np.stack([mask] * 3, axis=2)
# 2. Получаем размеры целевого изображения
h, w = target_img.shape[:2]
# 3. Нормализация swapped_face к float32 [0,1] для warp
swapped_face_norm = swapped_face.astype(np.float32) / 255.0
mask_norm = mask_3c.astype(np.float32) # Маска уже [0,1]
# 4. Обратное преобразование (WARP_INVERSE_MAP) для лица И маски
# Используем BORDER_CONSTANT с borderValue=0.5 (серый, чтобы избежать синих/зеленых артефактов)
warped_face = cv2.warpAffine(
swapped_face_norm,
M,
(w, h),
flags=cv2.INTER_LANCZOS4 | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0.5
)
# Для маски (INTER_CUBIC — плавные границы)
warped_mask = cv2.warpAffine(
mask_norm,
M,
(w, h),
flags=cv2.INTER_CUBIC | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0.0 # Маска: 0 за пределами
)
# 5. Обработка после warp: Clip, NaN fix
warped_face = np.clip(warped_face, 0, 1) # Убираем отрицательные
warped_face = np.nan_to_num(warped_face, nan=0.5) # NaN -> серый
warped_mask = np.clip(warped_mask, 0, 1)
warped_mask = np.nan_to_num(warped_mask, nan=0.0)
# 6. Дополнительное размытие для устранения артефактов
warped_mask = cv2.GaussianBlur(warped_mask, (3, 3), 0)
# 7. Плавное наложение в float32
target_float = target_img.astype(np.float32) / 255.0
result_float = target_float * (1.0 - warped_mask) + warped_face * warped_mask
# 8. Обратная нормализация к uint8
result = (result_float * 255).clip(0, 255).astype(np.uint8)
return result
def visualize_points(self, img, points, color=(0, 255, 0)):
img = img.copy()
for p in points:
cv2.circle(img, tuple(p.astype(int)), 3, color, -1)
# Итоговая функция run_hyperswap (get) с аффинным преобразованием
def get(self, img, target_face, source_face, paste_back=True):
# 1. Подготовка эмбеддинга
source_embedding = source_face.normed_embedding.reshape(1, -1).astype(np.float32)
# 2. Получаем 5 точек target
target_landmarks_5 = self.get_landmarks_5(target_face)
# self.visualize_points(img, target_landmarks_5, (0, 255, 0)) # не для продакшена
if target_landmarks_5 is None:
return img if paste_back else (None, None)
# 3. Определение эталонных точек для выравнивания 256x256 (FFHQ Alignment)
std_landmarks_256 = np.array([
[ 84.87, 105.94], # Левый глаз
[171.13, 105.94], # Правый глаз
[128.00, 146.66], # Кончик носа
[ 96.95, 188.64], # Левый уголок рта
[159.05, 188.64] # Правый уголок рта
], dtype=np.float32)
# Вычисляем аффинную матрицу
M = self.get_affine_transform(target_landmarks_5.astype(np.float32), std_landmarks_256)
# Применяем аффинное преобразование с новой матрицей M
crop = cv2.warpAffine(img, M, (256, 256), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
# 4. Преобразуем crop для модели
crop_input = crop[:, :, ::-1].astype(np.float32) / 255.0 # RGB -> [0,1]
crop_input = (crop_input - 0.5) / 0.5 # Нормализация
crop_input = crop_input.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32)
# 5. Инференс
try:
output = self.session.run(None, {'source': source_embedding, 'target': crop_input})[0][0]
except:
return img if paste_back else (None, None)
if isinstance(output, np.ndarray):
# устранение NaN и бесконечностей
output = np.nan_to_num(output, nan=0.0, posinf=255.0, neginf=0.0)
# если диапазон похож на [-1,1] → нормализуем в [0,255]
if output.min() < 0.0 or output.max() <= 1.5:
output = ((output + 1.0) / 2.0 * 255.0)
# жёсткое ограничение диапазона и тип для OpenCV
output = np.clip(output, 0, 255).astype(np.uint8).copy()
# защита от повторного использования буфера (inplace CPU bug)
try:
output.setflags(write=True)
except Exception:
pass
# 6. Обратная нормализация
output = output.transpose(1, 2, 0) # CHW -> HWC
output = output[:, :, ::-1] # BGR -> RGB
# 7. Возвращаем результат в зависимости от флага paste_back
if not paste_back:
return output, M # Возвращаем только кроп лица (256x256) и матрицу M
# Если нужна полная вклейка в исходное изображение:
return self.paste_back(img, output, M, crop_size=256)
+440
View File
@@ -0,0 +1,440 @@
import cv2
import numpy as np
from .face_objects import BaseONNXModel
from .meanshape_68 import MEANSHAPE_68
# --- Математика для 3D позы ---
def estimate_affine_matrix_3d23d(X, Y):
''' Вычисляет аффинную матрицу трансформации 3D -> 3D '''
X_homo = np.hstack((X, np.ones([X.shape[0], 1])))
P = np.linalg.lstsq(X_homo, Y, rcond=None)[0].T
return P
def P2sRt(P):
''' Разбивает матрицу проекции '''
t1 = np.linalg.norm(P[:,0])
t2 = np.linalg.norm(P[:,1])
t3 = np.linalg.norm(P[:,2])
s = (t1 + t2 + t3) / 3.0
P1 = P / s
R = P1[:, 0:3]
t = P1[:, 3]
return s, R, t
def matrix2angle(R):
''' Превращает матрицу поворота в углы Эйлера (pitch, yaw, roll) '''
if R[2,0] != 1 and R[2,0] != -1:
pitch = -np.arcsin(R[2,0])
yaw = np.arctan2(R[2,1]/np.cos(pitch), R[2,2]/np.cos(pitch))
roll = np.arctan2(R[1,0]/np.cos(pitch), R[0,0]/np.cos(pitch))
else:
yaw = 0
if R[2,0] == -1:
pitch = np.pi/2
roll = yaw + np.arctan2(R[0,1], R[0,2])
else:
pitch = -np.pi/2
roll = -yaw + np.arctan2(-R[0,1], -R[0,2])
return pitch, yaw, roll
# --- Вспомогательные функции ---
def distance2bbox(points, distance, max_shape=None):
x1 = points[:, 0] - distance[:, 0]
y1 = points[:, 1] - distance[:, 1]
x2 = points[:, 0] + distance[:, 2]
y2 = points[:, 1] + distance[:, 3]
if max_shape is not None:
x1 = np.clip(x1, 0, max_shape[1])
y1 = np.clip(y1, 0, max_shape[0])
x2 = np.clip(x2, 0, max_shape[1])
y2 = np.clip(y2, 0, max_shape[0])
return np.stack([x1, y1, x2, y2], axis=-1)
def distance2kps(points, distance, max_shape=None):
preds = []
for i in range(0, distance.shape[1], 2):
px = points[:, i%2] + distance[:, i]
py = points[:, i%2+1] + distance[:, i+1]
if max_shape is not None:
px = np.clip(px, 0, max_shape[1])
py = np.clip(py, 0, max_shape[0])
preds.append(px)
preds.append(py)
return np.stack(preds, axis=-1)
# Стандартные точки FFHQ/ArcFace для матрицы трансформации 112x112
ARCFACE_STD_POINTS = np.array([
[38.2946, 51.6963],
[73.5318, 51.5014],
[56.0252, 71.7366],
[41.5493, 92.3655],
[70.7299, 92.2041]
], dtype=np.float32)
def norm_crop(img, landmark, image_size=112):
"""Выравнивает и обрезает лицо (для ArcFace)"""
M, _ = cv2.estimateAffinePartial2D(landmark, ARCFACE_STD_POINTS)
warped = cv2.warpAffine(img, M, (image_size, image_size), borderValue=0.0)
return warped
# --- Модели ---
class SCRFD(BaseONNXModel):
"""Детектор лиц (находит bbox и 5 ключевых точек)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.batched = len(self.outputs[0].shape) == 3
self.input_mean = 127.5
self.input_std = 128.0
self.use_kps = len(self.outputs) in [9, 15]
self.fmc = 5 if len(self.outputs) in [10, 15] else 3
self._feat_stride_fpn = [8, 16, 32, 64, 128][:self.fmc]
self._num_anchors = 2 if self.fmc == 3 else 1
self.center_cache = {}
def forward(self, img, threshold):
scores_list, bboxes_list, kpss_list = [], [], []
input_size = tuple(img.shape[0:2][::-1])
blob = cv2.dnn.blobFromImage(img, 1.0/self.input_std, input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
net_outs = self.session.run(self.output_names, {self.input_names[0]: blob})
input_height, input_width = blob.shape[2], blob.shape[3]
for idx, stride in enumerate(self._feat_stride_fpn):
if self.batched:
scores = net_outs[idx][0]
bbox_preds = net_outs[idx + self.fmc][0] * stride
if self.use_kps:
kps_preds = net_outs[idx + self.fmc * 2][0] * stride
else:
scores = net_outs[idx]
bbox_preds = net_outs[idx + self.fmc] * stride
if self.use_kps:
kps_preds = net_outs[idx + self.fmc * 2] * stride
height, width = input_height // stride, input_width // stride
key = (height, width, stride)
if key in self.center_cache:
anchor_centers = self.center_cache[key]
else:
anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
anchor_centers = (anchor_centers * stride).reshape((-1, 2))
if self._num_anchors > 1:
anchor_centers = np.stack([anchor_centers] * self._num_anchors, axis=1).reshape((-1, 2))
if len(self.center_cache) < 100:
self.center_cache[key] = anchor_centers
pos_inds = np.where(scores >= threshold)[0]
bboxes = distance2bbox(anchor_centers, bbox_preds)
scores_list.append(scores[pos_inds])
bboxes_list.append(bboxes[pos_inds])
if self.use_kps:
kpss = distance2kps(anchor_centers, kps_preds)
kpss = kpss.reshape((kpss.shape[0], -1, 2))
kpss_list.append(kpss[pos_inds])
return scores_list, bboxes_list, kpss_list
def detect(self, img, det_thresh=0.5, input_size=(640, 640), max_num=0):
im_ratio = float(img.shape[0]) / img.shape[1]
model_ratio = float(input_size[1]) / input_size[0]
if im_ratio > model_ratio:
new_height = input_size[1]
new_width = int(new_height / im_ratio)
else:
new_width = input_size[0]
new_height = int(new_width * im_ratio)
det_scale = float(new_height) / img.shape[0]
resized_img = cv2.resize(img, (new_width, new_height))
det_img = np.zeros((input_size[1], input_size[0], 3), dtype=np.uint8)
det_img[:new_height, :new_width, :] = resized_img
scores_list, bboxes_list, kpss_list = self.forward(det_img, det_thresh)
scores = np.vstack(scores_list).ravel()
order = scores.argsort()[::-1]
bboxes = np.vstack(bboxes_list) / det_scale
if self.use_kps:
kpss = np.vstack(kpss_list) / det_scale
pre_det = np.hstack((bboxes, scores[:, None])).astype(np.float32, copy=False)
pre_det = pre_det[order, :]
keep = self.nms(pre_det)
det = pre_det[keep, :]
kpss = kpss[order, :, :][keep, :, :] if self.use_kps else None
if max_num > 0 and det.shape[0] > max_num:
area = (det[:, 2] - det[:, 0]) * (det[:, 3] - det[:, 1])
img_center = img.shape[0] // 2, img.shape[1] // 2
offsets = np.vstack([
(det[:, 0] + det[:, 2]) / 2 - img_center[1],
(det[:, 1] + det[:, 3]) / 2 - img_center[0]
])
offset_dist_squared = np.sum(np.power(offsets, 2.0), 0)
values = area - offset_dist_squared * 2.0
bindex = np.argsort(values)[::-1][:max_num]
det = det[bindex, :]
if kpss is not None:
kpss = kpss[bindex, :]
return det, kpss
def nms(self, dets, nms_thresh=0.4):
x1, y1, x2, y2, scores = dets[:, 0], dets[:, 1], dets[:, 2], dets[:, 3], dets[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= nms_thresh)[0]
order = order[inds + 1]
return keep
class ArcFaceONNX(BaseONNXModel):
"""Распознаватель лиц (выдает вектор/эмбеддинг)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 127.5
self.input_std = 127.5
self.input_size = tuple(self.input_shape[2:4][::-1])
def get(self, img, face):
aimg = norm_crop(img, landmark=face.kps, image_size=self.input_size[0])
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
net_out = self.session.run(self.output_names, {self.input_names[0]: blob})[0]
face.embedding = net_out.flatten()
return face.embedding
class Attribute(BaseONNXModel):
"""Анализатор атрибутов (выдает пол и возраст)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 0.0
self.input_std = 1.0
self.input_size = tuple(self.input_shape[2:4][::-1])
def get(self, img, face):
bbox = face.bbox
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
_scale = self.input_size[0] / (max(w, h) * 1.5)
# Простая трансформация для Attribute (не требует 5 точек, только центр и масштаб)
M = np.array([
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
], dtype=np.float32)
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
# Получаем гендер и возраст
gender = int(np.argmax(pred[:2]))
age = int(np.round(pred[2] * 100))
face.gender = gender
face.age = age
return gender, age
class INSwapper(BaseONNXModel):
"""Свопер лиц (модели inswapper_128, reswapper)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 0.0
self.input_std = 255.0
self.input_size = tuple(self.input_shape[2:4][::-1])
# Хак для экономии памяти: импортируем onnx только здесь,
# читаем нужную матрицу emap и сразу выгружаем тяжелую модель из RAM.
import onnx
from onnx import numpy_helper
model = onnx.load(self.model_file, load_external_data=False)
self.emap = numpy_helper.to_array(model.graph.initializer[-1])
del model
def get(self, img, target_face, source_face, paste_back=True):
# 1. Идеальное позиционирование (1 в 1 как в оригинальном C++ Insightface)
# ВАЖНО: Insightface центрирует лицо для INSwapper ТОЛЬКО по оси X!
# По оси Y оно остается прижатым выше, сохраняя оригинальные пропорции.
ratio = float(self.input_size[0]) / 128.0
diff_x = 8.0 * ratio
src_pts = ARCFACE_STD_POINTS.copy() * ratio
src_pts[:, 0] += diff_x # Смещаем ТОЛЬКО координаты X!
# 2. Вычисляем аффинную матрицу родным методом OpenCV
M, _ = cv2.estimateAffinePartial2D(target_face.kps, src_pts)
# 3. Кропаем и выравниваем лицо
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
# 4. Подготавливаем эмбеддинг донора
latent = source_face.normed_embedding.reshape((1, -1))
latent = np.dot(latent, self.emap)
latent /= np.linalg.norm(latent)
# 5. Инференс
pred = self.session.run(self.output_names, {
self.input_names[0]: blob,
self.input_names[1]: latent.astype(np.float32)
})[0]
img_fake = pred.transpose((0, 2, 3, 1))[0]
bgr_fake = np.clip(255 * img_fake, 0, 255).astype(np.uint8)[:, :, ::-1]
if not paste_back:
return bgr_fake, M
# 6. Обратная вклейка (Paste Back)
target_img = img
fake_diff = bgr_fake.astype(np.float32) - aimg.astype(np.float32)
fake_diff = np.abs(fake_diff).mean(axis=2)
# Обрезаем края
fake_diff[:2, :] = 0
fake_diff[-2:, :] = 0
fake_diff[:, :2] = 0
fake_diff[:, -2:] = 0
IM = cv2.invertAffineTransform(M)
img_white = np.full((aimg.shape[0], aimg.shape[1]), 255, dtype=np.float32)
# Возвращаем в исходную перспективу
bgr_fake_warped = cv2.warpAffine(bgr_fake, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white_warped = cv2.warpAffine(img_white, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
fake_diff_warped = cv2.warpAffine(fake_diff, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white_warped[img_white_warped > 20] = 255
fthresh = 10
fake_diff_warped[fake_diff_warped < fthresh] = 0
fake_diff_warped[fake_diff_warped >= fthresh] = 255
img_mask = img_white_warped
mask_h_inds, mask_w_inds = np.where(img_mask == 255)
# Защита от пустой маски
if len(mask_h_inds) > 0 and len(mask_w_inds) > 0:
mask_h = np.max(mask_h_inds) - np.min(mask_h_inds)
mask_w = np.max(mask_w_inds) - np.min(mask_w_inds)
mask_size = int(np.sqrt(mask_h * mask_w))
k = max(mask_size // 10, 10)
kernel = np.ones((k, k), np.uint8)
img_mask = cv2.erode(img_mask, kernel, iterations=1)
kernel = np.ones((2, 2), np.uint8)
fake_diff_warped = cv2.dilate(fake_diff_warped, kernel, iterations=1)
k = max(mask_size // 20, 5)
blur_size = (k * 2 + 1, k * 2 + 1)
img_mask = cv2.GaussianBlur(img_mask, blur_size, 0)
k = 5
blur_size = (k * 2 + 1, k * 2 + 1)
fake_diff_warped = cv2.GaussianBlur(fake_diff_warped, blur_size, 0)
img_mask /= 255.0
img_mask = np.reshape(img_mask, [img_mask.shape[0], img_mask.shape[1], 1])
fake_merged = img_mask * bgr_fake_warped + (1.0 - img_mask) * target_img.astype(np.float32)
return fake_merged.astype(np.uint8)
class Landmark(BaseONNXModel):
"""Извлекает 106 (2D) или 68 (3D) точек лица"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 127.5
self.input_std = 128.0
self.input_size = tuple(self.input_shape[2:4][::-1])
output_shape = self.outputs[0].shape
# Определяем, какая это модель (3D или 2D) по размеру выхода
if output_shape[1] == 3309:
self.lmk_dim = 3
self.lmk_num = 68
self.taskname = 'landmark_3d_68'
else:
self.lmk_dim = 2
self.lmk_num = output_shape[1] // self.lmk_dim
self.taskname = f'landmark_2d_{self.lmk_num}'
def get(self, img, face):
bbox = face.bbox
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
_scale = self.input_size[0] / (max(w, h) * 1.5)
# Матрица трансформации (выравнивание по центру bbox)
M = np.array([
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
], dtype=np.float32)
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
if pred.shape[0] >= 3000:
pred = pred.reshape((-1, 3))
else:
pred = pred.reshape((-1, 2))
if self.lmk_num < pred.shape[0]:
pred = pred[-self.lmk_num:, :]
# Денормализация точек в размер модели
pred[:, 0:2] += 1
pred[:, 0:2] *= (self.input_size[0] // 2)
if pred.shape[1] == 3:
pred[:, 2] *= (self.input_size[0] // 2)
# Обратная трансформация точек на оригинальное изображение
IM = cv2.invertAffineTransform(M)
pred_xy = pred[:, 0:2]
pred_xy = np.hstack((pred_xy, np.ones((pred_xy.shape[0], 1)))) # Добавляем гомогенную координату
pred_xy = np.dot(IM, pred_xy.T).T
if pred.shape[1] == 3:
pred = np.hstack((pred_xy, pred[:, 2:3])) # Возвращаем Z
else:
pred = pred_xy
# Сохраняем в объект Face под правильным именем
setattr(face, self.taskname, pred)
# Честный расчет 3D позы
if self.taskname == 'landmark_3d_68':
P = estimate_affine_matrix_3d23d(MEANSHAPE_68, pred)
_, R, _ = P2sRt(P)
rx, ry, rz = matrix2angle(R)
face.pose = np.array([rx, ry, rz], dtype=np.float32)
return pred
+348
View File
@@ -0,0 +1,348 @@
import numpy as np
# эталонная 3D-модель "усредненного" человеческого лица (68 точек в 3D-пространстве)
matrix = [
[
-0.6266950368881226,
-0.2926996946334839,
-0.3140018582344055
],
[
-0.5996649265289307,
-0.12250272184610367,
-0.2924409508705139
],
[
-0.571026086807251,
0.05118739977478981,
-0.2549465000629425
],
[
-0.5338566899299622,
0.21284838020801544,
-0.1866333782672882
],
[
-0.4797332286834717,
0.3506459593772888,
-0.047699932008981705
],
[
-0.3957556486129761,
0.4465116560459137,
0.07328663021326065
],
[
-0.29880842566490173,
0.5106605887413025,
0.17977793514728546
],
[
-0.18838681280612946,
0.554440438747406,
0.3166600465774536
],
[
0.0014708322705700994,
0.584439218044281,
0.38841578364372253
],
[
0.19099053740501404,
0.5517070889472961,
0.31433114409446716
],
[
0.3269285559654236,
0.48957568407058716,
0.16839952766895294
],
[
0.4400261342525482,
0.4023579955101013,
0.03596242889761925
],
[
0.5068787336349487,
0.3116249442100525,
-0.09476063400506973
],
[
0.540894627571106,
0.20452618598937988,
-0.20267142355442047
],
[
0.574118435382843,
0.04570329561829567,
-0.2841764986515045
],
[
0.5991416573524475,
-0.14585931599140167,
-0.296495646238327
],
[
0.6275436282157898,
-0.30774804949760437,
-0.30199483036994934
],
[
-0.47466811537742615,
-0.4376046359539032,
0.23648710548877716
],
[
-0.4166599214076996,
-0.4717560112476349,
0.3159925639629364
],
[
-0.3475387990474701,
-0.4840780198574066,
0.366113543510437
],
[
-0.26063987612724304,
-0.4763909876346588,
0.3992317318916321
],
[
-0.16712301969528198,
-0.45777544379234314,
0.4166091978549957
],
[
0.1231740415096283,
-0.45874011516571045,
0.4250619411468506
],
[
0.2063615769147873,
-0.4804193377494812,
0.41578391194343567
],
[
0.28667303919792175,
-0.4901494085788727,
0.3919537663459778
],
[
0.3623969256877899,
-0.4768601655960083,
0.3527863025665283
],
[
0.425568163394928,
-0.4500580430030823,
0.2953187823295593
],
[
-0.007627937477082014,
-0.3230886459350586,
0.46194377541542053
],
[
-0.007876846939325333,
-0.255738765001297,
0.5104694366455078
],
[
-0.007687545381486416,
-0.19917990267276764,
0.5525456666946411
],
[
-0.007345120422542095,
-0.14261691272258759,
0.5986667275428772
],
[
-0.144961878657341,
0.033126939088106155,
0.4196909964084625
],
[
-0.08434253931045532,
0.03127360716462135,
0.4731740951538086
],
[
-0.005499685648828745,
0.03975145146250725,
0.5146695971488953
],
[
0.06346292793750763,
0.04613516479730606,
0.4792240560054779
],
[
0.13398391008377075,
0.02204025723040104,
0.41907867789268494
],
[
-0.38675081729888916,
-0.3133975565433502,
0.259631872177124
],
[
-0.3166607916355133,
-0.35007160902023315,
0.3285270035266876
],
[
-0.2341379076242447,
-0.35491427779197693,
0.3334933817386627
],
[
-0.15516234934329987,
-0.31524932384490967,
0.3143281042575836
],
[
-0.23092176020145416,
-0.28427040576934814,
0.3255828619003296
],
[
-0.3175090253353119,
-0.28516536951065063,
0.3098825216293335
],
[
0.13895708322525024,
-0.30982404947280884,
0.3182835876941681
],
[
0.21945592761039734,
-0.35319215059280396,
0.33802759647369385
],
[
0.30174651741981506,
-0.349665105342865,
0.333102285861969
],
[
0.37665316462516785,
-0.31351813673973083,
0.26322856545448303
],
[
0.2966947853565216,
-0.2871439754962921,
0.3220130503177643
],
[
0.21462441980838776,
-0.2905276417732239,
0.33124178647994995
],
[
-0.20143844187259674,
0.23736143112182617,
0.37953662872314453
],
[
-0.13732077181339264,
0.18578563630580902,
0.46525245904922485
],
[
-0.07648587226867676,
0.15119342505931854,
0.5035716891288757
],
[
-0.002535885199904442,
0.16872699558734894,
0.5164376497268677
],
[
0.06442102789878845,
0.15088020265102386,
0.5045241117477417
],
[
0.126465305685997,
0.17947596311569214,
0.4685916602611542
],
[
0.21824784576892853,
0.23899227380752563,
0.37567368149757385
],
[
0.13288259506225586,
0.28392839431762695,
0.4400508999824524
],
[
0.06802233308553696,
0.2973543107509613,
0.4774041771888733
],
[
-0.0004690653004217893,
0.30004069209098816,
0.4871094226837158
],
[
-0.06934267282485962,
0.29696860909461975,
0.4805404841899872
],
[
-0.14252015948295593,
0.2743033170700073,
0.43808019161224365
],
[
-0.1781347393989563,
0.23059049248695374,
0.3963589668273926
],
[
-0.07403063774108887,
0.2147187888622284,
0.4653262794017792
],
[
-0.002636224264279008,
0.21414154767990112,
0.4832296073436737
],
[
0.05981616675853729,
0.21076396107673645,
0.47224411368370056
],
[
0.16690002381801605,
0.23127099871635437,
0.396789014339447
],
[
0.059809282422065735,
0.22376428544521332,
0.46641337871551514
],
[
-0.0014343614457175136,
0.2257590889930725,
0.4752439558506012
],
[
-0.07522077113389969,
0.23065608739852905,
0.4671475291252136
]
]
MEANSHAPE_68 = np.array(matrix, dtype=np.float32)
-161
View File
@@ -1,161 +0,0 @@
import os.path as osp
import glob
import logging
import insightface
from insightface.model_zoo.model_zoo import ModelRouter, PickableInferenceSession
from insightface.model_zoo.retinaface import RetinaFace
from insightface.model_zoo.landmark import Landmark
from insightface.model_zoo.attribute import Attribute
from insightface.model_zoo.inswapper import INSwapper
from insightface.model_zoo.arcface_onnx import ArcFaceONNX
from insightface.app import FaceAnalysis
from insightface.utils import DEFAULT_MP_NAME, ensure_available
from insightface.model_zoo import model_zoo
import onnxruntime
import onnx
from onnx import numpy_helper
from scripts.reactor_logger import logger
def patched_get_model_log(self, **kwargs):
session = PickableInferenceSession(self.onnx_file, **kwargs)
print(f'Applied providers: {session._providers}, with options: {session._provider_options}')
inputs = session.get_inputs()
input_cfg = inputs[0]
input_shape = input_cfg.shape
outputs = session.get_outputs()
if len(outputs) >= 5:
return RetinaFace(model_file=self.onnx_file, session=session)
elif input_shape[2] == 192 and input_shape[3] == 192:
return Landmark(model_file=self.onnx_file, session=session)
elif input_shape[2] == 96 and input_shape[3] == 96:
return Attribute(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
return INSwapper(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
return INSwapper(model_file=self.onnx_file, session=session)
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
return ArcFaceONNX(model_file=self.onnx_file, session=session)
else:
return None
def patched_get_model(self, **kwargs):
session = PickableInferenceSession(self.onnx_file, **kwargs)
inputs = session.get_inputs()
input_cfg = inputs[0]
input_shape = input_cfg.shape
outputs = session.get_outputs()
if len(outputs) >= 5:
return RetinaFace(model_file=self.onnx_file, session=session)
elif input_shape[2] == 192 and input_shape[3] == 192:
return Landmark(model_file=self.onnx_file, session=session)
elif input_shape[2] == 96 and input_shape[3] == 96:
return Attribute(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
return INSwapper(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
return INSwapper(model_file=self.onnx_file, session=session)
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
return ArcFaceONNX(model_file=self.onnx_file, session=session)
else:
return None
def patched_faceanalysis_init(self, name=DEFAULT_MP_NAME, root='~/.insightface', allowed_modules=None, **kwargs):
onnxruntime.set_default_logger_severity(3)
self.models = {}
self.model_dir = ensure_available('models', name, root=root)
onnx_files = glob.glob(osp.join(self.model_dir, '*.onnx'))
onnx_files = sorted(onnx_files)
for onnx_file in onnx_files:
model = model_zoo.get_model(onnx_file, **kwargs)
if model is None:
print('model not recognized:', onnx_file)
elif allowed_modules is not None and model.taskname not in allowed_modules:
print('model ignore:', onnx_file, model.taskname)
del model
elif model.taskname not in self.models and (allowed_modules is None or model.taskname in allowed_modules):
self.models[model.taskname] = model
else:
print('duplicated model task type, ignore:', onnx_file, model.taskname)
del model
assert 'detection' in self.models
self.det_model = self.models['detection']
def patched_faceanalysis_prepare(self, ctx_id, det_thresh=0.5, det_size=(640, 640)):
self.det_thresh = det_thresh
assert det_size is not None
self.det_size = det_size
for taskname, model in self.models.items():
if taskname == 'detection':
model.prepare(ctx_id, input_size=det_size, det_thresh=det_thresh)
else:
model.prepare(ctx_id)
def patched_inswapper_init(self, model_file=None, session=None):
self.model_file = model_file
self.session = session
model = onnx.load(self.model_file)
graph = model.graph
self.emap = numpy_helper.to_array(graph.initializer[-1])
self.input_mean = 0.0
self.input_std = 255.0
if self.session is None:
self.session = onnxruntime.InferenceSession(self.model_file, None)
inputs = self.session.get_inputs()
self.input_names = []
for inp in inputs:
self.input_names.append(inp.name)
outputs = self.session.get_outputs()
output_names = []
for out in outputs:
output_names.append(out.name)
self.output_names = output_names
assert len(self.output_names) == 1
input_cfg = inputs[0]
input_shape = input_cfg.shape
self.input_shape = input_shape
self.input_size = tuple(input_shape[2:4][::-1])
def pathced_retinaface_prepare(self, ctx_id, **kwargs):
if ctx_id<0:
self.session.set_providers(['CPUExecutionProvider'])
nms_thresh = kwargs.get('nms_thresh', None)
if nms_thresh is not None:
self.nms_thresh = nms_thresh
det_thresh = kwargs.get('det_thresh', None)
if det_thresh is not None:
self.det_thresh = det_thresh
input_size = kwargs.get('input_size', None)
if input_size is not None and self.input_size is None:
self.input_size = input_size
def patch_insightface(get_model, faceanalysis_init, faceanalysis_prepare, inswapper_init, retinaface_prepare):
insightface.model_zoo.model_zoo.ModelRouter.get_model = get_model
insightface.app.FaceAnalysis.__init__ = faceanalysis_init
insightface.app.FaceAnalysis.prepare = faceanalysis_prepare
insightface.model_zoo.inswapper.INSwapper.__init__ = inswapper_init
insightface.model_zoo.retinaface.RetinaFace.prepare = retinaface_prepare
# original_functions = [ModelRouter.get_model, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
original_functions = [patched_get_model_log, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
patched_functions = [patched_get_model, patched_faceanalysis_init, patched_faceanalysis_prepare, patched_inswapper_init, pathced_retinaface_prepare]
def apply_patch(console_log_level):
if console_log_level == 0:
patch_insightface(*patched_functions)
logger.setLevel(logging.WARNING)
elif console_log_level == 1:
patch_insightface(*patched_functions)
logger.setLevel(logging.STATUS)
elif console_log_level == 2:
patch_insightface(*original_functions)
logger.setLevel(logging.INFO)
+38 -1
View File
@@ -7,13 +7,14 @@ import cv2
import math
import logging
import hashlib
from insightface.app.common import Face
from reactor_core.face_objects import Face
from safetensors.torch import save_file, safe_open
from tqdm import tqdm
import urllib.request
import onnxruntime
from typing import Any
import folder_paths
from comfy.utils import ProgressBar
ORT_SESSION = None
@@ -24,6 +25,34 @@ def tensor_to_pil(img_tensor, batch_index=0):
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8).squeeze())
return img
# def tensor_to_pil(img, batch_index=0):
# """Безопасное преобразование тензора в PIL.Image с обработкой особых случаев"""
# try:
# # Обработка пакетных данных
# if isinstance(img, torch.Tensor):
# if len(img.shape) == 4:
# img = img[batch_index] # Выбор элемента батча
# img = img.detach().cpu().numpy()
# # Нормализация и приведение типа
# if img.dtype == np.float32:
# img = np.clip(255. * img, 0, 255).astype(np.uint8)
# # Обработка нестандартных размерностей
# if img.shape[-1] > 4: # Если каналов больше 4
# img = img[..., :3] # Берем первые 3 канала
# # Преобразование в 2D/3D массив
# if len(img.shape) == 3 and img.shape[0] == 1: # [C, H, W] → [H, W]
# img = img.squeeze(0)
# elif len(img.shape) == 3 and img.shape[2] == 1: # [H, W, C=1]
# img = img.squeeze(-1)
# return Image.fromarray(img)
# except Exception as e:
# raise RuntimeError(f"Невозможно преобразовать тензор формы {img.shape} в PIL.Image") from e
def batch_tensor_to_pil(img_tensor):
# Convert tensor of shape [batch_size, channels, height, width] to a list of PIL Images
@@ -208,6 +237,14 @@ def normalize_cropped_face(cropped_face):
return cropped_face
def progress_bar(total):
return ProgressBar(total)
def progress_bar_reset(pbar):
pbar.current = 0
pbar.update(0)
# author: Trung0246 --->
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
# Iterate over the list of full folder paths
+1 -2
View File
@@ -1,7 +1,6 @@
albumentations>=1.4.16
insightface==0.7.3
onnx>=1.14.0
opencv-python>=4.7.0.72
numpy==1.26.3
numpy
segment_anything
ultralytics
+34 -12
View File
@@ -312,15 +312,24 @@ class SafeToGPU:
if is_same_device(device, 'cpu'):
obj.to(device)
else:
if is_same_device(obj.device, 'cpu'): # cpu to gpu
# TRY to check submodule device
current_device = None
try:
current_device = next(obj.parameters()).device
except:
pass
if current_device is None or is_same_device(current_device, 'cpu'):
model_management.free_memory(self.size * 1.3, device)
if model_management.get_free_memory(device) > self.size * 1.3:
try:
print("Moving to GPU...", end=" ")
obj.to(device)
print("OK")
except:
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [1]")
print(f"Failed\nWARN: Model not moved to '{device}' [1]")
else:
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [2]")
print(f"WARN: Model not moved to '{device}' [2]")
def center_of_bbox(bbox):
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
@@ -532,9 +541,10 @@ def merge_and_stack_masks(stacked_masks, group_size):
def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
if sam_model.is_auto_mode:
if getattr(sam_model, 'is_auto_mode', False):
device = model_management.get_torch_device()
sam_model.safe_to.to_device(sam_model, device=device)
# sam_model.safe_to.to_device(sam_model, device=device)
sam_model.to(device)
try:
predictor = SamPredictor(sam_model)
@@ -588,18 +598,29 @@ def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
mask = combine_masks2(total_masks)
finally:
if sam_model.is_auto_mode:
sam_model.cpu()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
sam_model.to(device)
# if sam_model.is_auto_mode:
# sam_model.cpu()
pass
mask_working_device = torch.device("cpu")
# mask_working_device = torch.device("cpu")
mask_working_device = model_management.get_torch_device()
if mask is not None:
mask = mask.float()
mask = dilate_mask(mask.cpu().numpy(), dilation)
mask = torch.from_numpy(mask)
mask = mask.to(device=mask_working_device)
# mask = dilate_mask(mask.cpu().numpy(), dilation)
# mask = torch.from_numpy(mask)
# mask = mask.to(device=mask_working_device)
mask = mask.unsqueeze(0).unsqueeze(0) # [1,1,H,W]
if dilation > 0:
kernel_size = 1 + dilation * 2
mask = torch.nn.functional.max_pool2d(mask, kernel_size=kernel_size, stride=1, padding=dilation)
elif dilation < 0:
kernel_size = 1 + abs(dilation) * 2
mask = -torch.nn.functional.max_pool2d(-mask, kernel_size=kernel_size, stride=1, padding=abs(dilation))
mask = mask.squeeze(0).squeeze(0) # [H,W]
else:
# Extracting batch, height and width
height, width, _ = image.shape
@@ -641,7 +662,8 @@ def tensor2rgba(t: torch.Tensor) -> torch.Tensor:
elif size[3] == 1:
return t.repeat(1, 1, 1, 4)
elif size[3] == 3:
alpha_tensor = torch.ones((size[0], size[1], size[2], 1))
# alpha_tensor = torch.ones((size[0], size[1], size[2], 1)).to(t.device)
alpha_tensor = t.new_ones((size[0], size[1], size[2], 1))
return torch.cat((t, alpha_tensor), dim=3)
else:
return t
+16 -58
View File
@@ -2,14 +2,14 @@ import os, glob
from PIL import Image
import modules.scripts as scripts
import r_modules.scripts as scripts
# from modules.upscaler import Upscaler, UpscalerData
from modules import scripts, scripts_postprocessing
from modules.processing import (
StableDiffusionProcessing,
StableDiffusionProcessingImg2Img,
from r_modules import scripts, scripts_postprocessing
from r_modules.processing import (
Processing,
ProcessingImg2Img,
)
from modules.shared import state
from r_modules.shared import state
from scripts.reactor_logger import logger
from scripts.reactor_swapper import (
swap_face,
@@ -26,7 +26,8 @@ import comfy.model_management as model_management
def get_models():
swappers = [
"insightface",
"reswapper"
"reswapper",
"hyperswap"
]
models_list = []
for folder in swappers:
@@ -42,7 +43,7 @@ class FaceSwapScript(scripts.Script):
def process(
self,
p: StableDiffusionProcessing,
p: Processing,
img,
enable,
source_faces_index,
@@ -101,12 +102,12 @@ class FaceSwapScript(scripts.Script):
self.gender_target = 2
# if self.source is not None:
if isinstance(p, StableDiffusionProcessingImg2Img) and swap_in_source:
if isinstance(p, ProcessingImg2Img) and swap_in_source:
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
if len(p.init_images) == 1:
result = swap_face(
result, bbox, swapped_indexes = swap_face(
self.source,
p.init_images[0],
source_faces_index=self.source_faces_index,
@@ -123,27 +124,11 @@ class FaceSwapScript(scripts.Script):
interpolation=self.interpolation,
)
p.init_images[0] = result
# for i in range(len(p.init_images)):
# if state.interrupted or model_management.processing_interrupted():
# logger.status("Interrupted by User")
# break
# if len(p.init_images) > 1:
# logger.status(f"Swap in %s", i)
# result = swap_face(
# self.source,
# p.init_images[i],
# source_faces_index=self.source_faces_index,
# faces_index=self.faces_index,
# model=self.model,
# gender_source=self.gender_source,
# gender_target=self.gender_target,
# face_model=self.face_model,
# )
# p.init_images[i] = result
p.bbox = bbox
p.swapped_indexes = swapped_indexes
elif len(p.init_images) > 1:
result = swap_face_many(
result, bbox, swapped_indexes = swap_face_many(
self.source,
p.init_images,
source_faces_index=self.source_faces_index,
@@ -160,34 +145,7 @@ class FaceSwapScript(scripts.Script):
interpolation=self.interpolation,
)
p.init_images = result
p.bbox = bbox
p.swapped_indexes = swapped_indexes
logger.status("--Done!--")
# else:
# logger.error(f"Please provide a source face")
def postprocess_batch(self, p, *args, **kwargs):
if self.enable:
images = kwargs["images"]
def postprocess_image(self, p, script_pp: scripts.PostprocessImageArgs, *args):
if self.enable and self.swap_in_generated:
if self.source is not None:
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
image: Image.Image = script_pp.image
result = swap_face(
self.source,
image,
source_faces_index=self.source_faces_index,
faces_index=self.faces_index,
model=self.model,
upscale_options=self.upscale_options,
gender_source=self.gender_source,
gender_target=self.gender_target,
)
try:
pp = scripts_postprocessing.PostprocessedImage(result)
pp.info = {}
p.extra_generation_params.update(pp.info)
script_pp.image = pp.image
except:
logger.error(f"Cannot create a result image")
+1 -1
View File
@@ -2,7 +2,7 @@ import logging
import copy
import sys
from modules import shared
from r_modules import shared
from reactor_utils import addLoggingLevel
+37 -16
View File
@@ -1,34 +1,55 @@
from transformers import pipeline
from PIL import Image
import io
import logging
import os
import comfy.model_management as model_management
from reactor_utils import download
from scripts.reactor_logger import logger
MODEL_EXISTS = False
def ensure_nsfw_model(nsfwdet_model_path):
"""Download NSFW detection model if it doesn't exist"""
if not os.path.exists(nsfwdet_model_path):
os.makedirs(nsfwdet_model_path)
nd_urls = [
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/config.json",
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/model.safetensors",
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/preprocessor_config.json",
]
for model_url in nd_urls:
model_name = os.path.basename(model_url)
model_path = os.path.join(nsfwdet_model_path, model_name)
global MODEL_EXISTS
downloaded = 0
nd_urls = [
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/config.json",
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/model.safetensors",
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/preprocessor_config.json",
]
for model_url in nd_urls:
model_name = os.path.basename(model_url)
model_path = os.path.join(nsfwdet_model_path, model_name)
if not os.path.exists(model_path):
if not os.path.exists(nsfwdet_model_path):
os.makedirs(nsfwdet_model_path)
download(model_url, model_path, model_name)
if os.path.exists(model_path):
downloaded += 1
MODEL_EXISTS = True if downloaded == 3 else False
return MODEL_EXISTS
SCORE = 0.96
SCORE = 0.979
logging.getLogger("transformers").setLevel(logging.ERROR)
def nsfw_image(img_path: str, model_path: str):
ensure_nsfw_model(model_path)
with Image.open(img_path) as img:
predict = pipeline("image-classification", model=model_path)
def nsfw_image(img_data, model_path: str):
if not MODEL_EXISTS:
logger.status("Ensuring NSFW detection model exists...")
if not ensure_nsfw_model(model_path):
return True
device = model_management.get_torch_device()
with Image.open(io.BytesIO(img_data)) as img:
if "cpu" in str(device):
predict = pipeline("image-classification", model=model_path)
else:
device_id = 0
if "cuda" in str(device):
device_id = int(str(device).split(":")[1])
predict = pipeline("image-classification", model=model_path, device=device_id)
result = predict(img)
if result[0]["label"] == "nsfw" and result[0]["score"] > SCORE:
logger.status(f"NSFW content detected, skipping...")
logger.status(f'NSFW content detected with score={result[0]["score"]}, skipping...')
return True
return False
+184 -179
View File
@@ -6,22 +6,22 @@ import cv2
import numpy as np
from PIL import Image
import insightface
from insightface.app.common import Face
# try:
# import torch.cuda as cuda
# except:
# cuda = None
from reactor_core.analyzer import ReActorFaceAnalysis
from reactor_core.face_objects import Face
from reactor_core.inswap import INSwapper
from reactor_core.hyperswap import HyperSwapper
import torch
import folder_paths
import comfy.model_management as model_management
from modules.shared import state
from r_modules.shared import state
from scripts.reactor_logger import logger
from reactor_utils import (
move_path,
get_image_md5hash,
progress_bar,
progress_bar_reset
)
from scripts.r_faceboost import swapper, restorer
@@ -43,13 +43,6 @@ try:
except Exception as e:
logger.debug(f"ExecutionProviderError: {e}.\nEP is set to CPU.")
providers = ["CPUExecutionProvider"]
# if cuda is not None:
# if cuda.is_available():
# providers = ["CUDAExecutionProvider"]
# else:
# providers = ["CPUExecutionProvider"]
# else:
# providers = ["CPUExecutionProvider"]
models_path_old = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
insightface_path_old = os.path.join(models_path_old, "insightface")
@@ -59,6 +52,7 @@ models_path = folder_paths.models_dir
insightface_path = os.path.join(models_path, "insightface")
insightface_models_path = os.path.join(insightface_path, "models")
reswapper_path = os.path.join(models_path, "reswapper")
hyperswap_path = os.path.join(models_path, "hyperswap")
if os.path.exists(models_path_old):
move_path(insightface_models_path_old, insightface_models_path)
@@ -86,11 +80,6 @@ TARGET_IMAGE_LIST_HASH = []
def unload_model(model):
if model is not None:
# check if model has unload method
# if "unload" in model:
# model.unload()
# if "model_unload" in model:
# model.model_unload()
del model
return None
@@ -108,7 +97,7 @@ def getAnalysisModel(det_size = (640, 640)):
global ANALYSIS_MODELS
ANALYSIS_MODEL = ANALYSIS_MODELS[str(det_size[0])]
if ANALYSIS_MODEL is None:
ANALYSIS_MODEL = insightface.app.FaceAnalysis(
ANALYSIS_MODEL = ReActorFaceAnalysis(
name="buffalo_l", providers=providers, root=insightface_path
)
ANALYSIS_MODEL.prepare(ctx_id=0, det_size=det_size)
@@ -120,11 +109,18 @@ def getFaceSwapModel(model_path: str):
if FS_MODEL is None or CURRENT_FS_MODEL_PATH is None or CURRENT_FS_MODEL_PATH != model_path:
CURRENT_FS_MODEL_PATH = model_path
FS_MODEL = unload_model(FS_MODEL)
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
model_filename = os.path.basename(model_path)
if "hyperswap" in model_filename.lower(): # Если это Hyperswap
model_path = os.path.join(folder_paths.models_dir, "hyperswap", model_filename)
FS_MODEL = HyperSwapper(model_path, providers=providers)
else: # Если это INSwapper / Reswapper
if "reswapper" in model_filename.lower():
model_path = os.path.join(folder_paths.models_dir, "reswapper", model_filename)
FS_MODEL = INSwapper(model_path, providers=providers)
return FS_MODEL
def sort_by_order(face, order: str):
if order == "left-right":
return sorted(face, key=lambda x: x.bbox[0])
@@ -136,42 +132,47 @@ def sort_by_order(face, order: str):
return sorted(face, key=lambda x: x.bbox[1], reverse = True)
if order == "small-large":
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
# if order == "large-small":
# return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
# by default "large-small":
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
def get_face_gender(
face,
face_index,
gender_condition,
operated: str,
order: str,
face,
face_index,
gender_condition,
operated: str,
order: str,
):
gender = [
x.sex
for x in face
]
gender.reverse()
# If index is outside of bounds, return None, avoid exception
if face_index >= len(gender):
logger.status("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(gender))
return None, 0
face_gender = gender[face_index]
logger.status("%s Face %s: Detected Gender -%s-", operated, face_index, face_gender)
if (gender_condition == 1 and face_gender == "F") or (gender_condition == 2 and face_gender == "M"):
logger.status("OK - Detected Gender matches Condition")
try:
faces_sorted = sort_by_order(face, order)
return faces_sorted[face_index], 0
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
except IndexError:
return None, 0
else:
logger.status("WRONG - Detected Gender doesn't match Condition")
faces_sorted = sort_by_order(face, order)
return faces_sorted[face_index], 1
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 1
# 1. Сортируем ВСЕ найденные лица (без фильтрации!)
faces_sorted = sort_by_order(face, order)
# 2. Проверяем, существует ли вообще лицо с таким визуальным индексом
if face_index >= len(faces_sorted):
logger.info("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(faces_sorted) - 1)
return None, 0, None
# 3. Берем конкретное лицо по его позиции на фото (например, второе справа)
face_selected = faces_sorted[face_index]
# Если фильтр по полу отключен (no) - сразу отдаем лицо в работу
if gender_condition == 0:
return face_selected, 0, face_index
# 4. Проверяем пол выбранного лица
# face.gender: 0 = female, 1 = male
# gender_condition: 1 = female, 2 = male
expected_gender = 0 if gender_condition == 1 else 1
actual_gender = getattr(face_selected, 'gender', -1)
sel_gender_str = "Male" if actual_gender == 1 else "Female" if actual_gender == 0 else "Unknown"
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, sel_gender_str)
# Если пол не совпадает с тем, что заказал юзер
if actual_gender != expected_gender:
logger.info(f"{operated} Face {face_index}: WRONG gender ({sel_gender_str})")
return face_selected, 1, face_index # 1 означает флаг wrong_gender = True (цикл его пропустит)
# Если всё идеально
return face_selected, 0, face_index
def half_det_size(det_size):
logger.status("Trying to halve 'det_size' parameter")
@@ -179,7 +180,15 @@ def half_det_size(det_size):
def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
face_analyser = getAnalysisModel(det_size)
faces = face_analyser.get(img_data)
faces = []
try:
faces = face_analyser.get(img_data)
except Exception as e:
# import traceback
# traceback.print_exc()
# logger.error(f"Error during face analysis: {e}")
logger.error("No faces found")
# Try halving det_size if no faces are found
if len(faces) == 0 and det_size[0] > 320 and det_size[1] > 320:
@@ -212,10 +221,9 @@ def get_face_single(img_data: np.ndarray, face, face_index=0, det_size=(640, 640
try:
faces_sorted = sort_by_order(face, order)
return faces_sorted[face_index], 0
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
return faces_sorted[face_index], 0, face_index
except IndexError:
return None, 0
return None, 0, None
def swap_face(
@@ -236,6 +244,8 @@ def swap_face(
):
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH
result_image = target_img
bbox = []
swapped_indexes = []
if model is not None:
@@ -312,82 +322,76 @@ def swap_face(
logger.status("Using Hashed Target Face(s) Model...")
target_faces = TARGET_FACES
# No use in trying to swap faces if no faces are found, enhancement
if len(target_faces) == 0:
logger.status("Cannot detect any Target, skipping swapping...")
return result_image
return result_image, bbox, swapped_indexes
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
# 1. Заранее собираем список ТОЛЬКО ВАЛИДНЫХ исходных лиц
valid_source_faces = []
if source_img is not None:
# separated management of wrong_gender between source and target, enhancement
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
for idx in source_faces_index:
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
if sf is not None and src_wrong_gender == 0:
valid_source_faces.append(sf)
else:
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
src_wrong_gender = 0
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
if sf is not None and src_wrong_gender == 0:
valid_source_faces.append(sf)
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
elif source_face is not None:
if len(valid_source_faces) == 0:
logger.status("No valid source face(s) found in the provided Index after gender filter")
else:
result = target_img
if "inswapper" in model:
model_path = os.path.join(insightface_path, model)
elif "reswapper" in model:
model_path = os.path.join(reswapper_path, model)
elif "hyperswap" in model:
model_path = os.path.join(hyperswap_path, model)
face_swapper = getFaceSwapModel(model_path)
source_face_idx = 0
# 2. Идем по целевым лицам
for face_num in faces_index:
# No use in trying to swap faces if no further faces are found, enhancement
if face_num >= len(target_faces):
logger.status("Checked all existing target faces, skipping swapping...")
break
if len(source_faces_index) > 1 and source_face_idx > 0:
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
source_face_idx += 1
if source_face is not None and src_wrong_gender == 0:
target_face, wrong_gender = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
if target_face is not None and wrong_gender == 0:
logger.status(f"Swapping...")
if face_boost_enabled:
logger.status(f"Face Boost is enabled")
bgr_fake, M = face_swapper.get(result, target_face, source_face, paste_back=False)
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
M *= scale
result = swapper.in_swap(target_img, bgr_fake, M)
else:
# logger.status(f"Swapping as-is")
result = face_swapper.get(result, target_face, source_face)
elif wrong_gender == 1:
wrong_gender = 0
# Keep searching for other faces if wrong gender is detected, enhancement
#if source_face_idx == len(source_faces_index):
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
# return result_image
logger.status("Wrong target gender detected")
continue
target_face, wrong_gender, target_face_index = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
if target_face is not None and wrong_gender == 0:
logger.status(f"Swapping...")
# 3. Берем валидное лицо (если их меньше, чем целей — идем по кругу)
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
if face_boost_enabled and "hyperswap" not in model:
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
bgr_fake, M = face_swapper.get(result, target_face, source_face_to_use, paste_back=False)
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
M *= scale
result = swapper.in_swap(result, bgr_fake, M)
else:
logger.status(f"No target face found for {face_num}")
elif src_wrong_gender == 1:
src_wrong_gender = 0
# Keep searching for other faces if wrong gender is detected, enhancement
#if source_face_idx == len(source_faces_index):
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
# return result_image
logger.status("Wrong source gender detected")
result = face_swapper.get(result, target_face, source_face_to_use)
bbox.append(tuple(map(float, target_face.bbox)))
swapped_indexes.append(target_face_index)
# Продвигаем индекс исходного лица ТОЛЬКО после УСПЕШНОГО применения
if len(valid_source_faces) > 1:
source_face_idx += 1
elif wrong_gender == 1:
logger.status("Wrong target gender detected")
continue
else:
logger.status(f"No source face found for face number {source_face_idx}.")
logger.info(f"No target face found for {face_num}")
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
else:
logger.status("No source face(s) in the provided Index")
else:
logger.status("No source face(s) found")
return result_image
return result_image, bbox, swapped_indexes
def swap_face_many(
source_img: Union[Image.Image, None],
@@ -407,28 +411,23 @@ def swap_face_many(
):
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, TARGET_FACES_LIST, TARGET_IMAGE_LIST_HASH
result_images = target_imgs
bbox = []
swapped_indexes = []
if model is not None:
if isinstance(source_img, str): # source_img is a base64 string
if isinstance(source_img, str):
import base64, io
if 'base64,' in source_img: # check if the base64 string has a data URL scheme
# split the base64 string to get the actual base64 encoded image data
if 'base64,' in source_img:
base64_data = source_img.split('base64,')[-1]
# decode base64 string to bytes
img_bytes = base64.b64decode(base64_data)
else:
# if no data URL scheme, just decode
img_bytes = base64.b64decode(source_img)
source_img = Image.open(io.BytesIO(img_bytes))
target_imgs = [cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) for target_img in target_imgs]
if source_img is not None:
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
source_image_md5hash = get_image_md5hash(source_img)
if SOURCE_IMAGE_HASH is None:
@@ -451,18 +450,22 @@ def swap_face_many(
source_faces = SOURCE_FACES
elif face_model is not None:
source_faces_index = [0]
logger.status("Using Loaded Source Face Model...")
source_face_model = [face_model]
source_faces = source_face_model
else:
logger.error("Cannot detect any Source")
if source_faces is not None:
target_faces = []
pbar = progress_bar(len(target_imgs))
if len(TARGET_IMAGE_LIST_HASH) > 0:
logger.status(f"Using Hashed Target Face(s) Model...")
else:
logger.status(f"Analyzing Target Image...")
for i, target_img in enumerate(target_imgs):
if state.interrupted or model_management.processing_interrupted():
logger.status("Interrupted by User")
@@ -484,93 +487,95 @@ def swap_face_many(
logger.info("(Image %s) Target Image the Same? %s", i, target_image_same)
if len(TARGET_FACES_LIST) == 0:
logger.status(f"Analyzing Target Image {i}...")
target_face = analyze_faces(target_img)
TARGET_FACES_LIST = [target_face]
elif len(TARGET_FACES_LIST) == i and not target_image_same:
logger.status(f"Analyzing Target Image {i}...")
target_face = analyze_faces(target_img)
TARGET_FACES_LIST.append(target_face)
elif len(TARGET_FACES_LIST) != i and not target_image_same:
logger.status(f"Analyzing Target Image {i}...")
target_face = analyze_faces(target_img)
TARGET_FACES_LIST[i] = target_face
elif target_image_same:
logger.status("(Image %s) Using Hashed Target Face(s) Model...", i)
target_face = TARGET_FACES_LIST[i]
# logger.status(f"Analyzing Target Image {i}...")
# target_face = analyze_faces(target_img)
if target_face is not None:
target_faces.append(target_face)
pbar.update(1)
# No use in trying to swap faces if no faces are found, enhancement
progress_bar_reset(pbar)
if len(target_faces) == 0:
logger.status("Cannot detect any Target, skipping swapping...")
return result_images
return result_images, bbox, swapped_indexes
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
valid_source_faces = []
if source_img is not None:
# separated management of wrong_gender between source and target, enhancement
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
for idx in source_faces_index:
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
if sf is not None and src_wrong_gender == 0:
valid_source_faces.append(sf)
else:
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
src_wrong_gender = 0
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
if sf is not None and src_wrong_gender == 0:
valid_source_faces.append(sf)
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
elif source_face is not None:
if len(valid_source_faces) == 0:
logger.status("No valid source face(s) found in the provided Index after gender filter")
else:
results = target_imgs
model_path = model_path = os.path.join(insightface_path, model)
if "inswapper" in model:
model_path = os.path.join(insightface_path, model)
elif "reswapper" in model:
model_path = os.path.join(reswapper_path, model)
elif "hyperswap" in model:
model_path = os.path.join(hyperswap_path, model)
face_swapper = getFaceSwapModel(model_path)
source_face_idx = 0
pbar = progress_bar(len(target_imgs))
logger.status(f"Swapping...")
for face_num in faces_index:
# No use in trying to swap faces if no further faces are found, enhancement
if face_num >= len(target_faces):
logger.status("Checked all existing target faces, skipping swapping...")
break
if len(source_faces_index) > 1 and source_face_idx > 0:
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
source_face_idx += 1
if source_face is not None and src_wrong_gender == 0:
# Reading results to make current face swap on a previous face result
for i, (target_img, target_face) in enumerate(zip(results, target_faces)):
target_face_single, wrong_gender = get_face_single(target_img, target_face, face_index=face_num, gender_target=gender_target, order=faces_order[0])
if target_face_single is not None and wrong_gender == 0:
result = target_img
logger.status(f"Swapping {i}...")
if face_boost_enabled:
logger.status(f"Face Boost is enabled")
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face, paste_back=False)
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
M *= scale
result = swapper.in_swap(target_img, bgr_fake, M)
else:
# logger.status(f"Swapping as-is")
result = face_swapper.get(target_img, target_face_single, source_face)
results[i] = result
elif wrong_gender == 1:
wrong_gender = 0
logger.status("Wrong target gender detected")
continue
target_used_in_any_image = False
for i, (target_img, target_face_list) in enumerate(zip(results, target_faces)):
target_face_single, wrong_gender, target_face_index = get_face_single(target_img, target_face_list, face_index=face_num, gender_target=gender_target, order=faces_order[0])
if target_face_single is not None and wrong_gender == 0:
target_used_in_any_image = True
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
result = target_img
if face_boost_enabled and "hyperswap" not in model:
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face_to_use, paste_back=False)
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
M *= scale
result = swapper.in_swap(target_img, bgr_fake, M)
else:
logger.status(f"No target face found for {face_num}")
elif src_wrong_gender == 1:
src_wrong_gender = 0
logger.status("Wrong source gender detected")
continue
else:
logger.status(f"No source face found for face number {source_face_idx}.")
result = face_swapper.get(target_img, target_face_single, source_face_to_use)
results[i] = result
bbox.append(tuple(map(float, target_face_single.bbox)))
swapped_indexes.append(target_face_index)
pbar.update(1)
elif wrong_gender == 1:
logger.status("Wrong target gender detected")
pbar.update(1)
continue
else:
logger.info(f"{i}: No target face found for {face_num}")
pbar.update(1)
if target_used_in_any_image and len(valid_source_faces) > 1:
source_face_idx += 1
progress_bar_reset(pbar)
result_images = [Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) for result in results]
else:
logger.status("No source face(s) in the provided Index")
else:
logger.status("No source face(s) found")
return result_images
return result_images, bbox, swapped_indexes
+1 -1
View File
@@ -1,5 +1,5 @@
app_title = "ReActor Node for ComfyUI"
version_flag = "v0.6.0"
version_flag = "v0.7.1-b3"
COLORS = {
"CYAN": "\033[0;36m", # CYAN