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