78 Commits
Author SHA1 Message Date
Gourieff | 古仁 fa813002f2 UPD: What's new sec 2026-10-10 17:32:20 +07:00
Gourieff | 古仁 e5850cc5c3 UPD: Usage sec 2026-10-10 17:11:12 +07:00
Gourieff | 古仁 5ec405e393 VersionUP
0.7.2 Beta1
2026-10-10 16:45:42 +07:00
Gourieff | 古仁 3725d4bf63 ADD: Nodes subcategories 2026-10-10 16:44:36 +07:00
Gourieff | 古仁 d7cabb2921 UPD: DLSS5 Enhancer to subcategory 2026-10-10 16:32:03 +07:00
Gourieff | 古仁 067ba6de36 Merge branch 'cinematic-enhance' into evolve 2026-10-10 16:05:46 +07:00
Gourieff | 古仁 0b377b00d4 UPD: Comfy Registry 2026-10-10 16:00:43 +07:00
Gourieff | 古仁 314ad7fe9e VersionUP
0.7.1 (Beta passed)
2026-10-10 15:59:32 +07:00
Gourieff | 古仁 3f66ebcba6 ADD: Cinematic Enhancer Node
v1
2026-10-10 15:54:56 +07:00
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
33 changed files with 2611 additions and 655 deletions
+1
View File
@@ -3,3 +3,4 @@ __pycache__/
.vscode/
example
input
*.dll
+94 -71
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.1-brightgreen?style=for-the-badge&labelColor=darkgreen)
![Version](https://img.shields.io/badge/node_version-0.7.2_beta1-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,67 @@
## What's new in the latest update
### 0.7.2 <sub><sup>BETA1</sup></sub>
- **New `Cinematic Enhancer` node**. Forget about plastic skin and boring pictures. This Node can help make any photorealistic image more soulful. You can add different effects (simple or advanced grain, vignette, blur, color correction, HDR) and apply LUT.<br><u>Requirements:</u>
- Put LUT-files in `models/luts`, you can find some collection [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/luts)<br>
- `pip install colour-science` in your Comfy Python -OR- just run install.bat (Win users) in the ReActor's folder
*Inspired by such nodes as: Image Apply LUT ([essentials](https://github.com/cubiq/ComfyUI_essentials)), LayerFilter: Film V2 ([LayerStyle](https://github.com/chflame163/ComfyUI_LayerStyle)), Color Correction ([post-processing-nodes](https://github.com/EllangoK/ComfyUI-post-processing-nodes)), HDR Effects ([SuperBeasts](https://github.com/SuperBeastsAI/ComfyUI-SuperBeasts))*
- Nodes divided into subcategories.
### 0.7.1
- **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>
- Windows 10/11
- NVIDIA display driver >= 616.x
- NVIDIA RTX 40/50-series GPU<br>
(compatibility with older RTX series is unconfirmed)
- Other fixes and improvements
### 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
@@ -69,9 +125,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!
@@ -112,7 +165,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
@@ -160,7 +213,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
@@ -183,12 +236,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
@@ -196,7 +249,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:
@@ -217,21 +270,25 @@ Thanks to everyone who finds bugs, suggests new features and supports this proje
## Installation
<details>
<summary>Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows</summary>
### Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows
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
@@ -252,9 +309,13 @@ 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)
- ReActorCinematicEnhancer (Add cinematic effects to any image)
Connect all required slots and run the query.
@@ -330,57 +391,19 @@ You can set the strength of face swap for `source_image` or `face_model` from 0%
## 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://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):
+95 -72
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.1-brightgreen?style=for-the-badge&labelColor=darkgreen)
<!--<sup>
<font color=brightred>
## !!! [Важные изменения](#latestupdate) !!!<br>Не забудьте добавить Нод заново в существующие воркфлоу
</font>
</sup>-->
![Version](https://img.shields.io/badge/версия_нода-0.7.2_beta1-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,67 @@
## Что нового в последнем обновлении
### 0.7.2 <sub><sup>BETA1</sup></sub>
- **Новый узел `Cinematic Enhancer`**. Забудьте о пластиковой коже и скучной картинке. Этот узел поможет вдохнуть душу в любое фотореалистичное изображение. Вы можете добавлять разные эффекты (простое и продвинутое зерно, виньетку, размытие, коррекцию цвета, HDR) и применить LUT.<br><u>Требования:</u>
- Положите LUT-файлы в `models/luts`, небольшую коллекцию можете найти [тут](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/luts)<br>
- `pip install colour-science` в Пайтон вашего Comfy -ИЛИ- просто запустите install.bat (пользователи Win) внутри папки РеАктора
*Вдохновлено такими узлами как: Image Apply LUT ([essentials](https://github.com/cubiq/ComfyUI_essentials)), LayerFilter: Film V2 ([LayerStyle](https://github.com/chflame163/ComfyUI_LayerStyle)), Color Correction ([post-processing-nodes](https://github.com/EllangoK/ComfyUI-post-processing-nodes)), HDR Effects ([SuperBeasts](https://github.com/SuperBeastsAI/ComfyUI-SuperBeasts))*
- Разделение узлов на подкатегории.
### 0.7.1
- **Новый узел `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>
- Windows 10/11
- NVIDIA display driver >= 616.x
- NVIDIA RTX 40/50-series GPU
(совместимость с более старыми сериями 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
- Улучшенная логика работы с индексами множества лиц при определении пола
@@ -69,9 +125,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! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
@@ -111,7 +164,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
@@ -159,7 +212,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 моделей восстановления лиц
@@ -182,12 +235,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 ОС
- Разные исправления и улучшения
@@ -195,7 +248,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, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
@@ -218,22 +271,24 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
## Установка
<details>
<summary>Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows</summary>
### Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows
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">
@@ -256,13 +311,17 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
- ReActorMakeFaceModelBatch (Создать пачку моделей лиц)
- ••• Дополнительные •••
- ReActorRestoreFace (Восстановление лиц)
- ReActorRestoreFaceAdvanced (Восстановление лиц продвинутое)
- ReActorFaceSimilarity (Оценка схожести лиц)
- ReActorImageDublicator (Сделать из одного изображения несколько дубликатов)
- ImageRGBA2RGB (Конвертировать RGBA в RGB)
- ReActorUnload (Выгрузить модели РеАктора из VRAM)
- DLSS5FrameEnhancer (Улучшение детализации кадра с NVIDIA DLSS 5)
- ReActorCinematicEnhancer (Добавление кинематографических эффектов к изображению)
Соедините все необходимые слоты (slots) и запустите очередь (query).
### Входы основного Нода
### Входы основного Узла
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
@@ -275,7 +334,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
- `face_boost` - для соединения с ReActorFaceBoost;
- Поддерживаемые ноды: "ReActorFaceBoost";
### Выходы основного Нода
### Выходы основного Узла
- `IMAGE` - выход с готовым изображением (результатом);
- Поддерживаемые ноды: любые ноды с изображением на входе;
@@ -337,55 +396,19 @@ ReActor заменит только то лицо, которое удовлет
<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://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` (только последний коммит):
+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^!
+495 -65
View File
@@ -7,6 +7,7 @@ import torchvision.transforms as T
from torchvision.transforms.functional import normalize
from torchvision.ops import masks_to_boxes
import onnxruntime
import numpy as np
import cv2
import math
@@ -14,12 +15,12 @@ from typing import List
from PIL import Image
import io
from scipy import stats
from insightface.app.common import Face
from reactor_core.face_objects import Face
from segment_anything import sam_model_registry
from modules.processing import StableDiffusionProcessingImg2Img
from modules.shared import state
# from comfy_extras.chainner_models import model_loading
from r_modules.processing import ProcessingImg2Img
from r_modules.shared import state
import comfy.model_management as model_management
import comfy.utils
import folder_paths
@@ -55,7 +56,7 @@ from reactor_utils import (
progress_bar,
progress_bar_reset
)
from reactor_patcher import apply_patch
from r_facelib.utils.face_restoration_helper import FaceRestoreHelper
from r_basicsr.utils.registry import ARCH_REGISTRY
import scripts.r_archs.codeformer_arch
@@ -65,6 +66,9 @@ import scripts.r_masking.segs as masking_segs
import scripts.reactor_sfw as sfw
from r_dlssnr.dlss5_node import DLSS5FrameEnhancer
from r_nodes.cinematic_enhancer import ReActorCinematicEnhancer
models_dir = folder_paths.models_dir
REACTOR_MODELS_PATH = os.path.join(models_dir, "reactor")
@@ -91,6 +95,17 @@ if "ultralytics" not in folder_paths.folder_names_and_paths:
if "sams" not in folder_paths.folder_names_and_paths:
add_folder_path_and_extensions("sams", [os.path.join(models_dir, "sams")], folder_paths.supported_pt_extensions)
def apply_log_level(console_log_level):
if console_log_level == 0:
logger.setLevel(logging.WARNING)
onnxruntime.set_default_logger_severity(3) # Убивает ворнинги ORT
elif console_log_level == 1:
logger.setLevel(logging.STATUS)
onnxruntime.set_default_logger_severity(3)
elif console_log_level == 2:
logger.setLevel(logging.INFO)
onnxruntime.set_default_logger_severity(0)
def get_facemodels():
models_path = os.path.join(FACE_MODELS_PATH, "*")
models = glob.glob(models_path)
@@ -159,7 +174,7 @@ class reactor:
RETURN_TYPES = ("IMAGE","FACE_MODEL","IMAGE")
RETURN_NAMES = ("SWAPPED_IMAGE","FACE_MODEL","ORIGINAL_IMAGE")
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def __init__(self):
# self.face_helper = None
@@ -171,16 +186,39 @@ class reactor:
self.interpolation = "Bicubic"
self.boost_model_visibility = 1
self.boost_cf_weight = 0.5
self.last_swapped_bboxes = None
# self.last_swapped_indices = None
self.restore_swapped_only = True
def restore_face(
self,
input_image,
face_restore_model,
face_restore_visibility,
codeformer_weight,
facedetection,
):
self,
input_image,
face_restore_model,
face_restore_visibility,
codeformer_weight,
facedetection,
):
# from datetime import datetime
# def _current_time():
# current_datetime = datetime.now()
# return current_datetime.strftime("%M:%S")
# локальная функция IoU
def _iou(b1, b2):
xA = max(b1[0], b2[0])
yA = max(b1[1], b2[1])
xB = min(b1[2], b2[2])
yB = min(b1[3], b2[3])
interW = max(0.0, xB - xA)
interH = max(0.0, yB - yA)
interArea = interW * interH
if interArea == 0:
return 0.0
boxAArea = max(1.0, (b1[2]-b1[0]) * (b1[3]-b1[1]))
boxBArea = max(1.0, (b2[2]-b2[0]) * (b2[3]-b2[1]))
return interArea / float(boxAArea + boxBArea - interArea)
result = input_image
if face_restore_model != "none" and not model_management.processing_interrupted():
@@ -256,52 +294,96 @@ class reactor:
self.face_helper.read_image(cur_image_np)
self.face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5)
self.face_helper.align_warp_face()
# restored_face = None
restored_faces = []
# берем сохранённые bbox из swap (или None)
swapped_bboxes = getattr(self, "last_swapped_bboxes", None)
# флаги, чтобы одно сохранённое bbox не совпало с несколькими лицами
used_swapped = [False] * len(swapped_bboxes) if swapped_bboxes else None
restored_face = None
IOU_THRESHOLD = 0.5
for idx, cropped_face in enumerate(self.face_helper.cropped_faces):
# if ".pth" in face_restore_model:
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
# определяем bbox текущего лица, который дал детектор внутри FaceRestoreHelper
current_bbox = None
if hasattr(self.face_helper, 'det_faces') and len(self.face_helper.det_faces) > idx:
det = self.face_helper.det_faces[idx]
# det м.б. [x1,y1,x2,y2,score]
current_bbox = (float(det[0]), float(det[1]), float(det[2]), float(det[3]))
try:
# логика: если у нас есть сохранённые bbox — ресторим только если iou с одним из них > порог
do_restore = True
# if self.last_swapped_indices is not None:
# do_restore = idx in self.last_swapped_indices
if swapped_bboxes and self.restore_swapped_only:
do_restore = False
if current_bbox is not None:
for s_idx, sbox in enumerate(swapped_bboxes):
if used_swapped is not None and used_swapped[s_idx]:
continue
if _iou(current_bbox, sbox) >= IOU_THRESHOLD:
cx1 = (current_bbox[0] + current_bbox[2]) / 2
cy1 = (current_bbox[1] + current_bbox[3]) / 2
cx2 = (sbox[0] + sbox[2]) / 2
cy2 = (sbox[1] + sbox[3]) / 2
if abs(cx1 - cx2) < (current_bbox[2] - current_bbox[0]) * 0.25:
if abs(cy1 - cy2) < (current_bbox[3] - current_bbox[1]) * 0.25:
do_restore = True
# do_restore = True
if used_swapped is not None:
used_swapped[s_idx] = True
break
if do_restore:
# if ".pth" in face_restore_model:
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
with torch.no_grad():
try:
if ".onnx" in face_restore_model: # ONNX models
with torch.no_grad():
for ort_session_input in ort_session.get_inputs():
if ort_session_input.name == "input":
cropped_face_prep = prepare_cropped_face(cropped_face)
ort_session_inputs[ort_session_input.name] = cropped_face_prep
if ort_session_input.name == "weight":
weight = np.array([ 1 ], dtype = np.double)
ort_session_inputs[ort_session_input.name] = weight
if ".onnx" in face_restore_model: # ONNX models
output = ort_session.run(None, ort_session_inputs)[0][0]
restored_face = normalize_cropped_face(output)
for ort_session_input in ort_session.get_inputs():
if ort_session_input.name == "input":
cropped_face_prep = prepare_cropped_face(cropped_face)
ort_session_inputs[ort_session_input.name] = cropped_face_prep
if ort_session_input.name == "weight":
weight = np.array([ 1 ], dtype = np.double)
ort_session_inputs[ort_session_input.name] = weight
else: # PTH models
output = ort_session.run(None, ort_session_inputs)[0][0]
restored_face = normalize_cropped_face(output)
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
else: # PTH models
del output
torch.cuda.empty_cache()
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
except Exception as error:
del output
torch.cuda.empty_cache()
print(f"\tFailed inference: {error}", file=sys.stderr)
restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
except Exception as error:
print(f"\tFailed inference: {error}", file=sys.stderr)
# restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
restored_face = cropped_face.copy()
else:
restored_face = cropped_face.copy()
if face_restore_visibility < 1:
restored_face = cropped_face * (1 - face_restore_visibility) + restored_face * face_restore_visibility
restored_face = restored_face.astype("uint8")
self.face_helper.add_restored_face(restored_face)
self.face_helper.get_inverse_affine(None)
restored_img = self.face_helper.paste_faces_to_input_image()
@@ -328,8 +410,14 @@ class reactor:
progress_bar_reset(pbar)
# if hasattr(self, "last_swapped_bboxes"):
self.last_swapped_bboxes = None
# if hasattr(self, "last_swapped_indices"):
# self.last_swapped_indices = None
return result
def execute(self, enabled, input_image, swap_model, detect_gender_source, detect_gender_input, source_faces_index, input_faces_index, console_log_level, face_restore_model,face_restore_visibility, codeformer_weight, facedetection, source_image=None, face_model=None, faces_order=None, face_boost=None):
device = model_management.get_torch_device()
@@ -350,7 +438,7 @@ class reactor:
if faces_order is None:
faces_order = self.faces_order
apply_patch(console_log_level)
apply_log_level(console_log_level)
if not enabled:
return (input_image,face_model)
@@ -388,7 +476,8 @@ class reactor:
source = tensor_to_pil(source_image)
else:
source = None
p = StableDiffusionProcessingImg2Img(pil_images)
p = ProcessingImg2Img(pil_images)
script.process(
p=p,
img=source,
@@ -410,6 +499,10 @@ class reactor:
interpolation=self.interpolation,
)
result = batched_pil_to_tensor(p.init_images)
# print(f"bbox={p.bbox}")
if len(p.bbox) > 0:
self.last_swapped_bboxes = p.bbox
# self.last_swapped_indices = p.swapped_indexes
original_image = input_image
if face_model is None:
@@ -454,7 +547,7 @@ class ReActorPlusOpt:
RETURN_TYPES = ("IMAGE","FACE_MODEL","IMAGE")
RETURN_NAMES = ("SWAPPED_IMAGE","FACE_MODEL","ORIGINAL_IMAGE")
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def __init__(self):
# self.face_helper = None
@@ -464,6 +557,7 @@ class ReActorPlusOpt:
self.input_faces_index = "0"
self.source_faces_index = "0"
self.console_log_level = 1
self.restore_swapped_only = True
# self.face_size = 512
self.face_boost_enabled = False
self.restore = True
@@ -481,6 +575,7 @@ class ReActorPlusOpt:
self.detect_gender_source = options["detect_gender_source"]
self.input_faces_index = options["input_faces_index"]
self.source_faces_index = options["source_faces_index"]
self.restore_swapped_only = options["restore_swapped_only"]
if face_boost is not None:
self.face_boost_enabled = face_boost["enabled"]
@@ -504,19 +599,21 @@ class LoadFaceModel:
}
}
RETURN_TYPES = ("FACE_MODEL",)
RETURN_TYPES = ("FACE_MODEL","STRING")
RETURN_NAMES = ("FACE_MODEL","FACE_MODEL_NAME")
FUNCTION = "load_model"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def load_model(self, face_model):
self.face_model = face_model
face_model = face_model.split(".safetensors")[0] if ".safetensors" in face_model else face_model
self.face_models_path = FACE_MODELS_PATH
if self.face_model != "none":
face_model_path = os.path.join(self.face_models_path, self.face_model)
out = load_face_model(face_model_path)
else:
out = None
return (out, )
return (out,face_model)
class ReActorWeight:
@@ -539,7 +636,7 @@ class ReActorWeight:
OUTPUT_NODE = True
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def set_weight(self, input_image, faceswap_weight, face_model=None, source_image=None):
@@ -584,7 +681,7 @@ class ReActorWeight:
images_list: List[Image.Image] = []
apply_patch(1)
apply_log_level(0)
if len(images) > 0:
@@ -643,7 +740,7 @@ class BuildFaceModel:
OUTPUT_NODE = True
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def build_face_model(self, image: Image.Image, det_size=(640, 640)):
logging.StreamHandler.terminator = "\n"
@@ -680,7 +777,7 @@ class BuildFaceModel:
faces = []
embeddings = []
apply_patch(1)
apply_log_level(0)
if images is not None:
images_list: List[Image.Image] = batch_tensor_to_pil(images)
@@ -773,13 +870,13 @@ class SaveFaceModel:
OUTPUT_NODE = True
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def save_model(self, save_mode, face_model_name, select_face_index, image=None, face_model=None, det_size=(640, 640)):
if save_mode and image is not None:
source = tensor_to_pil(image)
source = cv2.cvtColor(np.array(source), cv2.COLOR_RGB2BGR)
apply_patch(1)
apply_log_level(0)
logger.status("Building Face Model...")
face_model_raw = analyze_faces(source, det_size)
if len(face_model_raw) == 0:
@@ -814,14 +911,275 @@ class RestoreFace:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
# def __init__(self):
# self.face_helper = None
# self.face_size = 512
CATEGORY = "🌌 ReActor/PostProcessing"
def execute(self, image, model, visibility, codeformer_weight, facedetection):
result = reactor.restore_face(self,image,model,visibility,codeformer_weight,facedetection)
result = reactor.restore_face(
self, image, model, visibility, codeformer_weight, facedetection
)
return (result,)
class RestoreFaceAdvanced:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"facedetection": (["retinaface_resnet50", "retinaface_mobile0.25", "YOLOv5l", "YOLOv5n"],),
"model": (get_model_names(get_restorers),),
"visibility": ("FLOAT", {"default": 1, "min": 0.0, "max": 1, "step": 0.05}),
"codeformer_weight": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1, "step": 0.05}),
"face_selection": (["all", "filter", "largest"],{"default": "all"}),
},
"optional": {
"sort_by": (["area", "x_position", "y_position", "detection_confidence"],{"default": "area"}),
"reverse_order": ("BOOLEAN", {"default": False}),
"take_start": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"take_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor/PostProcessing"
def execute(
self, image, model, visibility, codeformer_weight, facedetection, face_selection, sort_by="area", reverse_order=False, take_start=0, take_count=1
):
min_x_position=0.0
max_x_position=1.0
min_y_position=0.0
max_y_position=1.0
result = image
face_restore_model = model
if face_restore_model != "none" and not model_management.processing_interrupted():
global FACE_SIZE, FACE_HELPER
self.face_helper = FACE_HELPER
faceSize = 512
if "1024" in face_restore_model.lower():
faceSize = 1024
elif "2048" in face_restore_model.lower():
faceSize = 2048
logger.status(f"Restoring with {face_restore_model} | Face Size is set to {faceSize}")
model_path = folder_paths.get_full_path("facerestore_models", face_restore_model)
device = model_management.get_torch_device()
if "codeformer" in face_restore_model.lower():
codeformer_net = ARCH_REGISTRY.get("CodeFormer")(
dim_embd=512,
codebook_size=1024,
n_head=8,
n_layers=9,
connect_list=["32", "64", "128", "256"],
).to(device)
checkpoint = torch.load(model_path)["params_ema"]
codeformer_net.load_state_dict(checkpoint)
facerestore_model = codeformer_net.eval()
elif ".onnx" in face_restore_model:
ort_session = set_ort_session(model_path, providers=providers)
ort_session_inputs = {}
facerestore_model = ort_session
else:
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
facerestore_model = model_loading.load_state_dict(sd).eval()
facerestore_model.to(device)
if faceSize != FACE_SIZE or self.face_helper is None:
self.face_helper = FaceRestoreHelper(1, face_size=faceSize, crop_ratio=(1, 1), det_model=facedetection, save_ext='png', use_parse=True, device=device)
FACE_SIZE = faceSize
FACE_HELPER = self.face_helper
image_np = 255. * result.cpu().numpy()
total_images = image_np.shape[0]
out_images = []
pbar = progress_bar(total_images)
for i in range(total_images):
cur_image_np = image_np[i,:, :, ::-1]
original_resolution = cur_image_np.shape[0:2]
if facerestore_model is None or self.face_helper is None:
return result
self.face_helper.clean_all()
self.face_helper.read_image(cur_image_np)
self.face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5)
self.face_helper.align_warp_face()
# Face-Filter Mode
# Фильтрация лиц
if face_selection != "all" and self.face_helper.cropped_faces:
# Собираем информацию о лицах для фильтрации
face_info = []
img_height, img_width = cur_image_np.shape[0:2]
for j, face in enumerate(self.face_helper.cropped_faces):
# Используем центр лица вместо левого верхнего угла
if hasattr(self.face_helper, 'det_faces') and len(self.face_helper.det_faces) > j:
bbox = self.face_helper.det_faces[j]
# Вычисляем центр лица для более точного позиционирования
x1 = ((bbox[0] + bbox[2]) / 2) / img_width # центр x
y1 = ((bbox[1] + bbox[3]) / 2) / img_height # центр y
area = face.shape[0] * face.shape[1]
confidence = bbox[4] if len(bbox) > 4 else 1.0
else:
# Если информация о bbox недоступна, используем приблизительные данные
area = face.shape[0] * face.shape[1]
x1, y1 = 0.5, 0.5 # центр изображения
confidence = 1.0
face_info.append({
'index': j,
'area': area,
'x_position': x1,
'y_position': y1,
'detection_confidence': confidence
})
# Сначала сортируем все лица по выбранному критерию
all_indices = list(range(len(self.face_helper.cropped_faces)))
# Вывод для x_position и y_position
if sort_by == "y_position":
all_positions = [(idx, face_info[idx]['y_position']) for idx in all_indices]
elif sort_by == "x_position":
all_positions = [(idx, face_info[idx]['x_position']) for idx in all_indices]
# Сортировка по выбранному критерию
sorted_indices = sorted(
all_indices,
key=lambda idx: face_info[idx][sort_by],
reverse=reverse_order
)
# Отладочный вывод после сортировки
if sort_by == "y_position":
sorted_positions = [(idx, face_info[idx]['y_position']) for idx in sorted_indices]
elif sort_by == "x_position":
sorted_positions = [(idx, face_info[idx]['x_position']) for idx in sorted_indices]
# Применяем фильтрацию в зависимости от режима
if face_selection == "filter":
# Фильтрация по координатам
filtered_indices = [
idx for idx in sorted_indices
if min_x_position <= face_info[idx]['x_position'] <= max_x_position and
min_y_position <= face_info[idx]['y_position'] <= max_y_position
]
# Выборка по take_start и take_count
selected_indices = filtered_indices[take_start:take_start + take_count]
elif face_selection == "largest":
# При выборе "largest" просто берем take_count лиц с наибольшей площадью, начиная с take_start
selected_indices = sorted_indices[take_start:take_start + take_count]
elif face_selection == "index":
# В режиме "index" просто берем лица, начиная с take_start
selected_indices = sorted_indices[take_start:take_start + take_count]
if selected_indices:
self.face_helper.cropped_faces = [self.face_helper.cropped_faces[j] for j in selected_indices]
if hasattr(self.face_helper, 'restored_faces'):
self.face_helper.restored_faces = []
if hasattr(self.face_helper, 'affine_matrices'):
self.face_helper.affine_matrices = [self.face_helper.affine_matrices[j] for j in selected_indices]
if hasattr(self.face_helper, 'det_faces'):
self.face_helper.det_faces = [self.face_helper.det_faces[j] for j in selected_indices]
# Face-Filter Mode END
restored_face = None
for idx, cropped_face in enumerate(self.face_helper.cropped_faces):
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
try:
with torch.no_grad():
if ".onnx" in face_restore_model: # ONNX models
for ort_session_input in ort_session.get_inputs():
if ort_session_input.name == "input":
cropped_face_prep = prepare_cropped_face(cropped_face)
ort_session_inputs[ort_session_input.name] = cropped_face_prep
if ort_session_input.name == "weight":
weight = np.array([ 1 ], dtype = np.double)
ort_session_inputs[ort_session_input.name] = weight
output = ort_session.run(None, ort_session_inputs)[0][0]
restored_face = normalize_cropped_face(output)
else: # PTH models
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
del output
torch.cuda.empty_cache()
except Exception as error:
print(f"\tFailed inference: {error}", file=sys.stderr)
restored_face = cropped_face.copy()
if visibility < 1:
restored_face = cropped_face * (1 - visibility) + restored_face * visibility
restored_face = restored_face.astype("uint8")
self.face_helper.add_restored_face(restored_face)
self.face_helper.get_inverse_affine(None)
restored_img = self.face_helper.paste_faces_to_input_image()
restored_img = restored_img[:, :, ::-1]
if original_resolution != restored_img.shape[0:2]:
restored_img = cv2.resize(restored_img, (0, 0), fx=original_resolution[1]/restored_img.shape[1], fy=original_resolution[0]/restored_img.shape[0], interpolation=cv2.INTER_AREA)
self.face_helper.clean_all()
out_images.append(restored_img)
if state.interrupted or model_management.processing_interrupted():
logger.status("Interrupted by User")
return image
pbar.update(1)
restored_img_np = np.array(out_images).astype(np.float32) / 255.0
restored_img_tensor = torch.from_numpy(restored_img_np)
result = restored_img_tensor
progress_bar_reset(pbar)
return (result,)
@@ -867,7 +1225,7 @@ class MaskHelper:
RETURN_TYPES = ("IMAGE","MASK","IMAGE","IMAGE")
RETURN_NAMES = ("IMAGE","MASK","MASK_PREVIEW","SWAPPED_FACE")
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Masking"
def execute(self, image, swapped_image, bbox_model_name, bbox_threshold, bbox_dilation, bbox_crop_factor, bbox_drop_size, sam_model_name, sam_dilation, sam_threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative, morphology_operation, morphology_distance, blur_radius, sigma_factor, mask_optional=None):
device = model_management.get_torch_device()
@@ -1069,8 +1427,11 @@ class MaskHelper:
result = image_base.detach().clone()
face_segment = mask_image_final
pbar = progress_bar(MB)
for i in range(0, MB):
if is_empty[i]:
pbar.update(1)
continue
else:
image_index = i
@@ -1128,12 +1489,17 @@ class MaskHelper:
face_segment[...,3] = mask[i]
result = rgba2rgb_tensor(result)
result = result.cpu() # Перемещаем результат обратно на CPU
pbar.update(1)
try:
torch.cuda.empty_cache()
except:
pass
progress_bar_reset(pbar)
return (result, combined_mask, mask_blurred, face_segment)
def iterative_morphology(self, image, distance, op="dilate"):
@@ -1162,7 +1528,7 @@ class ImageDublicator:
RETURN_NAMES = ("IMAGES",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Utils"
def execute(self, image, count):
images = [image for i in range(count)]
@@ -1180,7 +1546,7 @@ class ImageRGBA2RGB:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Utils"
def execute(self, image):
out = rgba2rgb_tensor(image)
@@ -1211,7 +1577,7 @@ class MakeFaceModelBatch:
RETURN_NAMES = ("FACE_MODELS",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def execute(self, **kwargs):
if len(kwargs) > 0:
@@ -1238,14 +1604,15 @@ class ReActorOptions:
"source_faces_index": ("STRING", {"default": "0"}),
"detect_gender_source": (["no","female","male"], {"default": "no"}),
"console_log_level": ([0, 1, 2], {"default": 1}),
"restore_swapped_only": ("BOOLEAN", {"default": True, "label_off": "no", "label_on": "yes"})
}
}
RETURN_TYPES = ("OPTIONS",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def execute(self,input_faces_order, input_faces_index, detect_gender_input, source_faces_order, source_faces_index, detect_gender_source, console_log_level):
def execute(self,input_faces_order, input_faces_index, detect_gender_input, source_faces_order, source_faces_index, detect_gender_source, console_log_level, restore_swapped_only):
options: dict = {
"input_faces_order": input_faces_order,
"input_faces_index": input_faces_index,
@@ -1254,6 +1621,7 @@ class ReActorOptions:
"source_faces_index": source_faces_index,
"detect_gender_source": detect_gender_source,
"console_log_level": console_log_level,
"restore_swapped_only": restore_swapped_only,
}
return (options, )
@@ -1274,7 +1642,7 @@ class ReActorFaceBoost:
RETURN_TYPES = ("FACE_BOOST",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Face Swap"
def execute(self,enabled,boost_model,interpolation,visibility,codeformer_weight,restore_with_main_after):
face_boost: dict = {
@@ -1287,6 +1655,7 @@ class ReActorFaceBoost:
}
return (face_boost, )
class ReActorUnload:
@classmethod
def INPUT_TYPES(s):
@@ -1298,13 +1667,66 @@ class ReActorUnload:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor"
CATEGORY = "🌌 ReActor/Utils"
def execute(self, trigger):
unload_all_models()
return (trigger,)
class ReActorFaceSimilarity:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
}
}
RETURN_TYPES = ("FLOAT", "STRING")
RETURN_NAMES = ("similarity_float", "similarity_text")
FUNCTION = "compare_faces"
CATEGORY = "🌌 ReActor/Utils"
def compare_faces(self, image1, image2):
apply_log_level(0)
# 1. Конвертируем тензоры ComfyUI в формат OpenCV (BGR)
img1_cv = 255. * image1[0].cpu().numpy()
img1_cv = cv2.cvtColor(img1_cv.astype(np.uint8), cv2.COLOR_RGB2BGR)
img2_cv = 255. * image2[0].cpu().numpy()
img2_cv = cv2.cvtColor(img2_cv.astype(np.uint8), cv2.COLOR_RGB2BGR)
# 2. Ищем лица через
faces1 = analyze_faces(img1_cv, det_size=(640, 640))
faces2 = analyze_faces(img2_cv, det_size=(640, 640))
# 3. Защита от отсутствия лиц
if not faces1 or not faces2:
return (0.0, "Face not found in one or both images")
# Берем первые найденные лица
face1 = faces1[0]
face2 = faces2[0]
# 4. Вычисляем косинусное сходство (Cosine Similarity)
emb1 = face1.normed_embedding
emb2 = face2.normed_embedding
# Скалярное произведение нормализованных векторов
similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
# 5. Форматируем результат
sim_float = float(similarity)
sim_float = max(0.0, min(1.0, sim_float))
sim_text = f"{sim_float * 100:.2f}%"
return (sim_float, sim_text)
NODE_CLASS_MAPPINGS = {
# --- MAIN NODES ---
"ReActorFaceSwap": reactor,
@@ -1320,9 +1742,13 @@ NODE_CLASS_MAPPINGS = {
"ReActorMakeFaceModelBatch": MakeFaceModelBatch,
# --- Additional Nodes ---
"ReActorRestoreFace": RestoreFace,
"ReActorRestoreFaceAdvanced": RestoreFaceAdvanced,
"ReActorFaceSimilarity": ReActorFaceSimilarity,
"ReActorImageDublicator": ImageDublicator,
"ImageRGBA2RGB": ImageRGBA2RGB,
"ReActorUnload": ReActorUnload,
"DLSS5FrameEnhancer": DLSS5FrameEnhancer,
"ReActorCinematicEnhancer": ReActorCinematicEnhancer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -1340,7 +1766,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ReActorMakeFaceModelBatch": "Make Face Model Batch 🌌 ReActor",
# --- Additional Nodes ---
"ReActorRestoreFace": "Restore Face 🌌 ReActor",
"ReActorRestoreFaceAdvanced": "Restore Face Advanced 🌌 ReActor",
"ReActorFaceSimilarity": "Face Similarity 🌌 ReActor",
"ReActorImageDublicator": "Image Dublicator (List) 🌌 ReActor",
"ImageRGBA2RGB": "Convert RGBA to RGB 🌌 ReActor",
"ReActorUnload": "Unload ReActor Models 🌌 ReActor",
"DLSS5FrameEnhancer": "DLSS5 Frame Enhancer 🌌 ReActor",
"ReActorCinematicEnhancer": "Cinematic Enhancer 🌌 ReActor",
}
+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.1"
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
version = "0.7.1"
license = { file = "LICENSE" }
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.4", "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/PostProcessing"
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
View File
+307
View File
@@ -0,0 +1,307 @@
import os
import torch
import numpy as np
import cv2
import folder_paths
from pathlib import Path
from PIL import Image, ImageEnhance, ImageCms
import comfy.model_management as model_management
from scripts.reactor_logger import logger
from r_modules.shared import state
from reactor_utils import (
progress_bar,
progress_bar_reset
)
try:
from colour.io.luts.iridas_cube import read_LUT_IridasCube
AVAILABLE = True
except Exception as e:
AVAILABLE = False
logger.error(f"{e}, ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
folder_paths.add_model_folder_path("luts", os.path.join(folder_paths.models_dir, "luts"))
# Профили для HDR (создаются один раз при импорте для экономии ресурсов)
sRGB_profile = ImageCms.createProfile('sRGB')
Lab_profile = ImageCms.createProfile('LAB')
def get_luts():
luts = folder_paths.get_filename_list("luts")
luts = [x for x in luts if x.lower().endswith(".cube") or x.lower().endswith(".spi1d") or x.lower().endswith(".spi3d") or x.lower().endswith(".spimtx") or x.lower().endswith(".csp")]
luts.sort(key=str.lower)
luts.insert(0, "none")
return luts
class ReActorCinematicEnhancer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", ),
# --- Global Blend ---
"intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.5"}),
# --- LUT ---
"lut_file": (get_luts(),),
"lut_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "0..1, def: 0.5"}),
"lut_gamma_correction": ("BOOLEAN", { "default": True }),
# --- Film Effect ---
"film_grain_method": (["advanced","fast"], {"default": "advanced"}),
"film_grain_power": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
"film_grain_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.1, "tooltip": "0.1..1, def: 0.1"}),
"film_grain_sat": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.15"}),
"film_grain_shadows": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
"film_grain_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
"film_blur_strength": ("INT", {"default": 1, "min": 0, "max": 256, "step": 1, "tooltip": "(by depth_map) 0..256, def: 1"}),
"film_focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01, "tooltip": "(by depth_map) 0..1, def: 0.9"}),
"film_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1, "tooltip": "(by depth_map) 0..8, def: 2.2"}),
"film_vignette_intensity": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
# --- Color Correct ---
"cc_temperature": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
"cc_hue": ("FLOAT", {"default": 0, "min": -90, "max": 90, "step": 5, "tooltip": "-90..90, def: 0"}),
"cc_brightness": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
"cc_contrast": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
"cc_saturation": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
"cc_gamma": ("FLOAT", {"default": 1, "min": 0.2, "max": 2.2, "step": 0.1, "tooltip": "0.2..2.2, def: 1.0"}),
# --- HDR Effects ---
"hdr_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "0..5, def: 0.5"}),
"hdr_shadow_intensity": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
"hdr_highlight_intensity": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.75"}),
"hdr_contrast": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
"hdr_enhance_color": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
},
"optional": {
"depth_map": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "execute"
CATEGORY = "🌌 ReActor/PostProcessing"
DESCRIPTION = (
"Requirements:\n"
"- pip install colour-science\n"
"- put LUT-files in models/luts\n"
"depth_map is optional"
)
def execute(self, image, intensity, lut_file, lut_strength, lut_gamma_correction,
film_grain_method, film_grain_power, film_grain_scale, film_grain_sat, film_grain_shadows, film_grain_highs,film_blur_strength, film_focal_depth, film_focus_spread, film_vignette_intensity,
cc_temperature, cc_hue, cc_brightness, cc_contrast, cc_saturation, cc_gamma,
hdr_intensity, hdr_shadow_intensity, hdr_highlight_intensity, hdr_contrast, hdr_enhance_color,
depth_map=None):
if not AVAILABLE:
logger.error("ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
return (image,)
# 1. Загрузка LUT
if lut_file != "none":
lut_file_path = folder_paths.get_full_path("luts", lut_file)
lut = read_LUT_IridasCube(lut_file_path) if lut_file_path and Path(lut_file_path).exists() else None
if lut:
lut.name = lut_file
logger.status(f"LUT {lut_file} selected")
else:
lut = None
batch_size = image.shape[0]
result_batch = []
pbar = progress_bar(batch_size)
logger.status(f"Enhancing...")
for b in range(batch_size):
if state.interrupted or model_management.processing_interrupted():
logger.status("Interrupted by User")
break
# --- БАЗА: NumPy (0-1) ---
orig_np = image[b].cpu().numpy().copy()
img_np = orig_np.copy()
# --- 1. LUT ---
if lut:
lut_img = img_np.copy()
if lut_gamma_correction:
lut_img = np.clip(lut_img ** (1/2.2), 0, 1)
lut_img = lut.apply(lut_img)
if lut_gamma_correction:
lut_img = np.clip(lut_img ** 2.2, 0, 1)
if lut_strength < 1.0:
img_np = lut_strength * lut_img + (1 - lut_strength) * img_np
else:
img_np = lut_img
# Подготовка маски глубины (если есть)
depth_np = None
if depth_map is not None:
d_tensor = depth_map[b if depth_map.shape[0] > 1 else 0].cpu().numpy()
if d_tensor.shape[:2] != img_np.shape[:2]:
d_tensor = cv2.resize(d_tensor, (img_np.shape[1], img_np.shape[0]), interpolation=cv2.INTER_LINEAR)
# Если глубина RGB, берем один канал
depth_np = d_tensor[:,:,0] if d_tensor.ndim == 3 else d_tensor
# --- 2. FILM EFFECT ---
# Виньетка (OpenCV)
if film_vignette_intensity > 0:
h, w = img_np.shape[:2]
Y, X = np.ogrid[:h, :w]
center_y, center_x = h / 2, w / 2
dist_from_center = np.sqrt((X - center_x)**2 + (Y - center_y)**2)
max_dist = np.sqrt(center_x**2 + center_y**2)
vignette = 1 - (dist_from_center / max_dist) * film_vignette_intensity
img_np = np.clip(img_np * vignette[..., np.newaxis], 0, 1)
# Глубинное размытие (пошаговая реализация)
if film_blur_strength > 0 and depth_np is not None:
# Нормализуем разницу глубины относительно точки фокуса
depth_diff = np.abs(depth_np - film_focal_depth) * film_focus_spread
depth_diff = np.clip(depth_diff, 0, 1)
# Пошаговый блендинг (5 шагов для плавности)
steps = 5
blurred_fg = np.zeros_like(img_np)
for step in range(steps + 1):
step_blur = int((film_blur_strength * step / steps) // 2 * 2 + 1)
if step_blur > 1:
blur_layer = cv2.GaussianBlur(img_np, (step_blur, step_blur), 0)
else:
blur_layer = img_np
# Маска весов для текущего шага
mask = np.clip(1.0 - np.abs(depth_diff * steps - step), 0, 1)
blurred_fg += blur_layer * mask[..., np.newaxis]
img_np = blurred_fg
# --- 3. COLOR CORRECT ---
fg_img = Image.fromarray((img_np * 255).astype(np.uint8))
# Яркость и Контраст (PIL)
if cc_brightness != 0:
fg_img = ImageEnhance.Brightness(fg_img).enhance(1 + (cc_brightness / 100))
if cc_contrast != 0:
fg_img = ImageEnhance.Contrast(fg_img).enhance(1 + (cc_contrast / 100))
fg_cv = np.array(fg_img).astype(np.float32) / 255.0
# Температура
temp_val = cc_temperature / 100
if temp_val > 0:
fg_cv[:, :, 0] = np.clip(fg_cv[:, :, 0] * (1 + temp_val), 0, 1)
fg_cv[:, :, 1] = np.clip(fg_cv[:, :, 1] * (1 + temp_val * 0.4), 0, 1)
elif temp_val < 0:
fg_cv[:, :, 2] = np.clip(fg_cv[:, :, 2] * (1 - temp_val), 0, 1)
# Гамма
if cc_gamma != 1.0:
fg_cv = np.clip(np.power(fg_cv, cc_gamma), 0, 1)
# HSV для Hue и Saturation
if cc_hue != 0 or cc_saturation != 0:
hsv = cv2.cvtColor((fg_cv * 255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
if cc_hue != 0:
hsv[:, :, 0] = (hsv[:, :, 0] + (cc_hue / 2)) % 180 # В OpenCV Hue до 180
if cc_saturation != 0:
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * (1 + cc_saturation / 100), 0, 255)
fg_cv = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB).astype(np.float32) / 255.0
img_np = fg_cv
# --- 4. FILM GRAIN ---
if film_grain_power > 0:
# --- 4.1 ADVANCED NATIVE GRAIN ---
if film_grain_method == "advanced":
# 1) Генерируем базовый цветной шум
noise_rgb = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
# 2) Генерируем монохромный шум (усреднение по каналам)
noise_mono = np.mean(noise_rgb, axis=-1, keepdims=True)
# 3) Смешиваем цветность зерна (grain_sat)
noise = noise_rgb * film_grain_sat + noise_mono * (1.0 - film_grain_sat)
# 4) Масштабирование зерна
if film_grain_scale != 1.0:
h, w = img_np.shape[:2]
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
# 5) Кинематографичная маскировка по яркости (Luminance)
# Вычисляем воспринимаемую яркость пикселей
lum = 0.299 * img_np[:,:,0] + 0.587 * img_np[:,:,1] + 0.114 * img_np[:,:,2]
lum = lum[..., np.newaxis]
# Создаем карту весов шума:
# Средние тона (0.5) получают вес 1.0.
# Тени (0.0) получают вес film_grain_shadows.
# Блики (1.0) получают вес film_grain_highs.
grain_mask = np.where(
lum < 0.5,
film_grain_shadows + (1.0 - film_grain_shadows) * (lum * 2.0), # Плавный переход от теней к средним
1.0 - (1.0 - film_grain_highs) * ((lum - 0.5) * 2.0) # Плавный переход от средних к бликам
)
# Накладываем шум с учетом яркостной маски
img_np = np.clip(img_np + noise * grain_mask, 0, 1)
# --- 4.2 FAST GRAIN ---
else:
noise = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
# Масштабирование шума
if film_grain_scale != 1.0:
h, w = img_np.shape[:2]
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
img_np = np.clip(img_np + noise * 0.5, 0, 1)
# --- 5. HDR EFFECTS ---
if hdr_intensity > 0:
final_pil = Image.fromarray((img_np * 255).astype(np.uint8))
# Основная математика тонирования LAB
img_lab = ImageCms.profileToProfile(final_pil, sRGB_profile, Lab_profile, outputMode='LAB')
luminance, a, b = img_lab.split()
base = np.array(luminance, dtype=np.float32)
scaled_shadow = (hdr_shadow_intensity ** 2) * hdr_intensity
scaled_highlight = (hdr_highlight_intensity ** 2) * hdr_intensity
shadow_mask = np.clip((1 - (base / 255)) ** 2, 0, 1)
highlight_mask = np.clip((base / 255) ** 2, 0, 1)
adj_shadows = np.clip(base * (1 - shadow_mask * scaled_shadow), 0, 255)
adj_highlights = np.clip(base + (255 - base) * highlight_mask * scaled_highlight, 0, 255)
adj_lum = np.clip(adj_shadows + adj_highlights - base, 0, 255)
final_lum = np.clip(base * (1 - hdr_intensity) + adj_lum * hdr_intensity, 0, 255).astype(np.uint8)
merged_lab = Image.merge('LAB', (Image.fromarray(final_lum), a, b))
final_pil = ImageCms.profileToProfile(merged_lab, Lab_profile, sRGB_profile, outputMode='RGB')
if hdr_contrast > 0:
final_pil = ImageEnhance.Contrast(final_pil).enhance(1 + hdr_contrast)
if hdr_enhance_color > 0:
final_pil = ImageEnhance.Color(final_pil).enhance(1 + hdr_enhance_color * 0.2)
# Возвращаем HDR в рабочий массив для финального смешивания
img_np = np.array(final_pil).astype(np.float32) / 255.0
# --- 6. GLOBAL INTENSITY BLEND ---
if intensity < 1.0:
img_np = orig_np * (1.0 - intensity) + img_np * intensity
# Возврат в тензор
out_tensor = torch.from_numpy(img_np)
result_batch.append(out_tensor)
pbar.update(1)
progress_bar_reset(pbar)
return (torch.stack(result_batch),)
+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)
+1 -1
View File
@@ -7,7 +7,7 @@ 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
+2 -2
View File
@@ -1,7 +1,7 @@
albumentations>=1.4.16
insightface==0.7.3
onnx>=1.14.0
opencv-python>=4.7.0.72
numpy==1.26.4
numpy
segment_anything
ultralytics
colour-science
+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
+1 -1
View File
@@ -30,7 +30,7 @@ def ensure_nsfw_model(nsfwdet_model_path):
MODEL_EXISTS = True if downloaded == 3 else False
return MODEL_EXISTS
SCORE = 0.96
SCORE = 0.979
logging.getLogger("transformers").setLevel(logging.ERROR)
+153 -178
View File
@@ -6,17 +6,15 @@ 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 (
@@ -45,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")
@@ -61,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)
@@ -88,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
@@ -110,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)
@@ -122,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])
@@ -148,36 +142,37 @@ def get_face_gender(
operated: str,
order: str,
):
filtered_faces = [
f for f in face
if (gender_condition == 0) or
(gender_condition == 1 and f.sex == "F") or
(gender_condition == 2 and f.sex == "M")
]
gender = "Female" if gender_condition == 1 else "Male" if gender_condition == 0 else ""
if len(filtered_faces) == 0:
if gender_condition != 0:
logger.status(f"No faces found for -{gender}-")
return None, 0 # treat as "wrong gender" to skip
faces_sorted = sort_by_order(filtered_faces, order)
# 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))
return None, 0
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]
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, face_selected.sex)
# Если фильтр по полу отключен (no) - сразу отдаем лицо в работу
if gender_condition == 0:
return face_selected, 0, face_index
expected_gender = "F" if gender_condition == 1 else "M"
if gender_condition != 0 and face_selected.sex != expected_gender:
logger.info(f"{operated} Face {face_index}: WRONG gender ({face_selected.sex})")
return face_selected, 1 # <-- есть, но не тот пол
# 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)
return face_selected, 0
# Если пол не совпадает с тем, что заказал юзер
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")
@@ -189,7 +184,10 @@ def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
faces = []
try:
faces = face_analyser.get(img_data)
except:
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
@@ -223,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(
@@ -247,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:
@@ -323,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.info(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],
@@ -418,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:
@@ -462,17 +450,14 @@ 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))
@@ -502,105 +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)
progress_bar_reset(pbar)
# 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_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
# logger.status(f"Swapping...")
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
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
pbar.update(1)
elif wrong_gender == 1:
wrong_gender = 0
logger.status("Wrong target gender detected")
pbar.update(1)
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.info(f"{i}: No target face found for {face_num}")
pbar.update(1)
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.1"
version_flag = "v0.7.2-b1"
COLORS = {
"CYAN": "\033[0;36m", # CYAN