Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa813002f2 | ||
|
|
e5850cc5c3 | ||
|
|
5ec405e393 | ||
|
|
3725d4bf63 | ||
|
|
d7cabb2921 | ||
|
|
067ba6de36 | ||
|
|
0b377b00d4 | ||
|
|
314ad7fe9e | ||
|
|
3f66ebcba6 | ||
|
|
a12c5b19dc | ||
|
|
ea86b52e0f | ||
|
|
02a3518aa7 | ||
|
|
3746860aa4 | ||
|
|
f92ebeb653 | ||
|
|
f6776fded7 | ||
|
|
7b1233edc0 | ||
|
|
b3a0ac8cf5 | ||
|
|
de7e09db0f | ||
|
|
60b310dd7f | ||
|
|
4351dc4272 | ||
|
|
5f68562b6d | ||
|
|
b9f8683da5 | ||
|
|
78c8844f26 | ||
|
|
b01880c37f | ||
|
|
50fdb4993f | ||
|
|
8fe1c0ec6d | ||
|
|
619eb16a1e | ||
|
|
4dab1f1b0a | ||
|
|
a51e88ca24 | ||
|
|
99e23652a2 | ||
|
|
0aa14fabb4 | ||
|
|
ecd2b3c16d | ||
|
|
6ad6b35a4d | ||
|
|
2e34bf8356 | ||
|
|
be42ca37ee | ||
|
|
a7628ccdff | ||
|
|
59040c1557 | ||
|
|
e0cd862c3d | ||
|
|
fc1691dda0 | ||
|
|
33d0feff2b | ||
|
|
5881d3ee2f | ||
|
|
febdf3027a | ||
|
|
74146c26e0 | ||
|
|
55787ee169 | ||
|
|
ca24587deb | ||
|
|
11db21f666 | ||
|
|
1356f18bf7 | ||
|
|
b60036fc85 | ||
|
|
a2a61fd5e7 | ||
|
|
d93450bd80 | ||
|
|
67ce1186f8 | ||
|
|
e008d3c5ae | ||
|
|
b26892f342 | ||
|
|
b51d34e05a | ||
|
|
4ea6a541ec | ||
|
|
18fc8a303f | ||
|
|
43bafc227e | ||
|
|
85763bc0b7 | ||
|
|
b8d49816be | ||
|
|
95ed7b248f | ||
|
|
d8e3b90c82 | ||
|
|
13d836015b | ||
|
|
fd5f9e1de2 | ||
|
|
a2ad854a09 | ||
|
|
a708f71a2f | ||
|
|
6ee22a8ecb | ||
|
|
9d5dba9801 | ||
|
|
dd44fffd9e | ||
|
|
4eb66b3a8f | ||
|
|
e7dd3bbbc9 | ||
|
|
e9d34b3a91 | ||
|
|
c98dc50515 | ||
|
|
14595e4350 | ||
|
|
1ee4e21d21 | ||
|
|
0a5f8ebffd | ||
|
|
d218c52501 | ||
|
|
ec743a7de4 | ||
|
|
8e41e6c900 | ||
|
|
5c7884118b | ||
|
|
0e8b1edd09 | ||
|
|
d60458f212 | ||
|
|
9b17e4cea5 | ||
|
|
1465ec6a66 | ||
|
|
560211e3ab | ||
|
|
aa318b5073 | ||
|
|
6244ee6cdc | ||
|
|
5230d0f09b | ||
|
|
392bbfd0af | ||
|
|
48a3ad27f9 | ||
|
|
da1d284141 | ||
|
|
be2e8d549a | ||
|
|
701259b66e | ||
|
|
bc439d97a1 | ||
|
|
3626434689 | ||
|
|
2041f67349 | ||
|
|
483d56cc80 | ||
|
|
3d65ba0765 | ||
|
|
6029ffd2cc | ||
|
|
f6b4a0ebce | ||
|
|
0addca8a40 | ||
|
|
6944f5d2c0 | ||
|
|
d901609a1d | ||
|
|
54c7c27714 | ||
|
|
9d835780d7 | ||
|
|
289001af81 | ||
|
|
afc62294ac | ||
|
|
b7e30eea5e | ||
|
|
3095e627cd | ||
|
|
e573562a11 | ||
|
|
42cfb3317f | ||
|
|
5944d9a3e1 | ||
|
|
fc69c2232d | ||
|
|
4a7587a3fe | ||
|
|
7b8f84b9c0 |
@@ -7,14 +7,18 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'Gourieff' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
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.
|
||||
|
||||
@@ -3,3 +3,4 @@ __pycache__/
|
||||
.vscode/
|
||||
example
|
||||
input
|
||||
*.dll
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
|
||||
|
||||

|
||||

|
||||
|
||||
<!--<sup>
|
||||
<font color=brightred>
|
||||
|
||||
## !!! [Important Update](#latestupdate) !!!<br>Don't forget to add the Node again in existing workflows
|
||||
|
||||
</font>
|
||||
</sup>-->
|
||||
|
||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Support Me on Boosty"/>
|
||||
<br>
|
||||
@@ -20,6 +12,9 @@
|
||||
</sup>
|
||||
</a>
|
||||
|
||||
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
|
||||
<hr>
|
||||
|
||||
[](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
|
||||
@@ -34,7 +29,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
### The Fast and Simple Face Swap Extension Nodes for ComfyUI, based on [blocked ReActor](https://github.com/Gourieff/comfyui-reactor-node) - now it has a nudity detector to avoid using this software with 18+ content
|
||||
### The Fast and Simple Face Swap Extension Nodes for ComfyUI, based on [blocked ReActor](https://web.archive.org/web/20241230084620/https://github.com/Gourieff/comfyui-reactor-node) - now it has a nudity detector to avoid using this software with 18+ content
|
||||
|
||||
> By using this Node you accept and assume [responsibility](#disclaimer)
|
||||
|
||||
@@ -51,27 +46,99 @@
|
||||
|
||||
## What's new in the latest update
|
||||
|
||||
### 0.5.2 <sub><sup>BETA1</sup></sub>
|
||||
### 0.7.2 <sub><sup>BETA1</sup></sub>
|
||||
|
||||
- **New `Cinematic Enhancer` node**. Forget about plastic skin and boring pictures. This Node can help make any photorealistic image more soulful. You can add different effects (simple or advanced grain, vignette, blur, color correction, HDR) and apply LUT.<br><u>Requirements:</u>
|
||||
- Put LUT-files in `models/luts`, you can find some collection [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/luts)<br>
|
||||
- `pip install colour-science` in your Comfy Python -OR- just run install.bat (Win users) in the ReActor's folder
|
||||
|
||||
*Inspired by such nodes as: Image Apply LUT ([essentials](https://github.com/cubiq/ComfyUI_essentials)), LayerFilter: Film V2 ([LayerStyle](https://github.com/chflame163/ComfyUI_LayerStyle)), Color Correction ([post-processing-nodes](https://github.com/EllangoK/ComfyUI-post-processing-nodes)), HDR Effects ([SuperBeasts](https://github.com/SuperBeastsAI/ComfyUI-SuperBeasts))*
|
||||
|
||||
- Nodes divided into subcategories.
|
||||
|
||||
### 0.7.1
|
||||
|
||||
- **New `DLSS5 Frame Enhancer` node**. NVIDIA's latest DLSS 5 technology for enhancing image quality (both overall frame or faces by mask). <br><u>See [Installation Instructions](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>The integration operates in an isolated Host mode, fully eliminating conflicts with ComfyUI's CUDA context.<br>Huge thanks to the author of the [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) project for the C++ wrapper (neuroframe_engine.dll and neuroframe_caller.dll), which formed the base of the computational bridge for this node.<br>Also thanks Gemini 3.1 Pro (via [Google Gemini](https://gemini.google.com/app)) for the contribution.</pre><u>Requirements:</u>
|
||||
- Windows 10/11
|
||||
- NVIDIA display driver >= 616.x
|
||||
- NVIDIA RTX 40/50-series GPU<br>
|
||||
(compatibility with older RTX series is unconfirmed)
|
||||
- Other fixes and improvements
|
||||
|
||||
### 0.7.0
|
||||
|
||||
- 💥 **Big Update! ☢ New ReActor Core!**<br>✅ No `Insightface` required!<br>✅ No `C++ Build Tools` required!<br>✅ Instalation process is much easier now!<br>✅ `Numpy 2.x` friendly as well as `1.x`!<br>⚠ <u>A swap result is slightly different now</u>. Hard to say if it’s "better" or "worse" — it’s just a bit different. But if we look at the numbers (cosine similarity of face embeddings), the accuracy is actually a little higher than with Insightface.<br>More info you can find here: https://t.me/reactor_faceswap/55
|
||||
- New Node "Face Similarity" to check face likeness after you make a swap
|
||||
- HyperSwap CPU Float normalization fix (thanks @Buumcode, issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
|
||||
- Fixed: gender detection issue and face index logic (issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
|
||||
- Comfy Desktop friendly `install.bat`
|
||||
- Other fixes and improvements
|
||||
|
||||
<details>
|
||||
<summary><a>Previous versions</a></summary>
|
||||
|
||||
### 0.6.2
|
||||
|
||||
- Added support of HyperSwap models by FaceFusion Labs (thanks [@Buumcode](https://github.com/Buumcode) for contribution)<br>You can download them [here](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>and put them into the `ComfyUI\models\hyperswap` directory
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
|
||||
</center>
|
||||
|
||||
[Comparison grid](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) of Inswapper vs Reswapper vs HyperSwap
|
||||
|
||||
- Face restoration process affects only swapped faces
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
|
||||
</center>
|
||||
|
||||
- New Node "Restore Face Advanced" with Face Restore Filter, thanks https://github.com/Buumcode for implementation of "Restore Face Filter"<br>This node helps you apply the restoration process to the face(s) you need
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
|
||||
</center>
|
||||
|
||||
- Added FACE_MODEL_NAME output for "Load Face Model" node
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
|
||||
</center>
|
||||
|
||||
- Fixes and improvements
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Gender detection better logic for many faces and many indexes
|
||||
- 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!
|
||||
|
||||
<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%"/>
|
||||
<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%"/>
|
||||
<center>
|
||||
<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:
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
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)
|
||||
|
||||
### 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
|
||||
|
||||
<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 +146,12 @@ Just put them into the "models/reswapper" directory.
|
||||
- Support of ORT CoreML and ROCM EPs, just install onnxruntime version you need
|
||||
- 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%"/>
|
||||
</center>
|
||||
|
||||
- Fixes and improvements
|
||||
|
||||
<details>
|
||||
<summary><a>Previous versions</a></summary>
|
||||
|
||||
### 0.5.1
|
||||
|
||||
@@ -96,7 +165,7 @@ Just put them into the "models/reswapper" directory.
|
||||
- Sorting facemodels alphabetically
|
||||
- A lot of fixes and improvements
|
||||
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
|
||||
- Spandrel lib support for GFPGAN
|
||||
|
||||
@@ -144,7 +213,7 @@ Use this Node to gain the best results of the face swapping process:
|
||||
|
||||
- Little speed boost when analyzing target images (unfortunately it is still quite slow in compare to swapping and restoring...)
|
||||
|
||||
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
|
||||
- GPEN-BFR-512 and RestoreFormer_Plus_Plus face restoration models support
|
||||
|
||||
@@ -167,12 +236,12 @@ Result example (the new face was created from 4 faces of different actresses):
|
||||
|
||||
Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
|
||||
|
||||
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
|
||||
- CUDA 12 Support - don't forget to run (Windows) `install.bat` or (Linux/MacOS) `install.py` for ComfyUI's Python enclosure or try to install ORT-GPU for CU12 manually (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
|
||||
- Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173 fix
|
||||
- Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173) fix
|
||||
|
||||
- Separate Node for the Face Restoration postprocessing (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), can be found inside ReActor's menu (RestoreFace Node)
|
||||
- Separate Node for the Face Restoration postprocessing (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), can be found inside ReActor's menu (RestoreFace Node)
|
||||
- (Windows) Installation can be done for Python from the System's PATH
|
||||
- Different fixes and improvements
|
||||
|
||||
@@ -180,7 +249,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
|
||||
|
||||
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
|
||||
- Input "input_image" goes first now, it gives a correct bypass and also it is right to have the main input first;
|
||||
- You can now save face models as "safetensors" files (`ComfyUI\models\reactor\faces`) and load them into ReActor implementing different scenarios and keeping super lightweight face models of the faces you use:
|
||||
@@ -201,47 +270,25 @@ Thanks to everyone who finds bugs, suggests new features and supports this proje
|
||||
|
||||
## Installation
|
||||
|
||||
<details>
|
||||
<summary>SD WebUI: <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/">AUTOMATIC1111</a> or <a href="https://github.com/vladmandic/automatic">SD.Next</a></summary>
|
||||
### Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows
|
||||
|
||||
1. Close (stop) your SD-WebUI/Comfy Server if it's running
|
||||
2. (For Windows Users):
|
||||
- Install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community version - you need this step to build Insightface)
|
||||
- OR only [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and select "Desktop Development with C++" under "Workloads -> Desktop & Mobile"
|
||||
- OR if you don't want to install VS or VS C++ BT - follow [this steps (sec. I)](#insightfacebuild)
|
||||
3. Go to the `extensions\sd-webui-comfyui\ComfyUI\custom_nodes`
|
||||
4. Open Console or Terminal and run `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||
5. Go to the SD WebUI root folder, open Console or Terminal and run (Windows users)`.\venv\Scripts\activate` or (Linux/MacOS)`venv/bin/activate`
|
||||
6. `python -m pip install -U pip`
|
||||
7. `cd extensions\sd-webui-comfyui\ComfyUI\custom_nodes\ComfyUI-ReActor`
|
||||
8. `python install.py`
|
||||
9. Please, wait until the installation process will be finished
|
||||
10. (From the version 0.3.0) Download additional facerestorers models from the link below and put them into the `extensions\sd-webui-comfyui\ComfyUI\models\facerestore_models` directory:<br>
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models
|
||||
11. Run SD WebUI and check console for the message that ReActor Node is running:
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/console_status_running.jpg?raw=true" alt="console_status_running" width="759"/>
|
||||
|
||||
12. Go to the ComfyUI tab and find there ReActor Node inside the menu `ReActor` or by using a search:
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/webui-demo.png?raw=true" alt="webui-demo" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/search-demo.png?raw=true" alt="webui-demo" width="1043"/>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Standalone (Portable) <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> for Windows</summary>
|
||||
|
||||
1. Do the following:
|
||||
- Install [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community version - you need this step to build Insightface)
|
||||
- OR only [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and select "Desktop Development with C++" under "Workloads -> Desktop & Mobile"
|
||||
- OR if you don't want to install VS or VS C++ BT - follow [this steps (sec. I)](#insightfacebuild)
|
||||
2. Choose between two options:
|
||||
1. Choose between two options:
|
||||
- (ComfyUI Manager) Open ComfyUI Manager, click "Install Custom Nodes", type "ReActor" in the "Search" field and then click "Install". After ComfyUI will complete the process - please restart the Server.
|
||||
- (Manually) Go to `ComfyUI\custom_nodes`, open Console and run `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||
3. Go to `ComfyUI\custom_nodes\ComfyUI-ReActor` and run `install.bat`
|
||||
4. If you don't have the "face_yolov8m.pt" Ultralytics model - you can download it from the [Assets](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) and put it into the "ComfyUI\models\ultralytics\bbox" directory<br>As well as one or both of "Sams" models from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - download (if you don't have them) and put into the "ComfyUI\models\sams" directory
|
||||
5. Run ComfyUI and find there ReActor Nodes inside the menu `ReActor` or by using a search
|
||||
2. Go to `ComfyUI\custom_nodes\ComfyUI-ReActor` and run `install.bat`
|
||||
3. Download required models from the Section below
|
||||
4. Run ComfyUI and find there ReActor Nodes inside the menu `ReActor` or by using a search
|
||||
|
||||
</details>
|
||||
|
||||
## Models
|
||||
|
||||
- buffalo_l: downloaded on first launch into `ComfyUI\models\insightface\models\buffalo_l`, or you can download manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- inswapper_128: downloaded during installation into `ComfyUI\models\insightface`, or you can download manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models put them into `ComfyUI\models\reswapper`
|
||||
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) put them into `ComfyUI\models\hyperswap`
|
||||
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models put any you like into `ComfyUI\models\facerestore_models`
|
||||
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt put into `ComfyUI\models\ultralytics\bbox`
|
||||
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams put into `ComfyUI\models\sams`
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -249,11 +296,12 @@ You can find ReActor Nodes inside the menu `ReActor` or by using a search (just
|
||||
|
||||
List of Nodes:
|
||||
- ••• Main Nodes •••
|
||||
- ReActorFaceSwap (Main Node)
|
||||
- ReActorFaceSwapOpt (Main Node with the additional Options input)
|
||||
- ReActorOptions (Options for ReActorFaceSwapOpt)
|
||||
- ReActorFaceBoost (Face Booster Node)
|
||||
- ReActorMaskHelper (Masking Helper)
|
||||
- ReActorFaceSwap (Main Node)
|
||||
- ReActorFaceSwapOpt (Main Node with the additional Options input)
|
||||
- ReActorOptions (Options for ReActorFaceSwapOpt)
|
||||
- ReActorFaceBoost (Face Booster Node)
|
||||
- ReActorMaskHelper (Masking Helper)
|
||||
- ReActorSetWeight (Set Face Swap Weight)
|
||||
- ••• Operations with Face Models •••
|
||||
- ReActorSaveFaceModel (Save Face Model)
|
||||
- ReActorLoadFaceModel (Load Face Model)
|
||||
@@ -261,8 +309,13 @@ List of Nodes:
|
||||
- ReActorMakeFaceModelBatch (Make Face Model Batch)
|
||||
- ••• Additional Nodes •••
|
||||
- ReActorRestoreFace (Face Restoration)
|
||||
- ReActorRestoreFaceAdvanced (Restore Face Advanced)
|
||||
- ReActorFaceSimilarity (Face Similarity)
|
||||
- ReActorImageDublicator (Dublicate one Image to Images List)
|
||||
- ImageRGBA2RGB (Convert RGBA to RGB)
|
||||
- ReActorUnload (Unload ReActor models from VRAM)
|
||||
- DLSS5FrameEnhancer (Enhance frame quality with NVIDIA DLSS 5)
|
||||
- ReActorCinematicEnhancer (Add cinematic effects to any image)
|
||||
|
||||
Connect all required slots and run the query.
|
||||
|
||||
@@ -274,6 +327,10 @@ Connect all required slots and run the query.
|
||||
- Supported Nodes: "Load Image" or any other nodes providing images as an output;
|
||||
- `face_model` - is the input for the "Load Face Model" Node or another ReActor node to provide a face model file (face embedding) you created earlier via the "Save Face Model" Node;
|
||||
- Supported Nodes: "Load Face Model", "Build Blended Face Model";
|
||||
- `options` - to connect ReActorOptions;
|
||||
- Supported Nodes: "ReActorOptions";
|
||||
- `face_boost` - to connect ReActorFaceBoost;
|
||||
- Supported Nodes: "ReActorFaceBoost";
|
||||
|
||||
### Main Node Outputs
|
||||
|
||||
@@ -281,6 +338,7 @@ Connect all required slots and run the query.
|
||||
- Supported Nodes: any nodes which have images as an input;
|
||||
- `FACE_MODEL` - is an output providing a source face's model being built during the swapping process;
|
||||
- Supported Nodes: "Save Face Model", "ReActor", "Make Face Model Batch";
|
||||
- `ORIGINAL_IMAGE` - `input_image` bypass;
|
||||
|
||||
### Face Restoration
|
||||
|
||||
@@ -309,59 +367,43 @@ Since version 0.4.0 you can save face models as "safetensors" files (stored in `
|
||||
To make new models appear in the list of the "Load Face Model" Node - just refresh the page of your ComfyUI web application.<br>
|
||||
(I recommend you to use ComfyUI Manager - otherwise you workflow can be lost after you refresh the page if you didn't save it before that).
|
||||
|
||||
### Masking Helper
|
||||
|
||||
Face Masking feature is available since version 0.5.0, just add the "ReActorMaskHelper" Node to the workflow and connect it as shown below:
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-01.jpg?raw=true" alt="0.5.0-whatsnew-01" width="100%"/>
|
||||
|
||||
If you don't have the "face_yolov8m.pt" Ultralytics model - you can download it from the [Assets](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) and put it into the "ComfyUI\models\ultralytics\bbox" directory
|
||||
<br>
|
||||
As well as ["sam_vit_b_01ec64.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_b_01ec64.pth) or ["sam_vit_l_0b3195.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_l_0b3195.pth) (better occlusion) - download (if you don't have it) and put it into the "ComfyUI\models\sams" directory;
|
||||
|
||||
Use this Node to gain the best results of the face swapping process:
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-02.jpg?raw=true" alt="0.5.0-whatsnew-02" width="100%"/>
|
||||
|
||||
### Face Swap Weigth
|
||||
|
||||
You can set the strength of face swap for `source_image` or `face_model` from 0% to 100% (in 12.5% step) with `ReActorSetWeight` node
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||
</center>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<a name="insightfacebuild">
|
||||
|
||||
### **I. (For Windows users) If you still cannot build Insightface for some reasons or just don't want to install Visual Studio or VS C++ Build Tools - do the following:**
|
||||
|
||||
1. (ComfyUI Portable) From the root folder check the version of Python:<br>run CMD and type `python_embeded\python.exe -V`
|
||||
2. Download prebuilt Insightface package [for Python 3.10](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp310-cp310-win_amd64.whl) or [for Python 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (if in the previous step you see 3.11) or [for Python 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (if in the previous step you see 3.12) and put into the stable-diffusion-webui (A1111 or SD.Next) root folder (where you have "webui-user.bat" file) or into ComfyUI root folder if you use ComfyUI Portable
|
||||
3. From the root folder run:
|
||||
- (SD WebUI) CMD and `.\venv\Scripts\activate`
|
||||
- (ComfyUI Portable) run CMD
|
||||
4. Then update your PIP:
|
||||
- (SD WebUI) `python -m pip install -U pip`
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
|
||||
5. Then install Insightface:
|
||||
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (for 3.10) or `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (for 3.11) or `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (for 3.10) or `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (for 3.11) or `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
6. Enjoy!
|
||||
|
||||
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
|
||||
This error may occur if there's smth wrong with the model file `inswapper_128.onnx`
|
||||
|
||||
Try to download it manually from [here](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx)
|
||||
Try to download it manually from [here](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
|
||||
and put it to the `ComfyUI\models\insightface` replacing existing one
|
||||
|
||||
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
|
||||
This means that input points have been changed with the latest update<br>
|
||||
Remove the current ReActor Node from your workflow and add it again
|
||||
|
||||
### **IV. ControlNet Aux Node IMPORT failed error when using with ReActor Node**
|
||||
|
||||
1. Close ComfyUI if it runs
|
||||
2. Go to the ComfyUI root folder, open CMD there and run:
|
||||
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
|
||||
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
|
||||
3. That's it!
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
|
||||
|
||||
### **V. "ModuleNotFoundError: No module named 'basicsr'" or "subprocess-exited-with-error" during future-0.18.3 installation**
|
||||
|
||||
- Download https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
|
||||
- Put it to ComfyUI root And run:
|
||||
|
||||
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
|
||||
|
||||
- Then:
|
||||
|
||||
python_embeded\python.exe -m pip install basicsr
|
||||
|
||||
### **VI. "fatal: fetch-pack: invalid index-pack output" when you try to `git clone` the repository"**
|
||||
### **III. "fatal: fetch-pack: invalid index-pack output" when you try to `git clone` the repository"**
|
||||
|
||||
Try to clone with `--depth=1` (last commit only):
|
||||
|
||||
@@ -460,8 +502,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) |  |
|
||||
| [GFPGANv1.3.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.3.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) |  |
|
||||
| [GFPGANv1.4.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.4.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) |  |
|
||||
| [inswapper_128.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) |  |
|
||||
| [inswapper_128_fp16.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) |  |
|
||||
| [inswapper_128.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) |  |
|
||||
| [inswapper_128_fp16.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) |  |
|
||||
|
||||
[BasicSR](https://github.com/XPixelGroup/BasicSR) - [@XPixelGroup](https://github.com/XPixelGroup) <br>
|
||||
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
|
||||
|
||||
+168
-127
@@ -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"/>
|
||||
|
||||

|
||||
|
||||
<!--<sup>
|
||||
<font color=brightred>
|
||||
|
||||
## !!! [Важные изменения](#latestupdate) !!!<br>Не забудьте добавить Нод заново в существующие воркфлоу
|
||||
|
||||
</font>
|
||||
</sup>-->
|
||||

|
||||
|
||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
||||
@@ -20,6 +12,9 @@
|
||||
</sup>
|
||||
</a>
|
||||
|
||||
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
|
||||
<hr>
|
||||
|
||||
[](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
|
||||
@@ -34,7 +29,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://github.com/Gourieff/comfyui-reactor-node) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
||||
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://web.archive.org/web/20241126185020/https://github.com/Gourieff/comfyui-reactor-node/blob/main/README_RU.md) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
||||
|
||||
> Используя данное ПО, вы понимаете и принимаете [ответственность](#disclaimer)
|
||||
|
||||
@@ -51,27 +46,99 @@
|
||||
|
||||
## Что нового в последнем обновлении
|
||||
|
||||
### 0.5.2 <sub><sup>BETA1</sup></sub>
|
||||
### 0.7.2 <sub><sup>BETA1</sup></sub>
|
||||
|
||||
- **Новый узел `Cinematic Enhancer`**. Забудьте о пластиковой коже и скучной картинке. Этот узел поможет вдохнуть душу в любое фотореалистичное изображение. Вы можете добавлять разные эффекты (простое и продвинутое зерно, виньетку, размытие, коррекцию цвета, HDR) и применить LUT.<br><u>Требования:</u>
|
||||
- Положите LUT-файлы в `models/luts`, небольшую коллекцию можете найти [тут](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/luts)<br>
|
||||
- `pip install colour-science` в Пайтон вашего Comfy -ИЛИ- просто запустите install.bat (пользователи Win) внутри папки РеАктора
|
||||
|
||||
*Вдохновлено такими узлами как: Image Apply LUT ([essentials](https://github.com/cubiq/ComfyUI_essentials)), LayerFilter: Film V2 ([LayerStyle](https://github.com/chflame163/ComfyUI_LayerStyle)), Color Correction ([post-processing-nodes](https://github.com/EllangoK/ComfyUI-post-processing-nodes)), HDR Effects ([SuperBeasts](https://github.com/SuperBeastsAI/ComfyUI-SuperBeasts))*
|
||||
|
||||
- Разделение узлов на подкатегории.
|
||||
|
||||
### 0.7.1
|
||||
|
||||
- **Новый узел `DLSS5 Frame Enhancer`**. Новейшая технология DLSS 5 от NVIDIA для улучшения качества изображения (кадра целиком или лиц по маске).<br><u>Следуйте [инструкции по установке](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>Интеграция работает в изолированном Host-режиме, что полностью исключает конфликты с CUDA-контекстом ComfyUI.<br>Огромная благодарность автору проекта [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) за C++ обертку (neuroframe_engine.dll и neuroframe_caller.dll), которая легла в основу вычислительного моста для данного узла.<br>Также благодарность Gemini 3.1 Pro (через [Google Gemini](https://gemini.google.com/app)) за ассистирование и помощь.</pre><u>Требования:</u>
|
||||
- Windows 10/11
|
||||
- NVIDIA display driver >= 616.x
|
||||
- NVIDIA RTX 40/50-series GPU
|
||||
(совместимость с более старыми сериями RTX не подтверждена)
|
||||
- Прочие улучшение и исправления
|
||||
|
||||
### 0.7.0
|
||||
|
||||
- 💥 **Важное обновление! ☢ Новое ядро РеАктора!**<br>✅ Библиотека `Insightface` больше не требуется!<br>✅ `C++ Build Tools` больше не требуются!<br>✅ Более простой процесс установки!<br>✅ Поддержка `Numpy 2.x` и `1.x`!<br>⚠ <u>Результат Свапа теперь слегка отличается</u>. Сложно сказать, хуже или лучше — результат просто чуть-чуть другой. По показателям схожести (оценка косинусного сходства векторов эмбеддингов лиц) результат стал немного лучше (хоть и незначительно), чем с Insightface.<br>Подробнее здесь: https://t.me/reactor_faceswap/55
|
||||
- Новый узел "Face Similarity" для проверки схожести лиц после свапа
|
||||
- Исправление "HyperSwap CPU Float normalization" (спасибо @Buumcode, Issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
|
||||
- Исправлено: проблема определения пола и логика индексов лиц (Issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
|
||||
- Улучшен `install.bat` для поддержки запуска установки в Comfy Desktop
|
||||
- Прочие улучшение и исправления
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
|
||||
### 0.6.2
|
||||
|
||||
- Добавлена поддержка моделей HyperSwap от команды FaceFusion Labs (спасибо [@Buumcode](https://github.com/Buumcode) за вариант реализации)<br>Модели можно скачать [отсюда](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>и положить в папку `ComfyUI/models/hyperswap`
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
|
||||
</center>
|
||||
|
||||
[Сравнение](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) моделей Inswapper, Reswapper, HyperSwap
|
||||
|
||||
- Теперь восстановление лиц затрагивает только заменённые лица
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01-ru.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
|
||||
</center>
|
||||
|
||||
- Новый узел "Restore Face Advanced" с фильтром по лицам, спасибо https://github.com/Buumcode за реализацию "Фильтра восстановления лиц"<br>Этот узел помогает применить восстановление именно к нужному лицу или лицам
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
|
||||
</center>
|
||||
|
||||
- Добавлен выход FACE_MODEL_NAME для узла "Load Face Model"
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
|
||||
</center>
|
||||
|
||||
- Исправления и улучшения
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Улучшенная логика работы с индексами множества лиц при определении пола
|
||||
- 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! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
|
||||
|
||||
<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%"/>
|
||||
<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%"/>
|
||||
<center>
|
||||
<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 можно отсюда:
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
Сохраните их в директорию "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)
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA2</sup></sub>
|
||||
|
||||
- Небольшие исправления
|
||||
|
||||
### 0.5.2 <sub><sup>ALPHA1</sup></sub>
|
||||
|
||||
- Новый нод "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%"/>
|
||||
@@ -79,10 +146,11 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
- Поддержка ORT CoreML and ROCM EPs, достаточно установить ту версию onnxruntime, которая соответствует вашему 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%"/>
|
||||
</center>
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
- Исправления и улучшения
|
||||
|
||||
### 0.5.1
|
||||
|
||||
@@ -96,7 +164,7 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
- Сортировка моделей лиц по алфавиту
|
||||
- Множество исправлений и улучшений
|
||||
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
|
||||
- Поддержка библиотеки Spandrel при работе с GFPGAN
|
||||
|
||||
@@ -144,7 +212,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
- Небольшое улучшение скорости анализа целевых изображений (input)
|
||||
|
||||
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
|
||||
- Добавлена поддержка GPEN-BFR-512 и RestoreFormer_Plus_Plus моделей восстановления лиц
|
||||
|
||||
@@ -167,12 +235,12 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
Базовый воркфлоу [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
|
||||
|
||||
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
|
||||
- Поддержка CUDA 12 - не забудьте запустить (Windows) `install.bat` или (Linux/MacOS) `install.py` для используемого Python окружения или попробуйте установить ORT-GPU для CU12 вручную (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
|
||||
- Исправление Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173
|
||||
- Исправление Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173)
|
||||
|
||||
- Отдельный Нод для восстаноления лиц (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), располагается внутри меню ReActor (нод RestoreFace)
|
||||
- Отдельный Нод для восстаноления лиц (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), располагается внутри меню ReActor (нод RestoreFace)
|
||||
- (Windows) Установка зависимостей теперь может быть выполнена в Python из PATH ОС
|
||||
- Разные исправления и улучшения
|
||||
|
||||
@@ -180,7 +248,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
|
||||
|
||||
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
|
||||
- Вход "input_image" теперь идёт первым, это даёт возможность корректного байпаса, а также это правильно с точки зрения расположения входов (главный вход - первый);
|
||||
- Теперь можно сохранять модели лиц в качестве файлов "safetensors" (`ComfyUI\models\reactor\faces`) и загружать их в ReActor, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
|
||||
@@ -203,48 +271,24 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
## Установка
|
||||
|
||||
<details>
|
||||
<summary>SD WebUI: <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/">AUTOMATIC1111</a> или <a href="https://github.com/vladmandic/automatic">SD.Next</a></summary>
|
||||
### Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows
|
||||
|
||||
1. Закройте (остановите) SD-WebUI Сервер, если запущен
|
||||
2. (Для пользователей Windows):
|
||||
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
|
||||
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
|
||||
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
|
||||
3. Перейдите в `extensions\sd-webui-comfyui\ComfyUI\custom_nodes`
|
||||
4. Откройте Консоль или Терминал и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||
5. Перейдите в корневую директорию SD WebUI, откройте Консоль или Терминал и выполните (для пользователей Windows)`.\venv\Scripts\activate` или (для пользователей Linux/MacOS)`venv/bin/activate`
|
||||
6. `python -m pip install -U pip`
|
||||
7. `cd extensions\sd-webui-comfyui\ComfyUI\custom_nodes\ComfyUI-ReActor`
|
||||
8. `python install.py`
|
||||
9. Пожалуйста, дождитесь полного завершения установки
|
||||
10. (Начиная с версии 0.3.0) Скачайте дополнительные модели восстановления лиц (по ссылке ниже) и сохраните их в папку `extensions\sd-webui-comfyui\ComfyUI\models\facerestore_models`:<br>
|
||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models
|
||||
11. Запустите SD WebUI и проверьте консоль на сообщение, что ReActor Node работает:
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/console_status_running.jpg?raw=true" alt="console_status_running" width="759"/>
|
||||
|
||||
12. Перейдите во вкладку ComfyUI и найдите там ReActor Node внутри меню `ReActor` или через поиск:
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/webui-demo.png?raw=true" alt="webui-demo" width="100%"/>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/search-demo.png?raw=true" alt="webui-demo" width="1043"/>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows</summary>
|
||||
|
||||
1. Сделайте следующее:
|
||||
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
|
||||
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
|
||||
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
|
||||
2. Выберите из двух вариантов:
|
||||
1. Выберите из двух вариантов:
|
||||
- (ComfyUI Manager) Откройте ComfyUI Manager, нажвите "Install Custom Nodes", введите "ReActor" в поле "Search" и далее нажмите "Install". После того, как ComfyUI завершит установку, перезагрузите сервер.
|
||||
- (Вручную) Перейдите в `ComfyUI\custom_nodes`, откройте Консоль и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||
3. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
||||
4. Если модель "face_yolov8m.pt" у вас отсутствует - можете скачать её [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положить в папку "ComfyUI\models\ultralytics\bbox"<br>
|
||||
То же самое и с "Sams" моделями, скачайте одну или обе [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - и положите в папку "ComfyUI\models\sams"
|
||||
5. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
||||
2. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
||||
3. Скачайте необходимые модели из Раздела "Модели" ниже
|
||||
4. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
||||
|
||||
</details>
|
||||
## Модели
|
||||
|
||||
- buffalo_l: скачиваются при первом запуске в `ComfyUI\models\insightface\models\buffalo_l`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- inswapper_128: скачивается при установке в `ComfyUI\models\insightface`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models скачать в `ComfyUI\models\reswapper`
|
||||
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) скачать в `ComfyUI\models\hyperswap`
|
||||
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models скачать любые предпочитаемые в `ComfyUI\models\facerestore_models`
|
||||
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt скачать в `ComfyUI\models\ultralytics\bbox`
|
||||
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams скачать в `ComfyUI\models\sams`
|
||||
|
||||
<a name="usage">
|
||||
|
||||
@@ -253,25 +297,31 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
|
||||
Вы можете найти ноды ReActor внутри меню `ReActor` или через поиск (достаточно ввести "ReActor" в поисковой строке)
|
||||
|
||||
Список нодов:
|
||||
- ••• Main Nodes •••
|
||||
- ReActorFaceSwap (Основной нод)
|
||||
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
|
||||
- ReActorOptions (Опции для ReActorFaceSwapOpt)
|
||||
- ReActorFaceBoost (Нод Face Booster)
|
||||
- ReActorMaskHelper (Masking Helper)
|
||||
- ••• Operations with Face Models •••
|
||||
- ReActorSaveFaceModel (Save Face Model)
|
||||
- ReActorLoadFaceModel (Load Face Model)
|
||||
- ReActorBuildFaceModel (Build Blended Face Model)
|
||||
- ReActorMakeFaceModelBatch (Make Face Model Batch)
|
||||
- ••• Additional Nodes •••
|
||||
- ReActorRestoreFace (Face Restoration)
|
||||
- ReActorImageDublicator (Dublicate one Image to Images List)
|
||||
- ImageRGBA2RGB (Convert RGBA to RGB)
|
||||
- ••• Основные •••
|
||||
- ReActorFaceSwap (Основной нод)
|
||||
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
|
||||
- ReActorOptions (Опции для ReActorFaceSwapOpt)
|
||||
- ReActorFaceBoost (Face Booster)
|
||||
- ReActorMaskHelper (Masking Helper)
|
||||
- ReActorSetWeight (Задать замены лица)
|
||||
- ••• Работа с моделями лиц •••
|
||||
- ReActorSaveFaceModel (Сохранить модель лица)
|
||||
- ReActorLoadFaceModel (Загрузить модель лица)
|
||||
- ReActorBuildFaceModel (Построить смешанную модель лица)
|
||||
- ReActorMakeFaceModelBatch (Создать пачку моделей лиц)
|
||||
- ••• Дополнительные •••
|
||||
- ReActorRestoreFace (Восстановление лиц)
|
||||
- ReActorRestoreFaceAdvanced (Восстановление лиц продвинутое)
|
||||
- ReActorFaceSimilarity (Оценка схожести лиц)
|
||||
- ReActorImageDublicator (Сделать из одного изображения несколько дубликатов)
|
||||
- ImageRGBA2RGB (Конвертировать RGBA в RGB)
|
||||
- ReActorUnload (Выгрузить модели РеАктора из VRAM)
|
||||
- DLSS5FrameEnhancer (Улучшение детализации кадра с NVIDIA DLSS 5)
|
||||
- ReActorCinematicEnhancer (Добавление кинематографических эффектов к изображению)
|
||||
|
||||
Соедините все необходимые слоты (slots) и запустите очередь (query).
|
||||
|
||||
### Входы основного Нода
|
||||
### Входы основного Узла
|
||||
|
||||
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
|
||||
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
|
||||
@@ -279,13 +329,18 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
|
||||
- Поддерживаемые ноды: "Load Image" или любые другие ноды с выходом Image(s);
|
||||
- `face_model` - это вход для выхода с нода "Load Face Model" или другого нода ReActor для загрузки модели лица (face model или face embedding), которое вы создали ранее через нод "Save Face Model";
|
||||
- Поддерживаемые ноды: "Load Face Model", "Build Blended Face Model";
|
||||
- `options` - для соединения с ReActorOptions;
|
||||
- Поддерживаемые ноды: "ReActorOptions";
|
||||
- `face_boost` - для соединения с ReActorFaceBoost;
|
||||
- Поддерживаемые ноды: "ReActorFaceBoost";
|
||||
|
||||
### Выходы основного Нода
|
||||
### Выходы основного Узла
|
||||
|
||||
- `IMAGE` - выход с готовым изображением (результатом);
|
||||
- Поддерживаемые ноды: любые ноды с изображением на входе;
|
||||
- `FACE_MODEL` - выход, предоставляющий модель лица, построенную в ходе замены;
|
||||
- Поддерживаемые ноды: "Save Face Model", "ReActor", "Make Face Model Batch";
|
||||
- `ORIGINAL_IMAGE` - `input_image` байпас;
|
||||
|
||||
### Восстановление лиц
|
||||
|
||||
@@ -313,61 +368,47 @@ ReActor заменит только то лицо, которое удовлет
|
||||
Чтобы новые модели появились в списке моделей нода "Load Face Model" - обновите страницу of с ComfyUI.<br>
|
||||
(Рекомендую использовать ComfyUI Manager - иначе ваше воркфлоу может быть потеряно после перезагрузки страницы, если вы не сохранили его).
|
||||
|
||||
### Masking Helper
|
||||
|
||||
Нод доступен с версии 0.5.0, просто добавьте "ReActorMaskHelper" в рабочий процесс и соедините как показано ниже:
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-01.jpg?raw=true" alt="0.5.0-whatsnew-01" width="100%"/>
|
||||
|
||||
Если модель "face_yolov8m.pt" отсутствует - скачайте [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положите в папку "ComfyUI\models\ultralytics\bbox"
|
||||
<br>
|
||||
Также и ["sam_vit_b_01ec64.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_b_01ec64.pth) или ["sam_vit_l_0b3195.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_l_0b3195.pth) (лучше окклюзия) - скачайте (если не качали ранее) и положите в папку "ComfyUI\models\sams";
|
||||
|
||||
Используйте этот нод для улучшенного результата при замене лиц:
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-02.jpg?raw=true" alt="0.5.0-whatsnew-02" width="100%"/>
|
||||
|
||||
### Сила замены лица
|
||||
|
||||
Для входов `source_image` or `face_model` можно задать силу замены лица от 0% до 100% (с шагом 12.5%) с помощью нода `ReActorSetWeight`
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||
</center>
|
||||
|
||||
<a name="troubleshooting">
|
||||
|
||||
## Устранение проблем
|
||||
|
||||
<a name="insightfacebuild">
|
||||
|
||||
### **I. (Для пользователей Windows) Если вы до сих пор не можете установить пакет Insightface по каким-то причинам или же просто не желаете устанавливать Visual Studio или VS C++ Build Tools - сделайте следующее:**
|
||||
|
||||
1. (ComfyUI Portable) Находясь в корневой директории, проверьте версию Python:<br>запустите CMD и выполните `python_embeded\python.exe -V`<br>Вы должны увидеть версию или 3.10, или 3.11, или 3.12
|
||||
2. Скачайте готовый пакет Insightface [для версии 3.10](https://github.com/Gourieff/sd-webui-reactor/raw/main/example/insightface-0.7.3-cp310-cp310-win_amd64.whl) или [для 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (если на предыдущем шаге вы увидели 3.11) или [для 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (если на предыдущем шаге вы увидели 3.12) и сохраните его в корневую директорию stable-diffusion-webui (A1111 или SD.Next) - туда, где лежит файл "webui-user.bat" -ИЛИ- в корневую директорию ComfyUI, если вы используете ComfyUI Portable
|
||||
3. Из корневой директории запустите:
|
||||
- (SD WebUI) CMD и `.\venv\Scripts\activate`
|
||||
- (ComfyUI Portable) CMD
|
||||
4. Обновите PIP:
|
||||
- (SD WebUI) `python -m pip install -U pip`
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
|
||||
5. Затем установите Insightface:
|
||||
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
6. Готово!
|
||||
|
||||
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
|
||||
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
|
||||
|
||||
Скачайте вручную по ссылке [отсюда](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx)
|
||||
Скачайте вручную по ссылке [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
|
||||
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
|
||||
|
||||
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
|
||||
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
|
||||
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
|
||||
|
||||
### **IV. ControlNet Aux Node IMPORT failed - при использовании совместно с нодом ReActor**
|
||||
|
||||
1. Закройте или остановите ComfyUI сервер, если он запущен
|
||||
2. Перейдите в корневую папку ComfyUI, откройте консоль CMD и выполните следующее:
|
||||
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
|
||||
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
|
||||
3. Готово!
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
|
||||
|
||||
### **V. "ModuleNotFoundError: No module named 'basicsr'" или "subprocess-exited-with-error" при установке пакета future-0.18.3**
|
||||
|
||||
- Скачайте https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
|
||||
- Скопируйте файл в корневую папку ComfyUI и выполните в консоли:
|
||||
|
||||
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
|
||||
|
||||
- Затем:
|
||||
|
||||
python_embeded\python.exe -m pip install basicsr
|
||||
|
||||
### **VI. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
||||
### **III. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
||||
|
||||
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
|
||||
|
||||
@@ -470,8 +511,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) |  |
|
||||
| [GFPGANv1.3.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.3.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) |  |
|
||||
| [GFPGANv1.4.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/GFPGANv1.4.pth) | [TencentARC](https://github.com/TencentARC/GFPGAN) |  |
|
||||
| [inswapper_128.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) |  |
|
||||
| [inswapper_128_fp16.onnx](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) |  |
|
||||
| [inswapper_128.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx) | [DeepInsight](https://github.com/deepinsight/insightface) |  |
|
||||
| [inswapper_128_fp16.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128_fp16.onnx) | [Hillobar](https://github.com/Hillobar/Rope) |  |
|
||||
|
||||
[BasicSR](https://github.com/XPixelGroup/BasicSR) - [@XPixelGroup](https://github.com/XPixelGroup) <br>
|
||||
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
|
||||
|
||||
+3
-31
@@ -1,39 +1,11 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Добавляем путь расширения, чтобы Питон видел наши папки (r_facelib, scripts и т.д.)
|
||||
repo_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, repo_dir)
|
||||
original_modules = sys.modules.copy()
|
||||
if repo_dir not in sys.path:
|
||||
sys.path.insert(0, repo_dir)
|
||||
|
||||
# Place aside existing modules if using a1111 web ui
|
||||
modules_used = [
|
||||
"modules",
|
||||
"modules.images",
|
||||
"modules.processing",
|
||||
"modules.scripts_postprocessing",
|
||||
"modules.scripts",
|
||||
"modules.shared",
|
||||
]
|
||||
original_webui_modules = {}
|
||||
for module in modules_used:
|
||||
if module in sys.modules:
|
||||
original_webui_modules[module] = sys.modules.pop(module)
|
||||
|
||||
# Proceed with node setup
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
# Clean up imports
|
||||
# Remove repo directory from path
|
||||
sys.path.remove(repo_dir)
|
||||
# Remove any new modules
|
||||
modules_to_remove = []
|
||||
for module in sys.modules:
|
||||
if module not in original_modules and not module.startswith("google.protobuf") and not module.startswith("onnx") and not module.startswith("cv2"):
|
||||
modules_to_remove.append(module)
|
||||
for module in modules_to_remove:
|
||||
del sys.modules[module]
|
||||
|
||||
# Restore original modules
|
||||
sys.modules.update(original_webui_modules)
|
||||
|
||||
+10
-6
@@ -1,22 +1,25 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Try to use embedded python first
|
||||
if exist ..\..\..\python_embeded\python.exe (
|
||||
:: Use the embedded python
|
||||
:: Try to use the Desktop version's venv python first
|
||||
if exist ..\..\.venv\Scripts\python.exe (
|
||||
:: Use the ComfyUI Desktop venv python
|
||||
set PYTHON=..\..\.venv\Scripts\python.exe
|
||||
) else if exist ..\..\..\python_embeded\python.exe (
|
||||
:: Use the embedded python (portable version)
|
||||
set PYTHON=..\..\..\python_embeded\python.exe
|
||||
) else (
|
||||
:: Embedded python not found, check for python in the PATH
|
||||
:: Neither found, check for python in the PATH
|
||||
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
|
||||
set PYTHON_VERSION=%%F
|
||||
)
|
||||
if errorlevel 1 (
|
||||
echo I couldn't find an embedded version of Python, nor one in the Windows PATH. Please install manually.
|
||||
echo I couldn't find a venv python ^(Desktop^), an embedded python ^(Portable^), nor one in the Windows PATH. Please install manually.
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
:: Use python from the PATH (if it's the right version and the user agrees)
|
||||
echo I couldn't find an embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
||||
echo I couldn't find a venv or embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
||||
echo Would you like to proceed with the install using that version? (Y/N^)
|
||||
set /p USE_PYTHON=
|
||||
if /i "!USE_PYTHON!"=="Y" (
|
||||
@@ -30,6 +33,7 @@ if exist ..\..\..\python_embeded\python.exe (
|
||||
)
|
||||
|
||||
:: Install the package
|
||||
echo Using Python: %PYTHON%
|
||||
echo Installing...
|
||||
%PYTHON% install.py
|
||||
echo Done^!
|
||||
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
[project]
|
||||
name = "comfyui-reactor"
|
||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension Node for ComfyUI, based on ReActor SD-WebUI Face Swap Extension"
|
||||
version = "0.5.2"
|
||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
|
||||
version = "0.7.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.3", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||
dependencies = ["onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/Gourieff/ComfyUI-ReActor"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "gourieff"
|
||||
|
||||
@@ -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>
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,113 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
import os
|
||||
import comfy.model_management as model_management
|
||||
from scripts.reactor_logger import logger
|
||||
from .dlss5_core import DLSSStandaloneManager
|
||||
from r_modules.shared import state
|
||||
from reactor_utils import (
|
||||
batch_tensor_to_pil,
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
|
||||
class DLSS5FrameEnhancer:
|
||||
def __init__(self):
|
||||
self.device = model_management.get_torch_device()
|
||||
self.manager = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"style": (["Default", "Nature", "Cinematic"],),
|
||||
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
|
||||
"local_tone": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 0.0"}),
|
||||
"local_structure": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
|
||||
"skin_structure": ("FLOAT", {"default": 0.5, "min": -1.0, "max": 2.0, "step": 0.05, "tooltip": "-1..2, def: 0.5"}),
|
||||
"color_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
|
||||
"tone_preservation": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
|
||||
"face_skin_protection": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
|
||||
"grain_preservation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
|
||||
"nr_passes": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "0..4, def: 1"}),
|
||||
"auto_mask": ("BOOLEAN", {"default": False, "label_off": "OFF", "label_on": "ON", "tooltip": "AI Automatic masking of complex areas to prevent over-detailing"}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("enhanced_image",)
|
||||
FUNCTION = "enhance"
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
DESCRIPTION = (
|
||||
"Requirements:\n"
|
||||
"- NVIDIA display driver >= 616.x\n"
|
||||
"- NVIDIA RTX 40/50-series GPU\n"
|
||||
"(compatibility with older RTX series is unconfirmed)\n"
|
||||
"- DLLs: neuroframe_caller.dll, neuroframe_engine.dll, nvngx_dlssnr.dll\nin custom_nodes/ComfyUI-ReActor/r_dlssnr/dll\nor custom_nodes/comfyui-reactor-node/r_dlssnr/dll\n"
|
||||
"(see README.md in dll folder for instructions)"
|
||||
)
|
||||
|
||||
def load_bridge(self):
|
||||
if self.manager is None:
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
dll_dir = os.path.join(current_dir, "dll")
|
||||
self.manager = DLSSStandaloneManager(dll_dir)
|
||||
ordinal = getattr(self.device, 'index', 0) if self.device.index is not None else 0
|
||||
self.manager.initialize(ordinal)
|
||||
logger.status(f"DLSS-5 Bridge initialized on GPU {ordinal}")
|
||||
|
||||
def enhance(self, image, style, intensity, local_tone, local_structure,
|
||||
skin_structure, color_strength, tone_preservation,
|
||||
face_skin_protection, grain_preservation, nr_passes, auto_mask, mask=None):
|
||||
|
||||
self.load_bridge()
|
||||
|
||||
style_map = {"Default": 0, "Nature": 1, "Cinematic": 2}
|
||||
settings = {
|
||||
"style": style_map[style], "intensity": intensity, "local_tone": local_tone,
|
||||
"local_structure": local_structure, "skin_structure": skin_structure,
|
||||
"color_strength": color_strength, "tone_preservation": tone_preservation,
|
||||
"face_skin_protection": face_skin_protection, "grain_preservation": grain_preservation,
|
||||
"nr_passes": nr_passes, "auto_mask": auto_mask,
|
||||
"shimmer_suppression": 0.0, "prefer_nvof": False
|
||||
}
|
||||
|
||||
enhanced_batch = []
|
||||
|
||||
pil_images = batch_tensor_to_pil(image)
|
||||
pbar = progress_bar(len(pil_images))
|
||||
|
||||
for i in range(len(image)):
|
||||
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
break
|
||||
|
||||
img_np = np.ascontiguousarray(image[i].cpu().numpy().astype(np.float32))
|
||||
dest_np = np.ascontiguousarray(np.zeros_like(img_np))
|
||||
|
||||
mask_np = None
|
||||
if mask is not None:
|
||||
mask_np = np.ascontiguousarray(mask[i].cpu().numpy().astype(np.float32))
|
||||
|
||||
# Вызываем Host-обработчик (без CUDA конфликтов)
|
||||
self.manager.process_host(
|
||||
source=img_np,
|
||||
destination=dest_np,
|
||||
settings=settings,
|
||||
reset=True,
|
||||
mask=mask_np
|
||||
)
|
||||
|
||||
out_tensor = torch.from_numpy(dest_np).to(self.device)
|
||||
enhanced_batch.append(out_tensor)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (torch.stack(enhanced_batch),)
|
||||
@@ -6,7 +6,7 @@ import torch.nn.functional as F
|
||||
from PIL import Image
|
||||
from torchvision.models._utils import IntermediateLayerGetter as IntermediateLayerGetter
|
||||
|
||||
from modules import shared
|
||||
from r_modules import shared
|
||||
|
||||
from r_facelib.detection.align_trans import get_reference_facial_points, warp_and_crop_face
|
||||
from r_facelib.detection.retinaface.retinaface_net import FPN, SSH, MobileNetV1, make_bbox_head, make_class_head, make_landmark_head
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
class StableDiffusionProcessing:
|
||||
class Processing:
|
||||
|
||||
def __init__(self, init_imgs):
|
||||
self.init_images = init_imgs
|
||||
self.width = init_imgs[0].width
|
||||
self.height = init_imgs[0].height
|
||||
self.extra_generation_params = {}
|
||||
self.bbox = []
|
||||
self.swapped_indexes = []
|
||||
|
||||
|
||||
class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
|
||||
class ProcessingImg2Img(Processing):
|
||||
|
||||
def __init__(self, init_img):
|
||||
super().__init__(init_img)
|
||||
@@ -0,0 +1,307 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
import folder_paths
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageEnhance, ImageCms
|
||||
import comfy.model_management as model_management
|
||||
from scripts.reactor_logger import logger
|
||||
from r_modules.shared import state
|
||||
from reactor_utils import (
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
try:
|
||||
from colour.io.luts.iridas_cube import read_LUT_IridasCube
|
||||
AVAILABLE = True
|
||||
except Exception as e:
|
||||
AVAILABLE = False
|
||||
logger.error(f"{e}, ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
|
||||
|
||||
folder_paths.add_model_folder_path("luts", os.path.join(folder_paths.models_dir, "luts"))
|
||||
|
||||
# Профили для HDR (создаются один раз при импорте для экономии ресурсов)
|
||||
sRGB_profile = ImageCms.createProfile('sRGB')
|
||||
Lab_profile = ImageCms.createProfile('LAB')
|
||||
|
||||
def get_luts():
|
||||
luts = folder_paths.get_filename_list("luts")
|
||||
luts = [x for x in luts if x.lower().endswith(".cube") or x.lower().endswith(".spi1d") or x.lower().endswith(".spi3d") or x.lower().endswith(".spimtx") or x.lower().endswith(".csp")]
|
||||
luts.sort(key=str.lower)
|
||||
luts.insert(0, "none")
|
||||
return luts
|
||||
|
||||
class ReActorCinematicEnhancer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", ),
|
||||
# --- Global Blend ---
|
||||
"intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.5"}),
|
||||
# --- LUT ---
|
||||
"lut_file": (get_luts(),),
|
||||
"lut_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "0..1, def: 0.5"}),
|
||||
"lut_gamma_correction": ("BOOLEAN", { "default": True }),
|
||||
# --- Film Effect ---
|
||||
"film_grain_method": (["advanced","fast"], {"default": "advanced"}),
|
||||
"film_grain_power": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
"film_grain_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.1, "tooltip": "0.1..1, def: 0.1"}),
|
||||
"film_grain_sat": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.15"}),
|
||||
"film_grain_shadows": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
|
||||
"film_grain_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
|
||||
"film_blur_strength": ("INT", {"default": 1, "min": 0, "max": 256, "step": 1, "tooltip": "(by depth_map) 0..256, def: 1"}),
|
||||
"film_focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01, "tooltip": "(by depth_map) 0..1, def: 0.9"}),
|
||||
"film_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1, "tooltip": "(by depth_map) 0..8, def: 2.2"}),
|
||||
"film_vignette_intensity": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
# --- Color Correct ---
|
||||
"cc_temperature": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_hue": ("FLOAT", {"default": 0, "min": -90, "max": 90, "step": 5, "tooltip": "-90..90, def: 0"}),
|
||||
"cc_brightness": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_contrast": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_saturation": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_gamma": ("FLOAT", {"default": 1, "min": 0.2, "max": 2.2, "step": 0.1, "tooltip": "0.2..2.2, def: 1.0"}),
|
||||
# --- HDR Effects ---
|
||||
"hdr_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "0..5, def: 0.5"}),
|
||||
"hdr_shadow_intensity": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
|
||||
"hdr_highlight_intensity": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.75"}),
|
||||
"hdr_contrast": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
"hdr_enhance_color": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
|
||||
},
|
||||
"optional": {
|
||||
"depth_map": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
DESCRIPTION = (
|
||||
"Requirements:\n"
|
||||
"- pip install colour-science\n"
|
||||
"- put LUT-files in models/luts\n"
|
||||
"depth_map is optional"
|
||||
)
|
||||
|
||||
def execute(self, image, intensity, lut_file, lut_strength, lut_gamma_correction,
|
||||
film_grain_method, film_grain_power, film_grain_scale, film_grain_sat, film_grain_shadows, film_grain_highs,film_blur_strength, film_focal_depth, film_focus_spread, film_vignette_intensity,
|
||||
cc_temperature, cc_hue, cc_brightness, cc_contrast, cc_saturation, cc_gamma,
|
||||
hdr_intensity, hdr_shadow_intensity, hdr_highlight_intensity, hdr_contrast, hdr_enhance_color,
|
||||
depth_map=None):
|
||||
|
||||
if not AVAILABLE:
|
||||
logger.error("ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
|
||||
return (image,)
|
||||
|
||||
# 1. Загрузка LUT
|
||||
|
||||
if lut_file != "none":
|
||||
lut_file_path = folder_paths.get_full_path("luts", lut_file)
|
||||
lut = read_LUT_IridasCube(lut_file_path) if lut_file_path and Path(lut_file_path).exists() else None
|
||||
if lut:
|
||||
lut.name = lut_file
|
||||
logger.status(f"LUT {lut_file} selected")
|
||||
else:
|
||||
lut = None
|
||||
|
||||
batch_size = image.shape[0]
|
||||
result_batch = []
|
||||
|
||||
pbar = progress_bar(batch_size)
|
||||
|
||||
logger.status(f"Enhancing...")
|
||||
|
||||
for b in range(batch_size):
|
||||
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
break
|
||||
|
||||
# --- БАЗА: NumPy (0-1) ---
|
||||
orig_np = image[b].cpu().numpy().copy()
|
||||
img_np = orig_np.copy()
|
||||
|
||||
# --- 1. LUT ---
|
||||
if lut:
|
||||
lut_img = img_np.copy()
|
||||
if lut_gamma_correction:
|
||||
lut_img = np.clip(lut_img ** (1/2.2), 0, 1)
|
||||
lut_img = lut.apply(lut_img)
|
||||
if lut_gamma_correction:
|
||||
lut_img = np.clip(lut_img ** 2.2, 0, 1)
|
||||
|
||||
if lut_strength < 1.0:
|
||||
img_np = lut_strength * lut_img + (1 - lut_strength) * img_np
|
||||
else:
|
||||
img_np = lut_img
|
||||
|
||||
# Подготовка маски глубины (если есть)
|
||||
depth_np = None
|
||||
if depth_map is not None:
|
||||
d_tensor = depth_map[b if depth_map.shape[0] > 1 else 0].cpu().numpy()
|
||||
if d_tensor.shape[:2] != img_np.shape[:2]:
|
||||
d_tensor = cv2.resize(d_tensor, (img_np.shape[1], img_np.shape[0]), interpolation=cv2.INTER_LINEAR)
|
||||
# Если глубина RGB, берем один канал
|
||||
depth_np = d_tensor[:,:,0] if d_tensor.ndim == 3 else d_tensor
|
||||
|
||||
# --- 2. FILM EFFECT ---
|
||||
# Виньетка (OpenCV)
|
||||
if film_vignette_intensity > 0:
|
||||
h, w = img_np.shape[:2]
|
||||
Y, X = np.ogrid[:h, :w]
|
||||
center_y, center_x = h / 2, w / 2
|
||||
dist_from_center = np.sqrt((X - center_x)**2 + (Y - center_y)**2)
|
||||
max_dist = np.sqrt(center_x**2 + center_y**2)
|
||||
vignette = 1 - (dist_from_center / max_dist) * film_vignette_intensity
|
||||
img_np = np.clip(img_np * vignette[..., np.newaxis], 0, 1)
|
||||
|
||||
# Глубинное размытие (пошаговая реализация)
|
||||
if film_blur_strength > 0 and depth_np is not None:
|
||||
# Нормализуем разницу глубины относительно точки фокуса
|
||||
depth_diff = np.abs(depth_np - film_focal_depth) * film_focus_spread
|
||||
depth_diff = np.clip(depth_diff, 0, 1)
|
||||
|
||||
# Пошаговый блендинг (5 шагов для плавности)
|
||||
steps = 5
|
||||
blurred_fg = np.zeros_like(img_np)
|
||||
for step in range(steps + 1):
|
||||
step_blur = int((film_blur_strength * step / steps) // 2 * 2 + 1)
|
||||
if step_blur > 1:
|
||||
blur_layer = cv2.GaussianBlur(img_np, (step_blur, step_blur), 0)
|
||||
else:
|
||||
blur_layer = img_np
|
||||
|
||||
# Маска весов для текущего шага
|
||||
mask = np.clip(1.0 - np.abs(depth_diff * steps - step), 0, 1)
|
||||
blurred_fg += blur_layer * mask[..., np.newaxis]
|
||||
img_np = blurred_fg
|
||||
|
||||
# --- 3. COLOR CORRECT ---
|
||||
fg_img = Image.fromarray((img_np * 255).astype(np.uint8))
|
||||
|
||||
# Яркость и Контраст (PIL)
|
||||
if cc_brightness != 0:
|
||||
fg_img = ImageEnhance.Brightness(fg_img).enhance(1 + (cc_brightness / 100))
|
||||
if cc_contrast != 0:
|
||||
fg_img = ImageEnhance.Contrast(fg_img).enhance(1 + (cc_contrast / 100))
|
||||
|
||||
fg_cv = np.array(fg_img).astype(np.float32) / 255.0
|
||||
|
||||
# Температура
|
||||
temp_val = cc_temperature / 100
|
||||
if temp_val > 0:
|
||||
fg_cv[:, :, 0] = np.clip(fg_cv[:, :, 0] * (1 + temp_val), 0, 1)
|
||||
fg_cv[:, :, 1] = np.clip(fg_cv[:, :, 1] * (1 + temp_val * 0.4), 0, 1)
|
||||
elif temp_val < 0:
|
||||
fg_cv[:, :, 2] = np.clip(fg_cv[:, :, 2] * (1 - temp_val), 0, 1)
|
||||
|
||||
# Гамма
|
||||
if cc_gamma != 1.0:
|
||||
fg_cv = np.clip(np.power(fg_cv, cc_gamma), 0, 1)
|
||||
|
||||
# HSV для Hue и Saturation
|
||||
if cc_hue != 0 or cc_saturation != 0:
|
||||
hsv = cv2.cvtColor((fg_cv * 255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
|
||||
if cc_hue != 0:
|
||||
hsv[:, :, 0] = (hsv[:, :, 0] + (cc_hue / 2)) % 180 # В OpenCV Hue до 180
|
||||
if cc_saturation != 0:
|
||||
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * (1 + cc_saturation / 100), 0, 255)
|
||||
fg_cv = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB).astype(np.float32) / 255.0
|
||||
|
||||
img_np = fg_cv
|
||||
|
||||
# --- 4. FILM GRAIN ---
|
||||
if film_grain_power > 0:
|
||||
|
||||
# --- 4.1 ADVANCED NATIVE GRAIN ---
|
||||
if film_grain_method == "advanced":
|
||||
# 1) Генерируем базовый цветной шум
|
||||
noise_rgb = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
|
||||
|
||||
# 2) Генерируем монохромный шум (усреднение по каналам)
|
||||
noise_mono = np.mean(noise_rgb, axis=-1, keepdims=True)
|
||||
|
||||
# 3) Смешиваем цветность зерна (grain_sat)
|
||||
noise = noise_rgb * film_grain_sat + noise_mono * (1.0 - film_grain_sat)
|
||||
|
||||
# 4) Масштабирование зерна
|
||||
if film_grain_scale != 1.0:
|
||||
h, w = img_np.shape[:2]
|
||||
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
|
||||
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# 5) Кинематографичная маскировка по яркости (Luminance)
|
||||
# Вычисляем воспринимаемую яркость пикселей
|
||||
lum = 0.299 * img_np[:,:,0] + 0.587 * img_np[:,:,1] + 0.114 * img_np[:,:,2]
|
||||
lum = lum[..., np.newaxis]
|
||||
|
||||
# Создаем карту весов шума:
|
||||
# Средние тона (0.5) получают вес 1.0.
|
||||
# Тени (0.0) получают вес film_grain_shadows.
|
||||
# Блики (1.0) получают вес film_grain_highs.
|
||||
grain_mask = np.where(
|
||||
lum < 0.5,
|
||||
film_grain_shadows + (1.0 - film_grain_shadows) * (lum * 2.0), # Плавный переход от теней к средним
|
||||
1.0 - (1.0 - film_grain_highs) * ((lum - 0.5) * 2.0) # Плавный переход от средних к бликам
|
||||
)
|
||||
|
||||
# Накладываем шум с учетом яркостной маски
|
||||
img_np = np.clip(img_np + noise * grain_mask, 0, 1)
|
||||
|
||||
# --- 4.2 FAST GRAIN ---
|
||||
else:
|
||||
noise = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
|
||||
# Масштабирование шума
|
||||
if film_grain_scale != 1.0:
|
||||
h, w = img_np.shape[:2]
|
||||
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
|
||||
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||
img_np = np.clip(img_np + noise * 0.5, 0, 1)
|
||||
|
||||
# --- 5. HDR EFFECTS ---
|
||||
if hdr_intensity > 0:
|
||||
final_pil = Image.fromarray((img_np * 255).astype(np.uint8))
|
||||
|
||||
# Основная математика тонирования LAB
|
||||
img_lab = ImageCms.profileToProfile(final_pil, sRGB_profile, Lab_profile, outputMode='LAB')
|
||||
luminance, a, b = img_lab.split()
|
||||
base = np.array(luminance, dtype=np.float32)
|
||||
|
||||
scaled_shadow = (hdr_shadow_intensity ** 2) * hdr_intensity
|
||||
scaled_highlight = (hdr_highlight_intensity ** 2) * hdr_intensity
|
||||
|
||||
shadow_mask = np.clip((1 - (base / 255)) ** 2, 0, 1)
|
||||
highlight_mask = np.clip((base / 255) ** 2, 0, 1)
|
||||
|
||||
adj_shadows = np.clip(base * (1 - shadow_mask * scaled_shadow), 0, 255)
|
||||
adj_highlights = np.clip(base + (255 - base) * highlight_mask * scaled_highlight, 0, 255)
|
||||
|
||||
adj_lum = np.clip(adj_shadows + adj_highlights - base, 0, 255)
|
||||
final_lum = np.clip(base * (1 - hdr_intensity) + adj_lum * hdr_intensity, 0, 255).astype(np.uint8)
|
||||
|
||||
merged_lab = Image.merge('LAB', (Image.fromarray(final_lum), a, b))
|
||||
final_pil = ImageCms.profileToProfile(merged_lab, Lab_profile, sRGB_profile, outputMode='RGB')
|
||||
|
||||
if hdr_contrast > 0:
|
||||
final_pil = ImageEnhance.Contrast(final_pil).enhance(1 + hdr_contrast)
|
||||
if hdr_enhance_color > 0:
|
||||
final_pil = ImageEnhance.Color(final_pil).enhance(1 + hdr_enhance_color * 0.2)
|
||||
|
||||
# Возвращаем HDR в рабочий массив для финального смешивания
|
||||
img_np = np.array(final_pil).astype(np.float32) / 255.0
|
||||
|
||||
# --- 6. GLOBAL INTENSITY BLEND ---
|
||||
if intensity < 1.0:
|
||||
img_np = orig_np * (1.0 - intensity) + img_np * intensity
|
||||
|
||||
# Возврат в тензор
|
||||
out_tensor = torch.from_numpy(img_np)
|
||||
result_batch.append(out_tensor)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (torch.stack(result_batch),)
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -7,13 +7,14 @@ import cv2
|
||||
import math
|
||||
import logging
|
||||
import hashlib
|
||||
from insightface.app.common import Face
|
||||
from reactor_core.face_objects import Face
|
||||
from safetensors.torch import save_file, safe_open
|
||||
from tqdm import tqdm
|
||||
import urllib.request
|
||||
import onnxruntime
|
||||
from typing import Any
|
||||
import folder_paths
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
ORT_SESSION = None
|
||||
|
||||
@@ -24,6 +25,34 @@ def tensor_to_pil(img_tensor, batch_index=0):
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8).squeeze())
|
||||
return img
|
||||
|
||||
# def tensor_to_pil(img, batch_index=0):
|
||||
# """Безопасное преобразование тензора в PIL.Image с обработкой особых случаев"""
|
||||
# try:
|
||||
# # Обработка пакетных данных
|
||||
# if isinstance(img, torch.Tensor):
|
||||
# if len(img.shape) == 4:
|
||||
# img = img[batch_index] # Выбор элемента батча
|
||||
# img = img.detach().cpu().numpy()
|
||||
|
||||
# # Нормализация и приведение типа
|
||||
# if img.dtype == np.float32:
|
||||
# img = np.clip(255. * img, 0, 255).astype(np.uint8)
|
||||
|
||||
# # Обработка нестандартных размерностей
|
||||
# if img.shape[-1] > 4: # Если каналов больше 4
|
||||
# img = img[..., :3] # Берем первые 3 канала
|
||||
|
||||
# # Преобразование в 2D/3D массив
|
||||
# if len(img.shape) == 3 and img.shape[0] == 1: # [C, H, W] → [H, W]
|
||||
# img = img.squeeze(0)
|
||||
# elif len(img.shape) == 3 and img.shape[2] == 1: # [H, W, C=1]
|
||||
# img = img.squeeze(-1)
|
||||
|
||||
# return Image.fromarray(img)
|
||||
|
||||
# except Exception as e:
|
||||
# raise RuntimeError(f"Невозможно преобразовать тензор формы {img.shape} в PIL.Image") from e
|
||||
|
||||
|
||||
def batch_tensor_to_pil(img_tensor):
|
||||
# Convert tensor of shape [batch_size, channels, height, width] to a list of PIL Images
|
||||
@@ -208,6 +237,14 @@ def normalize_cropped_face(cropped_face):
|
||||
return cropped_face
|
||||
|
||||
|
||||
def progress_bar(total):
|
||||
return ProgressBar(total)
|
||||
|
||||
def progress_bar_reset(pbar):
|
||||
pbar.current = 0
|
||||
pbar.update(0)
|
||||
|
||||
|
||||
# author: Trung0246 --->
|
||||
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
|
||||
# Iterate over the list of full folder paths
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
albumentations>=1.4.16
|
||||
insightface==0.7.3
|
||||
onnx>=1.14.0
|
||||
opencv-python>=4.7.0.72
|
||||
numpy==1.26.3
|
||||
numpy
|
||||
segment_anything
|
||||
ultralytics
|
||||
colour-science
|
||||
|
||||
+34
-12
@@ -312,15 +312,24 @@ class SafeToGPU:
|
||||
if is_same_device(device, 'cpu'):
|
||||
obj.to(device)
|
||||
else:
|
||||
if is_same_device(obj.device, 'cpu'): # cpu to gpu
|
||||
# TRY to check submodule device
|
||||
current_device = None
|
||||
try:
|
||||
current_device = next(obj.parameters()).device
|
||||
except:
|
||||
pass
|
||||
|
||||
if current_device is None or is_same_device(current_device, 'cpu'):
|
||||
model_management.free_memory(self.size * 1.3, device)
|
||||
if model_management.get_free_memory(device) > self.size * 1.3:
|
||||
try:
|
||||
print("Moving to GPU...", end=" ")
|
||||
obj.to(device)
|
||||
print("OK")
|
||||
except:
|
||||
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [1]")
|
||||
print(f"Failed\nWARN: Model not moved to '{device}' [1]")
|
||||
else:
|
||||
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [2]")
|
||||
print(f"WARN: Model not moved to '{device}' [2]")
|
||||
|
||||
def center_of_bbox(bbox):
|
||||
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
@@ -532,9 +541,10 @@ def merge_and_stack_masks(stacked_masks, group_size):
|
||||
|
||||
def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
|
||||
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
|
||||
if sam_model.is_auto_mode:
|
||||
if getattr(sam_model, 'is_auto_mode', False):
|
||||
device = model_management.get_torch_device()
|
||||
sam_model.safe_to.to_device(sam_model, device=device)
|
||||
# sam_model.safe_to.to_device(sam_model, device=device)
|
||||
sam_model.to(device)
|
||||
|
||||
try:
|
||||
predictor = SamPredictor(sam_model)
|
||||
@@ -588,18 +598,29 @@ def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
|
||||
mask = combine_masks2(total_masks)
|
||||
|
||||
finally:
|
||||
if sam_model.is_auto_mode:
|
||||
sam_model.cpu()
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
sam_model.to(device)
|
||||
# if sam_model.is_auto_mode:
|
||||
# sam_model.cpu()
|
||||
|
||||
pass
|
||||
|
||||
mask_working_device = torch.device("cpu")
|
||||
# mask_working_device = torch.device("cpu")
|
||||
mask_working_device = model_management.get_torch_device()
|
||||
|
||||
if mask is not None:
|
||||
mask = mask.float()
|
||||
mask = dilate_mask(mask.cpu().numpy(), dilation)
|
||||
mask = torch.from_numpy(mask)
|
||||
mask = mask.to(device=mask_working_device)
|
||||
# mask = dilate_mask(mask.cpu().numpy(), dilation)
|
||||
# mask = torch.from_numpy(mask)
|
||||
# mask = mask.to(device=mask_working_device)
|
||||
mask = mask.unsqueeze(0).unsqueeze(0) # [1,1,H,W]
|
||||
if dilation > 0:
|
||||
kernel_size = 1 + dilation * 2
|
||||
mask = torch.nn.functional.max_pool2d(mask, kernel_size=kernel_size, stride=1, padding=dilation)
|
||||
elif dilation < 0:
|
||||
kernel_size = 1 + abs(dilation) * 2
|
||||
mask = -torch.nn.functional.max_pool2d(-mask, kernel_size=kernel_size, stride=1, padding=abs(dilation))
|
||||
mask = mask.squeeze(0).squeeze(0) # [H,W]
|
||||
else:
|
||||
# Extracting batch, height and width
|
||||
height, width, _ = image.shape
|
||||
@@ -641,7 +662,8 @@ def tensor2rgba(t: torch.Tensor) -> torch.Tensor:
|
||||
elif size[3] == 1:
|
||||
return t.repeat(1, 1, 1, 4)
|
||||
elif size[3] == 3:
|
||||
alpha_tensor = torch.ones((size[0], size[1], size[2], 1))
|
||||
# alpha_tensor = torch.ones((size[0], size[1], size[2], 1)).to(t.device)
|
||||
alpha_tensor = t.new_ones((size[0], size[1], size[2], 1))
|
||||
return torch.cat((t, alpha_tensor), dim=3)
|
||||
else:
|
||||
return t
|
||||
|
||||
+16
-58
@@ -2,14 +2,14 @@ import os, glob
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import modules.scripts as scripts
|
||||
import r_modules.scripts as scripts
|
||||
# from modules.upscaler import Upscaler, UpscalerData
|
||||
from modules import scripts, scripts_postprocessing
|
||||
from modules.processing import (
|
||||
StableDiffusionProcessing,
|
||||
StableDiffusionProcessingImg2Img,
|
||||
from r_modules import scripts, scripts_postprocessing
|
||||
from r_modules.processing import (
|
||||
Processing,
|
||||
ProcessingImg2Img,
|
||||
)
|
||||
from modules.shared import state
|
||||
from r_modules.shared import state
|
||||
from scripts.reactor_logger import logger
|
||||
from scripts.reactor_swapper import (
|
||||
swap_face,
|
||||
@@ -26,7 +26,8 @@ import comfy.model_management as model_management
|
||||
def get_models():
|
||||
swappers = [
|
||||
"insightface",
|
||||
"reswapper"
|
||||
"reswapper",
|
||||
"hyperswap"
|
||||
]
|
||||
models_list = []
|
||||
for folder in swappers:
|
||||
@@ -42,7 +43,7 @@ class FaceSwapScript(scripts.Script):
|
||||
|
||||
def process(
|
||||
self,
|
||||
p: StableDiffusionProcessing,
|
||||
p: Processing,
|
||||
img,
|
||||
enable,
|
||||
source_faces_index,
|
||||
@@ -101,12 +102,12 @@ class FaceSwapScript(scripts.Script):
|
||||
self.gender_target = 2
|
||||
|
||||
# if self.source is not None:
|
||||
if isinstance(p, StableDiffusionProcessingImg2Img) and swap_in_source:
|
||||
if isinstance(p, ProcessingImg2Img) and swap_in_source:
|
||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
||||
|
||||
if len(p.init_images) == 1:
|
||||
|
||||
result = swap_face(
|
||||
result, bbox, swapped_indexes = swap_face(
|
||||
self.source,
|
||||
p.init_images[0],
|
||||
source_faces_index=self.source_faces_index,
|
||||
@@ -123,27 +124,11 @@ class FaceSwapScript(scripts.Script):
|
||||
interpolation=self.interpolation,
|
||||
)
|
||||
p.init_images[0] = result
|
||||
|
||||
# for i in range(len(p.init_images)):
|
||||
# if state.interrupted or model_management.processing_interrupted():
|
||||
# logger.status("Interrupted by User")
|
||||
# break
|
||||
# if len(p.init_images) > 1:
|
||||
# logger.status(f"Swap in %s", i)
|
||||
# result = swap_face(
|
||||
# self.source,
|
||||
# p.init_images[i],
|
||||
# source_faces_index=self.source_faces_index,
|
||||
# faces_index=self.faces_index,
|
||||
# model=self.model,
|
||||
# gender_source=self.gender_source,
|
||||
# gender_target=self.gender_target,
|
||||
# face_model=self.face_model,
|
||||
# )
|
||||
# p.init_images[i] = result
|
||||
p.bbox = bbox
|
||||
p.swapped_indexes = swapped_indexes
|
||||
|
||||
elif len(p.init_images) > 1:
|
||||
result = swap_face_many(
|
||||
result, bbox, swapped_indexes = swap_face_many(
|
||||
self.source,
|
||||
p.init_images,
|
||||
source_faces_index=self.source_faces_index,
|
||||
@@ -160,34 +145,7 @@ class FaceSwapScript(scripts.Script):
|
||||
interpolation=self.interpolation,
|
||||
)
|
||||
p.init_images = result
|
||||
p.bbox = bbox
|
||||
p.swapped_indexes = swapped_indexes
|
||||
|
||||
logger.status("--Done!--")
|
||||
# else:
|
||||
# logger.error(f"Please provide a source face")
|
||||
|
||||
def postprocess_batch(self, p, *args, **kwargs):
|
||||
if self.enable:
|
||||
images = kwargs["images"]
|
||||
|
||||
def postprocess_image(self, p, script_pp: scripts.PostprocessImageArgs, *args):
|
||||
if self.enable and self.swap_in_generated:
|
||||
if self.source is not None:
|
||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
||||
image: Image.Image = script_pp.image
|
||||
result = swap_face(
|
||||
self.source,
|
||||
image,
|
||||
source_faces_index=self.source_faces_index,
|
||||
faces_index=self.faces_index,
|
||||
model=self.model,
|
||||
upscale_options=self.upscale_options,
|
||||
gender_source=self.gender_source,
|
||||
gender_target=self.gender_target,
|
||||
)
|
||||
try:
|
||||
pp = scripts_postprocessing.PostprocessedImage(result)
|
||||
pp.info = {}
|
||||
p.extra_generation_params.update(pp.info)
|
||||
script_pp.image = pp.image
|
||||
except:
|
||||
logger.error(f"Cannot create a result image")
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from modules import shared
|
||||
from r_modules import shared
|
||||
from reactor_utils import addLoggingLevel
|
||||
|
||||
|
||||
|
||||
+43
-6
@@ -1,18 +1,55 @@
|
||||
from transformers import pipeline
|
||||
from PIL import Image
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import comfy.model_management as model_management
|
||||
from reactor_utils import download
|
||||
from scripts.reactor_logger import logger
|
||||
|
||||
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)
|
||||
|
||||
def nsfw_image(img_path: str, model_path: str):
|
||||
|
||||
with Image.open(img_path) as img:
|
||||
predict = pipeline("image-classification", model=model_path)
|
||||
def nsfw_image(img_data, model_path: str):
|
||||
if not MODEL_EXISTS:
|
||||
logger.status("Ensuring NSFW detection model exists...")
|
||||
if not ensure_nsfw_model(model_path):
|
||||
return True
|
||||
device = model_management.get_torch_device()
|
||||
with Image.open(io.BytesIO(img_data)) as img:
|
||||
if "cpu" in str(device):
|
||||
predict = pipeline("image-classification", model=model_path)
|
||||
else:
|
||||
device_id = 0
|
||||
if "cuda" in str(device):
|
||||
device_id = int(str(device).split(":")[1])
|
||||
predict = pipeline("image-classification", model=model_path, device=device_id)
|
||||
result = predict(img)
|
||||
if result[0]["label"] == "nsfw" and result[0]["score"] > SCORE:
|
||||
logger.status(f"NSFW content detected, skipping...")
|
||||
logger.status(f'NSFW content detected with score={result[0]["score"]}, skipping...')
|
||||
return True
|
||||
return False
|
||||
|
||||
+184
-179
@@ -6,22 +6,22 @@ import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
import insightface
|
||||
from insightface.app.common import Face
|
||||
# try:
|
||||
# import torch.cuda as cuda
|
||||
# except:
|
||||
# cuda = None
|
||||
from reactor_core.analyzer import ReActorFaceAnalysis
|
||||
from reactor_core.face_objects import Face
|
||||
from reactor_core.inswap import INSwapper
|
||||
from reactor_core.hyperswap import HyperSwapper
|
||||
import torch
|
||||
|
||||
import folder_paths
|
||||
import comfy.model_management as model_management
|
||||
from modules.shared import state
|
||||
from r_modules.shared import state
|
||||
|
||||
from scripts.reactor_logger import logger
|
||||
from reactor_utils import (
|
||||
move_path,
|
||||
get_image_md5hash,
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
from scripts.r_faceboost import swapper, restorer
|
||||
|
||||
@@ -43,13 +43,6 @@ try:
|
||||
except Exception as e:
|
||||
logger.debug(f"ExecutionProviderError: {e}.\nEP is set to CPU.")
|
||||
providers = ["CPUExecutionProvider"]
|
||||
# if cuda is not None:
|
||||
# if cuda.is_available():
|
||||
# providers = ["CUDAExecutionProvider"]
|
||||
# else:
|
||||
# providers = ["CPUExecutionProvider"]
|
||||
# else:
|
||||
# providers = ["CPUExecutionProvider"]
|
||||
|
||||
models_path_old = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
|
||||
insightface_path_old = os.path.join(models_path_old, "insightface")
|
||||
@@ -59,6 +52,7 @@ models_path = folder_paths.models_dir
|
||||
insightface_path = os.path.join(models_path, "insightface")
|
||||
insightface_models_path = os.path.join(insightface_path, "models")
|
||||
reswapper_path = os.path.join(models_path, "reswapper")
|
||||
hyperswap_path = os.path.join(models_path, "hyperswap")
|
||||
|
||||
if os.path.exists(models_path_old):
|
||||
move_path(insightface_models_path_old, insightface_models_path)
|
||||
@@ -86,11 +80,6 @@ TARGET_IMAGE_LIST_HASH = []
|
||||
|
||||
def unload_model(model):
|
||||
if model is not None:
|
||||
# check if model has unload method
|
||||
# if "unload" in model:
|
||||
# model.unload()
|
||||
# if "model_unload" in model:
|
||||
# model.model_unload()
|
||||
del model
|
||||
return None
|
||||
|
||||
@@ -108,7 +97,7 @@ def getAnalysisModel(det_size = (640, 640)):
|
||||
global ANALYSIS_MODELS
|
||||
ANALYSIS_MODEL = ANALYSIS_MODELS[str(det_size[0])]
|
||||
if ANALYSIS_MODEL is None:
|
||||
ANALYSIS_MODEL = insightface.app.FaceAnalysis(
|
||||
ANALYSIS_MODEL = ReActorFaceAnalysis(
|
||||
name="buffalo_l", providers=providers, root=insightface_path
|
||||
)
|
||||
ANALYSIS_MODEL.prepare(ctx_id=0, det_size=det_size)
|
||||
@@ -120,11 +109,18 @@ def getFaceSwapModel(model_path: str):
|
||||
if FS_MODEL is None or CURRENT_FS_MODEL_PATH is None or CURRENT_FS_MODEL_PATH != model_path:
|
||||
CURRENT_FS_MODEL_PATH = model_path
|
||||
FS_MODEL = unload_model(FS_MODEL)
|
||||
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
|
||||
|
||||
model_filename = os.path.basename(model_path)
|
||||
if "hyperswap" in model_filename.lower(): # Если это Hyperswap
|
||||
model_path = os.path.join(folder_paths.models_dir, "hyperswap", model_filename)
|
||||
FS_MODEL = HyperSwapper(model_path, providers=providers)
|
||||
else: # Если это INSwapper / Reswapper
|
||||
if "reswapper" in model_filename.lower():
|
||||
model_path = os.path.join(folder_paths.models_dir, "reswapper", model_filename)
|
||||
FS_MODEL = INSwapper(model_path, providers=providers)
|
||||
|
||||
return FS_MODEL
|
||||
|
||||
|
||||
def sort_by_order(face, order: str):
|
||||
if order == "left-right":
|
||||
return sorted(face, key=lambda x: x.bbox[0])
|
||||
@@ -136,42 +132,47 @@ def sort_by_order(face, order: str):
|
||||
return sorted(face, key=lambda x: x.bbox[1], reverse = True)
|
||||
if order == "small-large":
|
||||
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
|
||||
# if order == "large-small":
|
||||
# return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
|
||||
# by default "large-small":
|
||||
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
|
||||
|
||||
def get_face_gender(
|
||||
face,
|
||||
face_index,
|
||||
gender_condition,
|
||||
operated: str,
|
||||
order: str,
|
||||
face,
|
||||
face_index,
|
||||
gender_condition,
|
||||
operated: str,
|
||||
order: str,
|
||||
):
|
||||
gender = [
|
||||
x.sex
|
||||
for x in face
|
||||
]
|
||||
gender.reverse()
|
||||
# If index is outside of bounds, return None, avoid exception
|
||||
if face_index >= len(gender):
|
||||
logger.status("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(gender))
|
||||
return None, 0
|
||||
face_gender = gender[face_index]
|
||||
logger.status("%s Face %s: Detected Gender -%s-", operated, face_index, face_gender)
|
||||
if (gender_condition == 1 and face_gender == "F") or (gender_condition == 2 and face_gender == "M"):
|
||||
logger.status("OK - Detected Gender matches Condition")
|
||||
try:
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
return faces_sorted[face_index], 0
|
||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
|
||||
except IndexError:
|
||||
return None, 0
|
||||
else:
|
||||
logger.status("WRONG - Detected Gender doesn't match Condition")
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
return faces_sorted[face_index], 1
|
||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 1
|
||||
# 1. Сортируем ВСЕ найденные лица (без фильтрации!)
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
|
||||
# 2. Проверяем, существует ли вообще лицо с таким визуальным индексом
|
||||
if face_index >= len(faces_sorted):
|
||||
logger.info("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(faces_sorted) - 1)
|
||||
return None, 0, None
|
||||
|
||||
# 3. Берем конкретное лицо по его позиции на фото (например, второе справа)
|
||||
face_selected = faces_sorted[face_index]
|
||||
|
||||
# Если фильтр по полу отключен (no) - сразу отдаем лицо в работу
|
||||
if gender_condition == 0:
|
||||
return face_selected, 0, face_index
|
||||
|
||||
# 4. Проверяем пол выбранного лица
|
||||
# face.gender: 0 = female, 1 = male
|
||||
# gender_condition: 1 = female, 2 = male
|
||||
expected_gender = 0 if gender_condition == 1 else 1
|
||||
actual_gender = getattr(face_selected, 'gender', -1)
|
||||
|
||||
sel_gender_str = "Male" if actual_gender == 1 else "Female" if actual_gender == 0 else "Unknown"
|
||||
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, sel_gender_str)
|
||||
|
||||
# Если пол не совпадает с тем, что заказал юзер
|
||||
if actual_gender != expected_gender:
|
||||
logger.info(f"{operated} Face {face_index}: WRONG gender ({sel_gender_str})")
|
||||
return face_selected, 1, face_index # 1 означает флаг wrong_gender = True (цикл его пропустит)
|
||||
|
||||
# Если всё идеально
|
||||
return face_selected, 0, face_index
|
||||
|
||||
def half_det_size(det_size):
|
||||
logger.status("Trying to halve 'det_size' parameter")
|
||||
@@ -179,7 +180,15 @@ def half_det_size(det_size):
|
||||
|
||||
def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
|
||||
face_analyser = getAnalysisModel(det_size)
|
||||
faces = face_analyser.get(img_data)
|
||||
|
||||
faces = []
|
||||
try:
|
||||
faces = face_analyser.get(img_data)
|
||||
except Exception as e:
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
# logger.error(f"Error during face analysis: {e}")
|
||||
logger.error("No faces found")
|
||||
|
||||
# Try halving det_size if no faces are found
|
||||
if len(faces) == 0 and det_size[0] > 320 and det_size[1] > 320:
|
||||
@@ -212,10 +221,9 @@ def get_face_single(img_data: np.ndarray, face, face_index=0, det_size=(640, 640
|
||||
|
||||
try:
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
return faces_sorted[face_index], 0
|
||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
|
||||
return faces_sorted[face_index], 0, face_index
|
||||
except IndexError:
|
||||
return None, 0
|
||||
return None, 0, None
|
||||
|
||||
|
||||
def swap_face(
|
||||
@@ -236,6 +244,8 @@ def swap_face(
|
||||
):
|
||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH
|
||||
result_image = target_img
|
||||
bbox = []
|
||||
swapped_indexes = []
|
||||
|
||||
if model is not None:
|
||||
|
||||
@@ -312,82 +322,76 @@ def swap_face(
|
||||
logger.status("Using Hashed Target Face(s) Model...")
|
||||
target_faces = TARGET_FACES
|
||||
|
||||
# No use in trying to swap faces if no faces are found, enhancement
|
||||
if len(target_faces) == 0:
|
||||
logger.status("Cannot detect any Target, skipping swapping...")
|
||||
return result_image
|
||||
return result_image, bbox, swapped_indexes
|
||||
|
||||
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||
|
||||
# 1. Заранее собираем список ТОЛЬКО ВАЛИДНЫХ исходных лиц
|
||||
valid_source_faces = []
|
||||
if source_img is not None:
|
||||
# separated management of wrong_gender between source and target, enhancement
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
for idx in source_faces_index:
|
||||
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
else:
|
||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
||||
src_wrong_gender = 0
|
||||
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
|
||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
||||
elif source_face is not None:
|
||||
if len(valid_source_faces) == 0:
|
||||
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||
else:
|
||||
result = target_img
|
||||
if "inswapper" in model:
|
||||
model_path = os.path.join(insightface_path, model)
|
||||
elif "reswapper" in model:
|
||||
model_path = os.path.join(reswapper_path, model)
|
||||
elif "hyperswap" in model:
|
||||
model_path = os.path.join(hyperswap_path, model)
|
||||
|
||||
face_swapper = getFaceSwapModel(model_path)
|
||||
|
||||
source_face_idx = 0
|
||||
|
||||
# 2. Идем по целевым лицам
|
||||
for face_num in faces_index:
|
||||
# No use in trying to swap faces if no further faces are found, enhancement
|
||||
if face_num >= len(target_faces):
|
||||
logger.status("Checked all existing target faces, skipping swapping...")
|
||||
break
|
||||
|
||||
if len(source_faces_index) > 1 and source_face_idx > 0:
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
|
||||
source_face_idx += 1
|
||||
|
||||
if source_face is not None and src_wrong_gender == 0:
|
||||
target_face, wrong_gender = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
if target_face is not None and wrong_gender == 0:
|
||||
logger.status(f"Swapping...")
|
||||
if face_boost_enabled:
|
||||
logger.status(f"Face Boost is enabled")
|
||||
bgr_fake, M = face_swapper.get(result, target_face, source_face, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
# logger.status(f"Swapping as-is")
|
||||
result = face_swapper.get(result, target_face, source_face)
|
||||
elif wrong_gender == 1:
|
||||
wrong_gender = 0
|
||||
# Keep searching for other faces if wrong gender is detected, enhancement
|
||||
#if source_face_idx == len(source_faces_index):
|
||||
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
# return result_image
|
||||
logger.status("Wrong target gender detected")
|
||||
continue
|
||||
target_face, wrong_gender, target_face_index = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
|
||||
if target_face is not None and wrong_gender == 0:
|
||||
logger.status(f"Swapping...")
|
||||
|
||||
# 3. Берем валидное лицо (если их меньше, чем целей — идем по кругу)
|
||||
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||
|
||||
if face_boost_enabled and "hyperswap" not in model:
|
||||
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
|
||||
bgr_fake, M = face_swapper.get(result, target_face, source_face_to_use, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(result, bgr_fake, M)
|
||||
else:
|
||||
logger.status(f"No target face found for {face_num}")
|
||||
elif src_wrong_gender == 1:
|
||||
src_wrong_gender = 0
|
||||
# Keep searching for other faces if wrong gender is detected, enhancement
|
||||
#if source_face_idx == len(source_faces_index):
|
||||
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
# return result_image
|
||||
logger.status("Wrong source gender detected")
|
||||
result = face_swapper.get(result, target_face, source_face_to_use)
|
||||
|
||||
bbox.append(tuple(map(float, target_face.bbox)))
|
||||
swapped_indexes.append(target_face_index)
|
||||
|
||||
# Продвигаем индекс исходного лица ТОЛЬКО после УСПЕШНОГО применения
|
||||
if len(valid_source_faces) > 1:
|
||||
source_face_idx += 1
|
||||
|
||||
elif wrong_gender == 1:
|
||||
logger.status("Wrong target gender detected")
|
||||
continue
|
||||
else:
|
||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
||||
logger.info(f"No target face found for {face_num}")
|
||||
|
||||
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
|
||||
else:
|
||||
logger.status("No source face(s) in the provided Index")
|
||||
else:
|
||||
logger.status("No source face(s) found")
|
||||
return result_image
|
||||
return result_image, bbox, swapped_indexes
|
||||
|
||||
def swap_face_many(
|
||||
source_img: Union[Image.Image, None],
|
||||
@@ -407,28 +411,23 @@ def swap_face_many(
|
||||
):
|
||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, TARGET_FACES_LIST, TARGET_IMAGE_LIST_HASH
|
||||
result_images = target_imgs
|
||||
bbox = []
|
||||
swapped_indexes = []
|
||||
|
||||
if model is not None:
|
||||
|
||||
if isinstance(source_img, str): # source_img is a base64 string
|
||||
if isinstance(source_img, str):
|
||||
import base64, io
|
||||
if 'base64,' in source_img: # check if the base64 string has a data URL scheme
|
||||
# split the base64 string to get the actual base64 encoded image data
|
||||
if 'base64,' in source_img:
|
||||
base64_data = source_img.split('base64,')[-1]
|
||||
# decode base64 string to bytes
|
||||
img_bytes = base64.b64decode(base64_data)
|
||||
else:
|
||||
# if no data URL scheme, just decode
|
||||
img_bytes = base64.b64decode(source_img)
|
||||
|
||||
source_img = Image.open(io.BytesIO(img_bytes))
|
||||
|
||||
target_imgs = [cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) for target_img in target_imgs]
|
||||
|
||||
if source_img is not None:
|
||||
|
||||
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
||||
|
||||
source_image_md5hash = get_image_md5hash(source_img)
|
||||
|
||||
if SOURCE_IMAGE_HASH is None:
|
||||
@@ -451,18 +450,22 @@ def swap_face_many(
|
||||
source_faces = SOURCE_FACES
|
||||
|
||||
elif face_model is not None:
|
||||
|
||||
source_faces_index = [0]
|
||||
logger.status("Using Loaded Source Face Model...")
|
||||
source_face_model = [face_model]
|
||||
source_faces = source_face_model
|
||||
|
||||
else:
|
||||
logger.error("Cannot detect any Source")
|
||||
|
||||
if source_faces is not None:
|
||||
|
||||
target_faces = []
|
||||
pbar = progress_bar(len(target_imgs))
|
||||
|
||||
if len(TARGET_IMAGE_LIST_HASH) > 0:
|
||||
logger.status(f"Using Hashed Target Face(s) Model...")
|
||||
else:
|
||||
logger.status(f"Analyzing Target Image...")
|
||||
|
||||
for i, target_img in enumerate(target_imgs):
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
@@ -484,93 +487,95 @@ def swap_face_many(
|
||||
logger.info("(Image %s) Target Image the Same? %s", i, target_image_same)
|
||||
|
||||
if len(TARGET_FACES_LIST) == 0:
|
||||
logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST = [target_face]
|
||||
elif len(TARGET_FACES_LIST) == i and not target_image_same:
|
||||
logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST.append(target_face)
|
||||
elif len(TARGET_FACES_LIST) != i and not target_image_same:
|
||||
logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST[i] = target_face
|
||||
elif target_image_same:
|
||||
logger.status("(Image %s) Using Hashed Target Face(s) Model...", i)
|
||||
target_face = TARGET_FACES_LIST[i]
|
||||
|
||||
|
||||
# logger.status(f"Analyzing Target Image {i}...")
|
||||
# target_face = analyze_faces(target_img)
|
||||
if target_face is not None:
|
||||
target_faces.append(target_face)
|
||||
pbar.update(1)
|
||||
|
||||
# No use in trying to swap faces if no faces are found, enhancement
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
if len(target_faces) == 0:
|
||||
logger.status("Cannot detect any Target, skipping swapping...")
|
||||
return result_images
|
||||
return result_images, bbox, swapped_indexes
|
||||
|
||||
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||
|
||||
valid_source_faces = []
|
||||
if source_img is not None:
|
||||
# separated management of wrong_gender between source and target, enhancement
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
for idx in source_faces_index:
|
||||
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
else:
|
||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
||||
src_wrong_gender = 0
|
||||
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
|
||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
||||
elif source_face is not None:
|
||||
if len(valid_source_faces) == 0:
|
||||
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||
else:
|
||||
results = target_imgs
|
||||
model_path = model_path = os.path.join(insightface_path, model)
|
||||
if "inswapper" in model:
|
||||
model_path = os.path.join(insightface_path, model)
|
||||
elif "reswapper" in model:
|
||||
model_path = os.path.join(reswapper_path, model)
|
||||
elif "hyperswap" in model:
|
||||
model_path = os.path.join(hyperswap_path, model)
|
||||
|
||||
face_swapper = getFaceSwapModel(model_path)
|
||||
|
||||
source_face_idx = 0
|
||||
pbar = progress_bar(len(target_imgs))
|
||||
logger.status(f"Swapping...")
|
||||
|
||||
for face_num in faces_index:
|
||||
# No use in trying to swap faces if no further faces are found, enhancement
|
||||
if face_num >= len(target_faces):
|
||||
logger.status("Checked all existing target faces, skipping swapping...")
|
||||
break
|
||||
|
||||
if len(source_faces_index) > 1 and source_face_idx > 0:
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
|
||||
source_face_idx += 1
|
||||
|
||||
if source_face is not None and src_wrong_gender == 0:
|
||||
# Reading results to make current face swap on a previous face result
|
||||
for i, (target_img, target_face) in enumerate(zip(results, target_faces)):
|
||||
target_face_single, wrong_gender = get_face_single(target_img, target_face, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
if target_face_single is not None and wrong_gender == 0:
|
||||
result = target_img
|
||||
logger.status(f"Swapping {i}...")
|
||||
if face_boost_enabled:
|
||||
logger.status(f"Face Boost is enabled")
|
||||
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
# logger.status(f"Swapping as-is")
|
||||
result = face_swapper.get(target_img, target_face_single, source_face)
|
||||
results[i] = result
|
||||
elif wrong_gender == 1:
|
||||
wrong_gender = 0
|
||||
logger.status("Wrong target gender detected")
|
||||
continue
|
||||
target_used_in_any_image = False
|
||||
|
||||
for i, (target_img, target_face_list) in enumerate(zip(results, target_faces)):
|
||||
target_face_single, wrong_gender, target_face_index = get_face_single(target_img, target_face_list, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
|
||||
if target_face_single is not None and wrong_gender == 0:
|
||||
target_used_in_any_image = True
|
||||
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||
|
||||
result = target_img
|
||||
if face_boost_enabled and "hyperswap" not in model:
|
||||
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face_to_use, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
logger.status(f"No target face found for {face_num}")
|
||||
elif src_wrong_gender == 1:
|
||||
src_wrong_gender = 0
|
||||
logger.status("Wrong source gender detected")
|
||||
continue
|
||||
else:
|
||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
||||
result = face_swapper.get(target_img, target_face_single, source_face_to_use)
|
||||
|
||||
results[i] = result
|
||||
bbox.append(tuple(map(float, target_face_single.bbox)))
|
||||
swapped_indexes.append(target_face_index)
|
||||
pbar.update(1)
|
||||
|
||||
elif wrong_gender == 1:
|
||||
logger.status("Wrong target gender detected")
|
||||
pbar.update(1)
|
||||
continue
|
||||
else:
|
||||
logger.info(f"{i}: No target face found for {face_num}")
|
||||
pbar.update(1)
|
||||
|
||||
if target_used_in_any_image and len(valid_source_faces) > 1:
|
||||
source_face_idx += 1
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
result_images = [Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) for result in results]
|
||||
|
||||
else:
|
||||
logger.status("No source face(s) in the provided Index")
|
||||
else:
|
||||
logger.status("No source face(s) found")
|
||||
return result_images
|
||||
return result_images, bbox, swapped_indexes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
app_title = "ReActor Node for ComfyUI"
|
||||
version_flag = "v0.5.2"
|
||||
version_flag = "v0.7.2-b1"
|
||||
|
||||
COLORS = {
|
||||
"CYAN": "\033[0;36m", # CYAN
|
||||
|
||||
Reference in New Issue
Block a user