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

|

|
||||||
|
|
||||||
<!--<sup>
|
|
||||||
<font color=brightred>
|
|
||||||
|
|
||||||
## !!! [Важные изменения](#latestupdate) !!!<br>Не забудьте добавить Нод заново в существующие воркфлоу
|
|
||||||
|
|
||||||
</font>
|
|
||||||
</sup>-->
|
|
||||||
|
|
||||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||||
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
||||||
@@ -20,6 +12,9 @@
|
|||||||
</sup>
|
</sup>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||||
|
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
[](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
|
[](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
|
||||||
@@ -34,7 +29,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://github.com/Gourieff/comfyui-reactor-node) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://web.archive.org/web/20241126185020/https://github.com/Gourieff/comfyui-reactor-node/blob/main/README_RU.md) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
||||||
|
|
||||||
> Используя данное ПО, вы понимаете и принимаете [ответственность](#disclaimer)
|
> Используя данное ПО, вы понимаете и принимаете [ответственность](#disclaimer)
|
||||||
|
|
||||||
@@ -51,27 +46,88 @@
|
|||||||
|
|
||||||
## Что нового в последнем обновлении
|
## Что нового в последнем обновлении
|
||||||
|
|
||||||
### 0.5.2 <sub><sup>BETA1</sup></sub>
|
### 0.7.1 <sub><sup>BETA1</sup></sub>
|
||||||
|
|
||||||
|
- **Новый узел `DLSS5 Frame Enhancer`**. Новейшая технология DLSS 5 от NVIDIA для улучшения качества изображения (кадра целиком или лиц по маске).<br><u>Следуйте [инструкции по установке](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>Интеграция работает в изолированном Host-режиме, что полностью исключает конфликты с CUDA-контекстом ComfyUI.<br>Огромная благодарность автору проекта [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) за C++ обертку (neuroframe_engine.dll и neuroframe_caller.dll), которая легла в основу вычислительного моста для данного узла.<br>Также благодарность Gemini 3.1 Pro (через [Google Gemini](https://gemini.google.com/app)) за ассистирование и помощь.</pre><u>Требования:</u><br>
|
||||||
|
-- Windows 10/11<br>
|
||||||
|
-- NVIDIA display driver >= 616.x<br>
|
||||||
|
-- NVIDIA RTX 40/50-series GPU<br>
|
||||||
|
(совместимость с более старыми сериями RTX не подтверждена)
|
||||||
|
|
||||||
|
### 0.7.0
|
||||||
|
|
||||||
|
- 💥 **Важное обновление! ☢ Новое ядро РеАктора!**<br>✅ Библиотека `Insightface` больше не требуется!<br>✅ `C++ Build Tools` больше не требуются!<br>✅ Более простой процесс установки!<br>✅ Поддержка `Numpy 2.x` и `1.x`!<br>⚠ <u>Результат Свапа теперь слегка отличается</u>. Сложно сказать, хуже или лучше — результат просто чуть-чуть другой. По показателям схожести (оценка косинусного сходства векторов эмбеддингов лиц) результат стал немного лучше (хоть и незначительно), чем с Insightface.<br>Подробнее здесь: https://t.me/reactor_faceswap/55
|
||||||
|
- Новый узел "Face Similarity" для проверки схожести лиц после свапа
|
||||||
|
- Исправление "HyperSwap CPU Float normalization" (спасибо @Buumcode, Issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
|
||||||
|
- Исправлено: проблема определения пола и логика индексов лиц (Issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
|
||||||
|
- Улучшен `install.bat` для поддержки запуска установки в Comfy Desktop
|
||||||
|
- Прочие улучшение и исправления
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><a>Предыдущие версии</a></summary>
|
||||||
|
|
||||||
|
### 0.6.2
|
||||||
|
|
||||||
|
- Добавлена поддержка моделей HyperSwap от команды FaceFusion Labs (спасибо [@Buumcode](https://github.com/Buumcode) за вариант реализации)<br>Модели можно скачать [отсюда](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>и положить в папку `ComfyUI/models/hyperswap`
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
[Сравнение](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) моделей Inswapper, Reswapper, HyperSwap
|
||||||
|
|
||||||
|
- Теперь восстановление лиц затрагивает только заменённые лица
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01-ru.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
- Новый узел "Restore Face Advanced" с фильтром по лицам, спасибо https://github.com/Buumcode за реализацию "Фильтра восстановления лиц"<br>Этот узел помогает применить восстановление именно к нужному лицу или лицам
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
- Добавлен выход FACE_MODEL_NAME для узла "Load Face Model"
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
- Исправления и улучшения
|
||||||
|
|
||||||
|
### 0.6.1
|
||||||
|
|
||||||
|
- Улучшенная логика работы с индексами множества лиц при определении пола
|
||||||
|
- MaskHelper нод теперь почти вдвое быстрее - пока не идеально, но лучше, чем было ранее
|
||||||
|
- Нативный ProgressBar ComfyUI для разных шагов
|
||||||
|
- Добавлен выход ORIGINAL_IMAGE для основных нодов
|
||||||
|
- Разные исправления и улучшения (https://github.com/Gourieff/ComfyUI-ReActor/issues/25 фикс; временные файлы для NSFW детектора больше не создаются; NSFW детектор стал работать немного быстрее)
|
||||||
|
|
||||||
|
### 0.6.0
|
||||||
|
|
||||||
|
- Новый нод `ReActorSetWeight` - теперь можно установить силу замены лица для `source_image` или `face_model` от 0% до 100% (с шагом 12.5%)
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-02.jpg?raw=true" alt="0.6.0-whatsnew-02" width="100%"/>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
### 0.5.2
|
||||||
|
|
||||||
- Поддержка моделей ReSwapper. Несмотря на то, что Inswapper по-прежнему даёт лучшее сходство, но ReSwapper развивается - спасибо @somanchiu https://github.com/somanchiu/ReSwapper за эти модели и проект ReSwapper! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
|
- Поддержка моделей ReSwapper. Несмотря на то, что Inswapper по-прежнему даёт лучшее сходство, но ReSwapper развивается - спасибо @somanchiu https://github.com/somanchiu/ReSwapper за эти модели и проект ReSwapper! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
|
||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="100%"/>
|
<center>
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="100%"/>
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-03.jpg?raw=true" alt="0.5.2-whatsnew-03" width="75%"/>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-04.jpg?raw=true" alt="0.5.2-whatsnew-04" width="75%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
Скачать модели ReSwapper можно отсюда:
|
Скачать модели ReSwapper можно отсюда:
|
||||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||||
Сохраните их в директорию "models/reswapper".
|
Сохраните их в директорию "models/reswapper".
|
||||||
|
|
||||||
### 0.5.2 <sub><sup>ALPHA3</sup></sub>
|
|
||||||
|
|
||||||
- NSFW-детектор, чтобы не нарушать [правила GitHub](https://docs.github.com/en/site-policy/acceptable-use-policies/github-misinformation-and-disinformation#synthetic--manipulated-media-tools)
|
- NSFW-детектор, чтобы не нарушать [правила GitHub](https://docs.github.com/en/site-policy/acceptable-use-policies/github-misinformation-and-disinformation#synthetic--manipulated-media-tools)
|
||||||
|
|
||||||
### 0.5.2 <sub><sup>ALPHA2</sup></sub>
|
|
||||||
|
|
||||||
- Небольшие исправления
|
|
||||||
|
|
||||||
### 0.5.2 <sub><sup>ALPHA1</sup></sub>
|
|
||||||
|
|
||||||
- Новый нод "Unload ReActor Models" - полезен для сложных воркфлоу, когда вам нужно освободить ОЗУ, занятую РеАктором
|
- Новый нод "Unload ReActor Models" - полезен для сложных воркфлоу, когда вам нужно освободить ОЗУ, занятую РеАктором
|
||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-01.jpg?raw=true" alt="0.5.2-whatsnew-01" width="100%"/>
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-01.jpg?raw=true" alt="0.5.2-whatsnew-01" width="100%"/>
|
||||||
@@ -79,10 +135,11 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
|||||||
- Поддержка ORT CoreML and ROCM EPs, достаточно установить ту версию onnxruntime, которая соответствует вашему GPU
|
- Поддержка ORT CoreML and ROCM EPs, достаточно установить ту версию onnxruntime, которая соответствует вашему GPU
|
||||||
- Некоторые улучшения скрипта установки для поддержки последней версии ORT-GPU
|
- Некоторые улучшения скрипта установки для поддержки последней версии ORT-GPU
|
||||||
|
|
||||||
|
<center>
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-02.jpg?raw=true" alt="0.5.2-whatsnew-02" width="50%"/>
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.2-whatsnew-02.jpg?raw=true" alt="0.5.2-whatsnew-02" width="50%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
<details>
|
- Исправления и улучшения
|
||||||
<summary><a>Предыдущие версии</a></summary>
|
|
||||||
|
|
||||||
### 0.5.1
|
### 0.5.1
|
||||||
|
|
||||||
@@ -96,7 +153,7 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
|||||||
- Сортировка моделей лиц по алфавиту
|
- Сортировка моделей лиц по алфавиту
|
||||||
- Множество исправлений и улучшений
|
- Множество исправлений и улучшений
|
||||||
|
|
||||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||||
|
|
||||||
- Поддержка библиотеки Spandrel при работе с GFPGAN
|
- Поддержка библиотеки Spandrel при работе с GFPGAN
|
||||||
|
|
||||||
@@ -144,7 +201,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
|||||||
|
|
||||||
- Небольшое улучшение скорости анализа целевых изображений (input)
|
- Небольшое улучшение скорости анализа целевых изображений (input)
|
||||||
|
|
||||||
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||||
|
|
||||||
- Добавлена поддержка GPEN-BFR-512 и RestoreFormer_Plus_Plus моделей восстановления лиц
|
- Добавлена поддержка GPEN-BFR-512 и RestoreFormer_Plus_Plus моделей восстановления лиц
|
||||||
|
|
||||||
@@ -167,12 +224,12 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
|||||||
|
|
||||||
Базовый воркфлоу [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
|
Базовый воркфлоу [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
|
||||||
|
|
||||||
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||||
|
|
||||||
- Поддержка CUDA 12 - не забудьте запустить (Windows) `install.bat` или (Linux/MacOS) `install.py` для используемого Python окружения или попробуйте установить ORT-GPU для CU12 вручную (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
|
- Поддержка CUDA 12 - не забудьте запустить (Windows) `install.bat` или (Linux/MacOS) `install.py` для используемого Python окружения или попробуйте установить ORT-GPU для CU12 вручную (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
|
||||||
- Исправление Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173
|
- Исправление Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173)
|
||||||
|
|
||||||
- Отдельный Нод для восстаноления лиц (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), располагается внутри меню ReActor (нод RestoreFace)
|
- Отдельный Нод для восстаноления лиц (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), располагается внутри меню ReActor (нод RestoreFace)
|
||||||
- (Windows) Установка зависимостей теперь может быть выполнена в Python из PATH ОС
|
- (Windows) Установка зависимостей теперь может быть выполнена в Python из PATH ОС
|
||||||
- Разные исправления и улучшения
|
- Разные исправления и улучшения
|
||||||
|
|
||||||
@@ -180,7 +237,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
|||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
|
||||||
|
|
||||||
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||||
|
|
||||||
- Вход "input_image" теперь идёт первым, это даёт возможность корректного байпаса, а также это правильно с точки зрения расположения входов (главный вход - первый);
|
- Вход "input_image" теперь идёт первым, это даёт возможность корректного байпаса, а также это правильно с точки зрения расположения входов (главный вход - первый);
|
||||||
- Теперь можно сохранять модели лиц в качестве файлов "safetensors" (`ComfyUI\models\reactor\faces`) и загружать их в ReActor, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
|
- Теперь можно сохранять модели лиц в качестве файлов "safetensors" (`ComfyUI\models\reactor\faces`) и загружать их в ReActor, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
|
||||||
@@ -203,48 +260,24 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
|||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
<details>
|
### Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows
|
||||||
<summary>SD WebUI: <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/">AUTOMATIC1111</a> или <a href="https://github.com/vladmandic/automatic">SD.Next</a></summary>
|
|
||||||
|
|
||||||
1. Закройте (остановите) SD-WebUI Сервер, если запущен
|
1. Выберите из двух вариантов:
|
||||||
2. (Для пользователей Windows):
|
|
||||||
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
|
|
||||||
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
|
|
||||||
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
|
|
||||||
3. Перейдите в `extensions\sd-webui-comfyui\ComfyUI\custom_nodes`
|
|
||||||
4. Откройте Консоль или Терминал и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
|
||||||
5. Перейдите в корневую директорию SD WebUI, откройте Консоль или Терминал и выполните (для пользователей Windows)`.\venv\Scripts\activate` или (для пользователей Linux/MacOS)`venv/bin/activate`
|
|
||||||
6. `python -m pip install -U pip`
|
|
||||||
7. `cd extensions\sd-webui-comfyui\ComfyUI\custom_nodes\ComfyUI-ReActor`
|
|
||||||
8. `python install.py`
|
|
||||||
9. Пожалуйста, дождитесь полного завершения установки
|
|
||||||
10. (Начиная с версии 0.3.0) Скачайте дополнительные модели восстановления лиц (по ссылке ниже) и сохраните их в папку `extensions\sd-webui-comfyui\ComfyUI\models\facerestore_models`:<br>
|
|
||||||
https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models
|
|
||||||
11. Запустите SD WebUI и проверьте консоль на сообщение, что ReActor Node работает:
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/console_status_running.jpg?raw=true" alt="console_status_running" width="759"/>
|
|
||||||
|
|
||||||
12. Перейдите во вкладку ComfyUI и найдите там ReActor Node внутри меню `ReActor` или через поиск:
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/webui-demo.png?raw=true" alt="webui-demo" width="100%"/>
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/search-demo.png?raw=true" alt="webui-demo" width="1043"/>
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows</summary>
|
|
||||||
|
|
||||||
1. Сделайте следующее:
|
|
||||||
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
|
|
||||||
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
|
|
||||||
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
|
|
||||||
2. Выберите из двух вариантов:
|
|
||||||
- (ComfyUI Manager) Откройте ComfyUI Manager, нажвите "Install Custom Nodes", введите "ReActor" в поле "Search" и далее нажмите "Install". После того, как ComfyUI завершит установку, перезагрузите сервер.
|
- (ComfyUI Manager) Откройте ComfyUI Manager, нажвите "Install Custom Nodes", введите "ReActor" в поле "Search" и далее нажмите "Install". После того, как ComfyUI завершит установку, перезагрузите сервер.
|
||||||
- (Вручную) Перейдите в `ComfyUI\custom_nodes`, откройте Консоль и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
- (Вручную) Перейдите в `ComfyUI\custom_nodes`, откройте Консоль и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||||
3. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
2. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
||||||
4. Если модель "face_yolov8m.pt" у вас отсутствует - можете скачать её [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положить в папку "ComfyUI\models\ultralytics\bbox"<br>
|
3. Скачайте необходимые модели из Раздела "Модели" ниже
|
||||||
То же самое и с "Sams" моделями, скачайте одну или обе [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - и положите в папку "ComfyUI\models\sams"
|
4. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
||||||
5. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
|
||||||
|
|
||||||
</details>
|
## Модели
|
||||||
|
|
||||||
|
- buffalo_l: скачиваются при первом запуске в `ComfyUI\models\insightface\models\buffalo_l`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||||
|
- inswapper_128: скачивается при установке в `ComfyUI\models\insightface`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||||
|
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models скачать в `ComfyUI\models\reswapper`
|
||||||
|
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) скачать в `ComfyUI\models\hyperswap`
|
||||||
|
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models скачать любые предпочитаемые в `ComfyUI\models\facerestore_models`
|
||||||
|
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt скачать в `ComfyUI\models\ultralytics\bbox`
|
||||||
|
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams скачать в `ComfyUI\models\sams`
|
||||||
|
|
||||||
<a name="usage">
|
<a name="usage">
|
||||||
|
|
||||||
@@ -253,25 +286,30 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
|
|||||||
Вы можете найти ноды ReActor внутри меню `ReActor` или через поиск (достаточно ввести "ReActor" в поисковой строке)
|
Вы можете найти ноды ReActor внутри меню `ReActor` или через поиск (достаточно ввести "ReActor" в поисковой строке)
|
||||||
|
|
||||||
Список нодов:
|
Список нодов:
|
||||||
- ••• Main Nodes •••
|
- ••• Основные •••
|
||||||
- ReActorFaceSwap (Основной нод)
|
- ReActorFaceSwap (Основной нод)
|
||||||
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
|
- ReActorFaceSwapOpt (Основной нод с доп. входом Options)
|
||||||
- ReActorOptions (Опции для ReActorFaceSwapOpt)
|
- ReActorOptions (Опции для ReActorFaceSwapOpt)
|
||||||
- ReActorFaceBoost (Нод Face Booster)
|
- ReActorFaceBoost (Face Booster)
|
||||||
- ReActorMaskHelper (Masking Helper)
|
- ReActorMaskHelper (Masking Helper)
|
||||||
- ••• Operations with Face Models •••
|
- ReActorSetWeight (Задать замены лица)
|
||||||
- ReActorSaveFaceModel (Save Face Model)
|
- ••• Работа с моделями лиц •••
|
||||||
- ReActorLoadFaceModel (Load Face Model)
|
- ReActorSaveFaceModel (Сохранить модель лица)
|
||||||
- ReActorBuildFaceModel (Build Blended Face Model)
|
- ReActorLoadFaceModel (Загрузить модель лица)
|
||||||
- ReActorMakeFaceModelBatch (Make Face Model Batch)
|
- ReActorBuildFaceModel (Построить смешанную модель лица)
|
||||||
- ••• Additional Nodes •••
|
- ReActorMakeFaceModelBatch (Создать пачку моделей лиц)
|
||||||
- ReActorRestoreFace (Face Restoration)
|
- ••• Дополнительные •••
|
||||||
- ReActorImageDublicator (Dublicate one Image to Images List)
|
- ReActorRestoreFace (Восстановление лиц)
|
||||||
- ImageRGBA2RGB (Convert RGBA to RGB)
|
- ReActorRestoreFaceAdvanced (Восстановление лиц продвинутое)
|
||||||
|
- ReActorFaceSimilarity (Оценка схожести лиц)
|
||||||
|
- ReActorImageDublicator (Сделать из одного изображения несколько дубликатов)
|
||||||
|
- ImageRGBA2RGB (Конвертировать RGBA в RGB)
|
||||||
|
- ReActorUnload (Выгрузить модели РеАктора из VRAM)
|
||||||
|
- DLSS5FrameEnhancer (Улучшение детализации кадра с NVIDIA DLSS 5)
|
||||||
|
|
||||||
Соедините все необходимые слоты (slots) и запустите очередь (query).
|
Соедините все необходимые слоты (slots) и запустите очередь (query).
|
||||||
|
|
||||||
### Входы основного Нода
|
### Входы основного Узла
|
||||||
|
|
||||||
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
|
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
|
||||||
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
|
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
|
||||||
@@ -279,13 +317,18 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_mo
|
|||||||
- Поддерживаемые ноды: "Load Image" или любые другие ноды с выходом Image(s);
|
- Поддерживаемые ноды: "Load Image" или любые другие ноды с выходом Image(s);
|
||||||
- `face_model` - это вход для выхода с нода "Load Face Model" или другого нода ReActor для загрузки модели лица (face model или face embedding), которое вы создали ранее через нод "Save Face Model";
|
- `face_model` - это вход для выхода с нода "Load Face Model" или другого нода ReActor для загрузки модели лица (face model или face embedding), которое вы создали ранее через нод "Save Face Model";
|
||||||
- Поддерживаемые ноды: "Load Face Model", "Build Blended Face Model";
|
- Поддерживаемые ноды: "Load Face Model", "Build Blended Face Model";
|
||||||
|
- `options` - для соединения с ReActorOptions;
|
||||||
|
- Поддерживаемые ноды: "ReActorOptions";
|
||||||
|
- `face_boost` - для соединения с ReActorFaceBoost;
|
||||||
|
- Поддерживаемые ноды: "ReActorFaceBoost";
|
||||||
|
|
||||||
### Выходы основного Нода
|
### Выходы основного Узла
|
||||||
|
|
||||||
- `IMAGE` - выход с готовым изображением (результатом);
|
- `IMAGE` - выход с готовым изображением (результатом);
|
||||||
- Поддерживаемые ноды: любые ноды с изображением на входе;
|
- Поддерживаемые ноды: любые ноды с изображением на входе;
|
||||||
- `FACE_MODEL` - выход, предоставляющий модель лица, построенную в ходе замены;
|
- `FACE_MODEL` - выход, предоставляющий модель лица, построенную в ходе замены;
|
||||||
- Поддерживаемые ноды: "Save Face Model", "ReActor", "Make Face Model Batch";
|
- Поддерживаемые ноды: "Save Face Model", "ReActor", "Make Face Model Batch";
|
||||||
|
- `ORIGINAL_IMAGE` - `input_image` байпас;
|
||||||
|
|
||||||
### Восстановление лиц
|
### Восстановление лиц
|
||||||
|
|
||||||
@@ -313,61 +356,47 @@ ReActor заменит только то лицо, которое удовлет
|
|||||||
Чтобы новые модели появились в списке моделей нода "Load Face Model" - обновите страницу of с ComfyUI.<br>
|
Чтобы новые модели появились в списке моделей нода "Load Face Model" - обновите страницу of с ComfyUI.<br>
|
||||||
(Рекомендую использовать ComfyUI Manager - иначе ваше воркфлоу может быть потеряно после перезагрузки страницы, если вы не сохранили его).
|
(Рекомендую использовать ComfyUI Manager - иначе ваше воркфлоу может быть потеряно после перезагрузки страницы, если вы не сохранили его).
|
||||||
|
|
||||||
|
### Masking Helper
|
||||||
|
|
||||||
|
Нод доступен с версии 0.5.0, просто добавьте "ReActorMaskHelper" в рабочий процесс и соедините как показано ниже:
|
||||||
|
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-01.jpg?raw=true" alt="0.5.0-whatsnew-01" width="100%"/>
|
||||||
|
|
||||||
|
Если модель "face_yolov8m.pt" отсутствует - скачайте [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положите в папку "ComfyUI\models\ultralytics\bbox"
|
||||||
|
<br>
|
||||||
|
Также и ["sam_vit_b_01ec64.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_b_01ec64.pth) или ["sam_vit_l_0b3195.pth"](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/sams/sam_vit_l_0b3195.pth) (лучше окклюзия) - скачайте (если не качали ранее) и положите в папку "ComfyUI\models\sams";
|
||||||
|
|
||||||
|
Используйте этот нод для улучшенного результата при замене лиц:
|
||||||
|
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.5.0-whatsnew-02.jpg?raw=true" alt="0.5.0-whatsnew-02" width="100%"/>
|
||||||
|
|
||||||
|
### Сила замены лица
|
||||||
|
|
||||||
|
Для входов `source_image` or `face_model` можно задать силу замены лица от 0% до 100% (с шагом 12.5%) с помощью нода `ReActorSetWeight`
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-whatsnew-01.jpg?raw=true" alt="0.6.0-whatsnew-01" width="100%"/>
|
||||||
|
</center>
|
||||||
|
|
||||||
<a name="troubleshooting">
|
<a name="troubleshooting">
|
||||||
|
|
||||||
## Устранение проблем
|
## Устранение проблем
|
||||||
|
|
||||||
<a name="insightfacebuild">
|
<a name="insightfacebuild">
|
||||||
|
|
||||||
### **I. (Для пользователей Windows) Если вы до сих пор не можете установить пакет Insightface по каким-то причинам или же просто не желаете устанавливать Visual Studio или VS C++ Build Tools - сделайте следующее:**
|
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||||
|
|
||||||
1. (ComfyUI Portable) Находясь в корневой директории, проверьте версию Python:<br>запустите CMD и выполните `python_embeded\python.exe -V`<br>Вы должны увидеть версию или 3.10, или 3.11, или 3.12
|
|
||||||
2. Скачайте готовый пакет Insightface [для версии 3.10](https://github.com/Gourieff/sd-webui-reactor/raw/main/example/insightface-0.7.3-cp310-cp310-win_amd64.whl) или [для 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (если на предыдущем шаге вы увидели 3.11) или [для 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (если на предыдущем шаге вы увидели 3.12) и сохраните его в корневую директорию stable-diffusion-webui (A1111 или SD.Next) - туда, где лежит файл "webui-user.bat" -ИЛИ- в корневую директорию ComfyUI, если вы используете ComfyUI Portable
|
|
||||||
3. Из корневой директории запустите:
|
|
||||||
- (SD WebUI) CMD и `.\venv\Scripts\activate`
|
|
||||||
- (ComfyUI Portable) CMD
|
|
||||||
4. Обновите PIP:
|
|
||||||
- (SD WebUI) `python -m pip install -U pip`
|
|
||||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
|
|
||||||
5. Затем установите Insightface:
|
|
||||||
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
|
||||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
|
||||||
6. Готово!
|
|
||||||
|
|
||||||
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
|
||||||
|
|
||||||
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
|
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
|
||||||
|
|
||||||
Скачайте вручную по ссылке [отсюда](https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx)
|
Скачайте вручную по ссылке [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
|
||||||
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
|
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
|
||||||
|
|
||||||
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||||
|
|
||||||
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
|
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
|
||||||
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
|
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
|
||||||
|
|
||||||
### **IV. ControlNet Aux Node IMPORT failed - при использовании совместно с нодом ReActor**
|
### **III. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
||||||
|
|
||||||
1. Закройте или остановите ComfyUI сервер, если он запущен
|
|
||||||
2. Перейдите в корневую папку ComfyUI, откройте консоль CMD и выполните следующее:
|
|
||||||
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
|
|
||||||
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
|
|
||||||
3. Готово!
|
|
||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
|
|
||||||
|
|
||||||
### **V. "ModuleNotFoundError: No module named 'basicsr'" или "subprocess-exited-with-error" при установке пакета future-0.18.3**
|
|
||||||
|
|
||||||
- Скачайте https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
|
|
||||||
- Скопируйте файл в корневую папку ComfyUI и выполните в консоли:
|
|
||||||
|
|
||||||
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
|
|
||||||
|
|
||||||
- Затем:
|
|
||||||
|
|
||||||
python_embeded\python.exe -m pip install basicsr
|
|
||||||
|
|
||||||
### **VI. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
|
||||||
|
|
||||||
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
|
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
|
||||||
|
|
||||||
@@ -470,8 +499,8 @@ SHA256:4c06341c33c2ca1f86781dab0e829f88ad5b64be9fba56e56bc9ebdefc619e43
|
|||||||
| [codeformer-v0.1.0.pth](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/facerestore_models/codeformer-v0.1.0.pth) | [sczhou](https://github.com/sczhou/CodeFormer) |  |
|
| [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.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) |  |
|
| [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.onnx](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/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_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>
|
[BasicSR](https://github.com/XPixelGroup/BasicSR) - [@XPixelGroup](https://github.com/XPixelGroup) <br>
|
||||||
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
|
[facexlib](https://github.com/xinntao/facexlib) - [@xinntao](https://github.com/xinntao) <br>
|
||||||
|
|||||||
+3
-31
@@ -1,39 +1,11 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь расширения, чтобы Питон видел наши папки (r_facelib, scripts и т.д.)
|
||||||
repo_dir = os.path.dirname(os.path.realpath(__file__))
|
repo_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
sys.path.insert(0, repo_dir)
|
if repo_dir not in sys.path:
|
||||||
original_modules = sys.modules.copy()
|
sys.path.insert(0, repo_dir)
|
||||||
|
|
||||||
# Place aside existing modules if using a1111 web ui
|
|
||||||
modules_used = [
|
|
||||||
"modules",
|
|
||||||
"modules.images",
|
|
||||||
"modules.processing",
|
|
||||||
"modules.scripts_postprocessing",
|
|
||||||
"modules.scripts",
|
|
||||||
"modules.shared",
|
|
||||||
]
|
|
||||||
original_webui_modules = {}
|
|
||||||
for module in modules_used:
|
|
||||||
if module in sys.modules:
|
|
||||||
original_webui_modules[module] = sys.modules.pop(module)
|
|
||||||
|
|
||||||
# Proceed with node setup
|
|
||||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||||
|
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||||
|
|
||||||
# Clean up imports
|
|
||||||
# Remove repo directory from path
|
|
||||||
sys.path.remove(repo_dir)
|
|
||||||
# Remove any new modules
|
|
||||||
modules_to_remove = []
|
|
||||||
for module in sys.modules:
|
|
||||||
if module not in original_modules and not module.startswith("google.protobuf") and not module.startswith("onnx") and not module.startswith("cv2"):
|
|
||||||
modules_to_remove.append(module)
|
|
||||||
for module in modules_to_remove:
|
|
||||||
del sys.modules[module]
|
|
||||||
|
|
||||||
# Restore original modules
|
|
||||||
sys.modules.update(original_webui_modules)
|
|
||||||
|
|||||||
+10
-6
@@ -1,22 +1,25 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
:: Try to use embedded python first
|
:: Try to use the Desktop version's venv python first
|
||||||
if exist ..\..\..\python_embeded\python.exe (
|
if exist ..\..\.venv\Scripts\python.exe (
|
||||||
:: Use the embedded python
|
:: Use the ComfyUI Desktop venv python
|
||||||
|
set PYTHON=..\..\.venv\Scripts\python.exe
|
||||||
|
) else if exist ..\..\..\python_embeded\python.exe (
|
||||||
|
:: Use the embedded python (portable version)
|
||||||
set PYTHON=..\..\..\python_embeded\python.exe
|
set PYTHON=..\..\..\python_embeded\python.exe
|
||||||
) else (
|
) else (
|
||||||
:: Embedded python not found, check for python in the PATH
|
:: Neither found, check for python in the PATH
|
||||||
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
|
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
|
||||||
set PYTHON_VERSION=%%F
|
set PYTHON_VERSION=%%F
|
||||||
)
|
)
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo I couldn't find an embedded version of Python, nor one in the Windows PATH. Please install manually.
|
echo I couldn't find a venv python ^(Desktop^), an embedded python ^(Portable^), nor one in the Windows PATH. Please install manually.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
) else (
|
) else (
|
||||||
:: Use python from the PATH (if it's the right version and the user agrees)
|
:: Use python from the PATH (if it's the right version and the user agrees)
|
||||||
echo I couldn't find an embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
echo I couldn't find a venv or embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
||||||
echo Would you like to proceed with the install using that version? (Y/N^)
|
echo Would you like to proceed with the install using that version? (Y/N^)
|
||||||
set /p USE_PYTHON=
|
set /p USE_PYTHON=
|
||||||
if /i "!USE_PYTHON!"=="Y" (
|
if /i "!USE_PYTHON!"=="Y" (
|
||||||
@@ -30,6 +33,7 @@ if exist ..\..\..\python_embeded\python.exe (
|
|||||||
)
|
)
|
||||||
|
|
||||||
:: Install the package
|
:: Install the package
|
||||||
|
echo Using Python: %PYTHON%
|
||||||
echo Installing...
|
echo Installing...
|
||||||
%PYTHON% install.py
|
%PYTHON% install.py
|
||||||
echo Done^!
|
echo Done^!
|
||||||
|
|||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-reactor"
|
name = "comfyui-reactor"
|
||||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension Node for ComfyUI, based on ReActor SD-WebUI Face Swap Extension"
|
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
|
||||||
version = "0.5.2"
|
version = "0.7.1-b3"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.3", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
dependencies = ["onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Repository = "https://github.com/Gourieff/ComfyUI-ReActor"
|
Repository = "https://github.com/Gourieff/ComfyUI-ReActor"
|
||||||
# Used by Comfy Registry https://comfyregistry.org
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
[tool.comfy]
|
[tool.comfy]
|
||||||
PublisherId = "gourieff"
|
PublisherId = "gourieff"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
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 PIL import Image
|
||||||
from torchvision.models._utils import IntermediateLayerGetter as IntermediateLayerGetter
|
from torchvision.models._utils import IntermediateLayerGetter as IntermediateLayerGetter
|
||||||
|
|
||||||
from modules import shared
|
from r_modules import shared
|
||||||
|
|
||||||
from r_facelib.detection.align_trans import get_reference_facial_points, warp_and_crop_face
|
from r_facelib.detection.align_trans import get_reference_facial_points, warp_and_crop_face
|
||||||
from r_facelib.detection.retinaface.retinaface_net import FPN, SSH, MobileNetV1, make_bbox_head, make_class_head, make_landmark_head
|
from r_facelib.detection.retinaface.retinaface_net import FPN, SSH, MobileNetV1, make_bbox_head, make_class_head, make_landmark_head
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
class StableDiffusionProcessing:
|
class Processing:
|
||||||
|
|
||||||
def __init__(self, init_imgs):
|
def __init__(self, init_imgs):
|
||||||
self.init_images = init_imgs
|
self.init_images = init_imgs
|
||||||
self.width = init_imgs[0].width
|
self.width = init_imgs[0].width
|
||||||
self.height = init_imgs[0].height
|
self.height = init_imgs[0].height
|
||||||
self.extra_generation_params = {}
|
self.extra_generation_params = {}
|
||||||
|
self.bbox = []
|
||||||
|
self.swapped_indexes = []
|
||||||
|
|
||||||
|
|
||||||
class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
|
class ProcessingImg2Img(Processing):
|
||||||
|
|
||||||
def __init__(self, init_img):
|
def __init__(self, init_img):
|
||||||
super().__init__(init_img)
|
super().__init__(init_img)
|
||||||
@@ -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 math
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
from insightface.app.common import Face
|
from reactor_core.face_objects import Face
|
||||||
from safetensors.torch import save_file, safe_open
|
from safetensors.torch import save_file, safe_open
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import onnxruntime
|
import onnxruntime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
from comfy.utils import ProgressBar
|
||||||
|
|
||||||
ORT_SESSION = None
|
ORT_SESSION = None
|
||||||
|
|
||||||
@@ -24,6 +25,34 @@ def tensor_to_pil(img_tensor, batch_index=0):
|
|||||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8).squeeze())
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8).squeeze())
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
# def tensor_to_pil(img, batch_index=0):
|
||||||
|
# """Безопасное преобразование тензора в PIL.Image с обработкой особых случаев"""
|
||||||
|
# try:
|
||||||
|
# # Обработка пакетных данных
|
||||||
|
# if isinstance(img, torch.Tensor):
|
||||||
|
# if len(img.shape) == 4:
|
||||||
|
# img = img[batch_index] # Выбор элемента батча
|
||||||
|
# img = img.detach().cpu().numpy()
|
||||||
|
|
||||||
|
# # Нормализация и приведение типа
|
||||||
|
# if img.dtype == np.float32:
|
||||||
|
# img = np.clip(255. * img, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
# # Обработка нестандартных размерностей
|
||||||
|
# if img.shape[-1] > 4: # Если каналов больше 4
|
||||||
|
# img = img[..., :3] # Берем первые 3 канала
|
||||||
|
|
||||||
|
# # Преобразование в 2D/3D массив
|
||||||
|
# if len(img.shape) == 3 and img.shape[0] == 1: # [C, H, W] → [H, W]
|
||||||
|
# img = img.squeeze(0)
|
||||||
|
# elif len(img.shape) == 3 and img.shape[2] == 1: # [H, W, C=1]
|
||||||
|
# img = img.squeeze(-1)
|
||||||
|
|
||||||
|
# return Image.fromarray(img)
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# raise RuntimeError(f"Невозможно преобразовать тензор формы {img.shape} в PIL.Image") from e
|
||||||
|
|
||||||
|
|
||||||
def batch_tensor_to_pil(img_tensor):
|
def batch_tensor_to_pil(img_tensor):
|
||||||
# Convert tensor of shape [batch_size, channels, height, width] to a list of PIL Images
|
# Convert tensor of shape [batch_size, channels, height, width] to a list of PIL Images
|
||||||
@@ -208,6 +237,14 @@ def normalize_cropped_face(cropped_face):
|
|||||||
return cropped_face
|
return cropped_face
|
||||||
|
|
||||||
|
|
||||||
|
def progress_bar(total):
|
||||||
|
return ProgressBar(total)
|
||||||
|
|
||||||
|
def progress_bar_reset(pbar):
|
||||||
|
pbar.current = 0
|
||||||
|
pbar.update(0)
|
||||||
|
|
||||||
|
|
||||||
# author: Trung0246 --->
|
# author: Trung0246 --->
|
||||||
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
|
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
|
||||||
# Iterate over the list of full folder paths
|
# Iterate over the list of full folder paths
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
albumentations>=1.4.16
|
albumentations>=1.4.16
|
||||||
insightface==0.7.3
|
|
||||||
onnx>=1.14.0
|
onnx>=1.14.0
|
||||||
opencv-python>=4.7.0.72
|
opencv-python>=4.7.0.72
|
||||||
numpy==1.26.3
|
numpy
|
||||||
segment_anything
|
segment_anything
|
||||||
ultralytics
|
ultralytics
|
||||||
|
|||||||
+34
-12
@@ -312,15 +312,24 @@ class SafeToGPU:
|
|||||||
if is_same_device(device, 'cpu'):
|
if is_same_device(device, 'cpu'):
|
||||||
obj.to(device)
|
obj.to(device)
|
||||||
else:
|
else:
|
||||||
if is_same_device(obj.device, 'cpu'): # cpu to gpu
|
# TRY to check submodule device
|
||||||
|
current_device = None
|
||||||
|
try:
|
||||||
|
current_device = next(obj.parameters()).device
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if current_device is None or is_same_device(current_device, 'cpu'):
|
||||||
model_management.free_memory(self.size * 1.3, device)
|
model_management.free_memory(self.size * 1.3, device)
|
||||||
if model_management.get_free_memory(device) > self.size * 1.3:
|
if model_management.get_free_memory(device) > self.size * 1.3:
|
||||||
try:
|
try:
|
||||||
|
print("Moving to GPU...", end=" ")
|
||||||
obj.to(device)
|
obj.to(device)
|
||||||
|
print("OK")
|
||||||
except:
|
except:
|
||||||
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [1]")
|
print(f"Failed\nWARN: Model not moved to '{device}' [1]")
|
||||||
else:
|
else:
|
||||||
print(f"WARN: The model is not moved to the '{device}' due to insufficient memory. [2]")
|
print(f"WARN: Model not moved to '{device}' [2]")
|
||||||
|
|
||||||
def center_of_bbox(bbox):
|
def center_of_bbox(bbox):
|
||||||
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
@@ -532,9 +541,10 @@ def merge_and_stack_masks(stacked_masks, group_size):
|
|||||||
|
|
||||||
def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
|
def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
|
||||||
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
|
threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative):
|
||||||
if sam_model.is_auto_mode:
|
if getattr(sam_model, 'is_auto_mode', False):
|
||||||
device = model_management.get_torch_device()
|
device = model_management.get_torch_device()
|
||||||
sam_model.safe_to.to_device(sam_model, device=device)
|
# sam_model.safe_to.to_device(sam_model, device=device)
|
||||||
|
sam_model.to(device)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
predictor = SamPredictor(sam_model)
|
predictor = SamPredictor(sam_model)
|
||||||
@@ -588,18 +598,29 @@ def make_sam_mask_segmented(sam_model, segs, image, detection_hint, dilation,
|
|||||||
mask = combine_masks2(total_masks)
|
mask = combine_masks2(total_masks)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if sam_model.is_auto_mode:
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
sam_model.cpu()
|
sam_model.to(device)
|
||||||
|
# if sam_model.is_auto_mode:
|
||||||
|
# sam_model.cpu()
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
mask_working_device = torch.device("cpu")
|
# mask_working_device = torch.device("cpu")
|
||||||
|
mask_working_device = model_management.get_torch_device()
|
||||||
|
|
||||||
if mask is not None:
|
if mask is not None:
|
||||||
mask = mask.float()
|
mask = mask.float()
|
||||||
mask = dilate_mask(mask.cpu().numpy(), dilation)
|
# mask = dilate_mask(mask.cpu().numpy(), dilation)
|
||||||
mask = torch.from_numpy(mask)
|
# mask = torch.from_numpy(mask)
|
||||||
mask = mask.to(device=mask_working_device)
|
# mask = mask.to(device=mask_working_device)
|
||||||
|
mask = mask.unsqueeze(0).unsqueeze(0) # [1,1,H,W]
|
||||||
|
if dilation > 0:
|
||||||
|
kernel_size = 1 + dilation * 2
|
||||||
|
mask = torch.nn.functional.max_pool2d(mask, kernel_size=kernel_size, stride=1, padding=dilation)
|
||||||
|
elif dilation < 0:
|
||||||
|
kernel_size = 1 + abs(dilation) * 2
|
||||||
|
mask = -torch.nn.functional.max_pool2d(-mask, kernel_size=kernel_size, stride=1, padding=abs(dilation))
|
||||||
|
mask = mask.squeeze(0).squeeze(0) # [H,W]
|
||||||
else:
|
else:
|
||||||
# Extracting batch, height and width
|
# Extracting batch, height and width
|
||||||
height, width, _ = image.shape
|
height, width, _ = image.shape
|
||||||
@@ -641,7 +662,8 @@ def tensor2rgba(t: torch.Tensor) -> torch.Tensor:
|
|||||||
elif size[3] == 1:
|
elif size[3] == 1:
|
||||||
return t.repeat(1, 1, 1, 4)
|
return t.repeat(1, 1, 1, 4)
|
||||||
elif size[3] == 3:
|
elif size[3] == 3:
|
||||||
alpha_tensor = torch.ones((size[0], size[1], size[2], 1))
|
# alpha_tensor = torch.ones((size[0], size[1], size[2], 1)).to(t.device)
|
||||||
|
alpha_tensor = t.new_ones((size[0], size[1], size[2], 1))
|
||||||
return torch.cat((t, alpha_tensor), dim=3)
|
return torch.cat((t, alpha_tensor), dim=3)
|
||||||
else:
|
else:
|
||||||
return t
|
return t
|
||||||
|
|||||||
+16
-58
@@ -2,14 +2,14 @@ import os, glob
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import modules.scripts as scripts
|
import r_modules.scripts as scripts
|
||||||
# from modules.upscaler import Upscaler, UpscalerData
|
# from modules.upscaler import Upscaler, UpscalerData
|
||||||
from modules import scripts, scripts_postprocessing
|
from r_modules import scripts, scripts_postprocessing
|
||||||
from modules.processing import (
|
from r_modules.processing import (
|
||||||
StableDiffusionProcessing,
|
Processing,
|
||||||
StableDiffusionProcessingImg2Img,
|
ProcessingImg2Img,
|
||||||
)
|
)
|
||||||
from modules.shared import state
|
from r_modules.shared import state
|
||||||
from scripts.reactor_logger import logger
|
from scripts.reactor_logger import logger
|
||||||
from scripts.reactor_swapper import (
|
from scripts.reactor_swapper import (
|
||||||
swap_face,
|
swap_face,
|
||||||
@@ -26,7 +26,8 @@ import comfy.model_management as model_management
|
|||||||
def get_models():
|
def get_models():
|
||||||
swappers = [
|
swappers = [
|
||||||
"insightface",
|
"insightface",
|
||||||
"reswapper"
|
"reswapper",
|
||||||
|
"hyperswap"
|
||||||
]
|
]
|
||||||
models_list = []
|
models_list = []
|
||||||
for folder in swappers:
|
for folder in swappers:
|
||||||
@@ -42,7 +43,7 @@ class FaceSwapScript(scripts.Script):
|
|||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
p: StableDiffusionProcessing,
|
p: Processing,
|
||||||
img,
|
img,
|
||||||
enable,
|
enable,
|
||||||
source_faces_index,
|
source_faces_index,
|
||||||
@@ -101,12 +102,12 @@ class FaceSwapScript(scripts.Script):
|
|||||||
self.gender_target = 2
|
self.gender_target = 2
|
||||||
|
|
||||||
# if self.source is not None:
|
# if self.source is not None:
|
||||||
if isinstance(p, StableDiffusionProcessingImg2Img) and swap_in_source:
|
if isinstance(p, ProcessingImg2Img) and swap_in_source:
|
||||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
||||||
|
|
||||||
if len(p.init_images) == 1:
|
if len(p.init_images) == 1:
|
||||||
|
|
||||||
result = swap_face(
|
result, bbox, swapped_indexes = swap_face(
|
||||||
self.source,
|
self.source,
|
||||||
p.init_images[0],
|
p.init_images[0],
|
||||||
source_faces_index=self.source_faces_index,
|
source_faces_index=self.source_faces_index,
|
||||||
@@ -123,27 +124,11 @@ class FaceSwapScript(scripts.Script):
|
|||||||
interpolation=self.interpolation,
|
interpolation=self.interpolation,
|
||||||
)
|
)
|
||||||
p.init_images[0] = result
|
p.init_images[0] = result
|
||||||
|
p.bbox = bbox
|
||||||
# for i in range(len(p.init_images)):
|
p.swapped_indexes = swapped_indexes
|
||||||
# if state.interrupted or model_management.processing_interrupted():
|
|
||||||
# logger.status("Interrupted by User")
|
|
||||||
# break
|
|
||||||
# if len(p.init_images) > 1:
|
|
||||||
# logger.status(f"Swap in %s", i)
|
|
||||||
# result = swap_face(
|
|
||||||
# self.source,
|
|
||||||
# p.init_images[i],
|
|
||||||
# source_faces_index=self.source_faces_index,
|
|
||||||
# faces_index=self.faces_index,
|
|
||||||
# model=self.model,
|
|
||||||
# gender_source=self.gender_source,
|
|
||||||
# gender_target=self.gender_target,
|
|
||||||
# face_model=self.face_model,
|
|
||||||
# )
|
|
||||||
# p.init_images[i] = result
|
|
||||||
|
|
||||||
elif len(p.init_images) > 1:
|
elif len(p.init_images) > 1:
|
||||||
result = swap_face_many(
|
result, bbox, swapped_indexes = swap_face_many(
|
||||||
self.source,
|
self.source,
|
||||||
p.init_images,
|
p.init_images,
|
||||||
source_faces_index=self.source_faces_index,
|
source_faces_index=self.source_faces_index,
|
||||||
@@ -160,34 +145,7 @@ class FaceSwapScript(scripts.Script):
|
|||||||
interpolation=self.interpolation,
|
interpolation=self.interpolation,
|
||||||
)
|
)
|
||||||
p.init_images = result
|
p.init_images = result
|
||||||
|
p.bbox = bbox
|
||||||
|
p.swapped_indexes = swapped_indexes
|
||||||
|
|
||||||
logger.status("--Done!--")
|
logger.status("--Done!--")
|
||||||
# else:
|
|
||||||
# logger.error(f"Please provide a source face")
|
|
||||||
|
|
||||||
def postprocess_batch(self, p, *args, **kwargs):
|
|
||||||
if self.enable:
|
|
||||||
images = kwargs["images"]
|
|
||||||
|
|
||||||
def postprocess_image(self, p, script_pp: scripts.PostprocessImageArgs, *args):
|
|
||||||
if self.enable and self.swap_in_generated:
|
|
||||||
if self.source is not None:
|
|
||||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
|
||||||
image: Image.Image = script_pp.image
|
|
||||||
result = swap_face(
|
|
||||||
self.source,
|
|
||||||
image,
|
|
||||||
source_faces_index=self.source_faces_index,
|
|
||||||
faces_index=self.faces_index,
|
|
||||||
model=self.model,
|
|
||||||
upscale_options=self.upscale_options,
|
|
||||||
gender_source=self.gender_source,
|
|
||||||
gender_target=self.gender_target,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
pp = scripts_postprocessing.PostprocessedImage(result)
|
|
||||||
pp.info = {}
|
|
||||||
p.extra_generation_params.update(pp.info)
|
|
||||||
script_pp.image = pp.image
|
|
||||||
except:
|
|
||||||
logger.error(f"Cannot create a result image")
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import copy
|
import copy
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from modules import shared
|
from r_modules import shared
|
||||||
from reactor_utils import addLoggingLevel
|
from reactor_utils import addLoggingLevel
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+43
-6
@@ -1,18 +1,55 @@
|
|||||||
from transformers import pipeline
|
from transformers import pipeline
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import comfy.model_management as model_management
|
||||||
|
from reactor_utils import download
|
||||||
from scripts.reactor_logger import logger
|
from scripts.reactor_logger import logger
|
||||||
|
|
||||||
SCORE = 0.96
|
MODEL_EXISTS = False
|
||||||
|
|
||||||
|
def ensure_nsfw_model(nsfwdet_model_path):
|
||||||
|
"""Download NSFW detection model if it doesn't exist"""
|
||||||
|
global MODEL_EXISTS
|
||||||
|
downloaded = 0
|
||||||
|
nd_urls = [
|
||||||
|
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/config.json",
|
||||||
|
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/model.safetensors",
|
||||||
|
"https://huggingface.co/AdamCodd/vit-base-nsfw-detector/resolve/main/preprocessor_config.json",
|
||||||
|
]
|
||||||
|
for model_url in nd_urls:
|
||||||
|
model_name = os.path.basename(model_url)
|
||||||
|
model_path = os.path.join(nsfwdet_model_path, model_name)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
if not os.path.exists(nsfwdet_model_path):
|
||||||
|
os.makedirs(nsfwdet_model_path)
|
||||||
|
download(model_url, model_path, model_name)
|
||||||
|
if os.path.exists(model_path):
|
||||||
|
downloaded += 1
|
||||||
|
MODEL_EXISTS = True if downloaded == 3 else False
|
||||||
|
return MODEL_EXISTS
|
||||||
|
|
||||||
|
SCORE = 0.979
|
||||||
|
|
||||||
logging.getLogger("transformers").setLevel(logging.ERROR)
|
logging.getLogger("transformers").setLevel(logging.ERROR)
|
||||||
|
|
||||||
def nsfw_image(img_path: str, model_path: str):
|
def nsfw_image(img_data, model_path: str):
|
||||||
|
if not MODEL_EXISTS:
|
||||||
with Image.open(img_path) as img:
|
logger.status("Ensuring NSFW detection model exists...")
|
||||||
predict = pipeline("image-classification", model=model_path)
|
if not ensure_nsfw_model(model_path):
|
||||||
|
return True
|
||||||
|
device = model_management.get_torch_device()
|
||||||
|
with Image.open(io.BytesIO(img_data)) as img:
|
||||||
|
if "cpu" in str(device):
|
||||||
|
predict = pipeline("image-classification", model=model_path)
|
||||||
|
else:
|
||||||
|
device_id = 0
|
||||||
|
if "cuda" in str(device):
|
||||||
|
device_id = int(str(device).split(":")[1])
|
||||||
|
predict = pipeline("image-classification", model=model_path, device=device_id)
|
||||||
result = predict(img)
|
result = predict(img)
|
||||||
if result[0]["label"] == "nsfw" and result[0]["score"] > SCORE:
|
if result[0]["label"] == "nsfw" and result[0]["score"] > SCORE:
|
||||||
logger.status(f"NSFW content detected, skipping...")
|
logger.status(f'NSFW content detected with score={result[0]["score"]}, skipping...')
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
+184
-179
@@ -6,22 +6,22 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import insightface
|
from reactor_core.analyzer import ReActorFaceAnalysis
|
||||||
from insightface.app.common import Face
|
from reactor_core.face_objects import Face
|
||||||
# try:
|
from reactor_core.inswap import INSwapper
|
||||||
# import torch.cuda as cuda
|
from reactor_core.hyperswap import HyperSwapper
|
||||||
# except:
|
|
||||||
# cuda = None
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import comfy.model_management as model_management
|
import comfy.model_management as model_management
|
||||||
from modules.shared import state
|
from r_modules.shared import state
|
||||||
|
|
||||||
from scripts.reactor_logger import logger
|
from scripts.reactor_logger import logger
|
||||||
from reactor_utils import (
|
from reactor_utils import (
|
||||||
move_path,
|
move_path,
|
||||||
get_image_md5hash,
|
get_image_md5hash,
|
||||||
|
progress_bar,
|
||||||
|
progress_bar_reset
|
||||||
)
|
)
|
||||||
from scripts.r_faceboost import swapper, restorer
|
from scripts.r_faceboost import swapper, restorer
|
||||||
|
|
||||||
@@ -43,13 +43,6 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"ExecutionProviderError: {e}.\nEP is set to CPU.")
|
logger.debug(f"ExecutionProviderError: {e}.\nEP is set to CPU.")
|
||||||
providers = ["CPUExecutionProvider"]
|
providers = ["CPUExecutionProvider"]
|
||||||
# if cuda is not None:
|
|
||||||
# if cuda.is_available():
|
|
||||||
# providers = ["CUDAExecutionProvider"]
|
|
||||||
# else:
|
|
||||||
# providers = ["CPUExecutionProvider"]
|
|
||||||
# else:
|
|
||||||
# providers = ["CPUExecutionProvider"]
|
|
||||||
|
|
||||||
models_path_old = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
|
models_path_old = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
|
||||||
insightface_path_old = os.path.join(models_path_old, "insightface")
|
insightface_path_old = os.path.join(models_path_old, "insightface")
|
||||||
@@ -59,6 +52,7 @@ models_path = folder_paths.models_dir
|
|||||||
insightface_path = os.path.join(models_path, "insightface")
|
insightface_path = os.path.join(models_path, "insightface")
|
||||||
insightface_models_path = os.path.join(insightface_path, "models")
|
insightface_models_path = os.path.join(insightface_path, "models")
|
||||||
reswapper_path = os.path.join(models_path, "reswapper")
|
reswapper_path = os.path.join(models_path, "reswapper")
|
||||||
|
hyperswap_path = os.path.join(models_path, "hyperswap")
|
||||||
|
|
||||||
if os.path.exists(models_path_old):
|
if os.path.exists(models_path_old):
|
||||||
move_path(insightface_models_path_old, insightface_models_path)
|
move_path(insightface_models_path_old, insightface_models_path)
|
||||||
@@ -86,11 +80,6 @@ TARGET_IMAGE_LIST_HASH = []
|
|||||||
|
|
||||||
def unload_model(model):
|
def unload_model(model):
|
||||||
if model is not None:
|
if model is not None:
|
||||||
# check if model has unload method
|
|
||||||
# if "unload" in model:
|
|
||||||
# model.unload()
|
|
||||||
# if "model_unload" in model:
|
|
||||||
# model.model_unload()
|
|
||||||
del model
|
del model
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -108,7 +97,7 @@ def getAnalysisModel(det_size = (640, 640)):
|
|||||||
global ANALYSIS_MODELS
|
global ANALYSIS_MODELS
|
||||||
ANALYSIS_MODEL = ANALYSIS_MODELS[str(det_size[0])]
|
ANALYSIS_MODEL = ANALYSIS_MODELS[str(det_size[0])]
|
||||||
if ANALYSIS_MODEL is None:
|
if ANALYSIS_MODEL is None:
|
||||||
ANALYSIS_MODEL = insightface.app.FaceAnalysis(
|
ANALYSIS_MODEL = ReActorFaceAnalysis(
|
||||||
name="buffalo_l", providers=providers, root=insightface_path
|
name="buffalo_l", providers=providers, root=insightface_path
|
||||||
)
|
)
|
||||||
ANALYSIS_MODEL.prepare(ctx_id=0, det_size=det_size)
|
ANALYSIS_MODEL.prepare(ctx_id=0, det_size=det_size)
|
||||||
@@ -120,11 +109,18 @@ def getFaceSwapModel(model_path: str):
|
|||||||
if FS_MODEL is None or CURRENT_FS_MODEL_PATH is None or CURRENT_FS_MODEL_PATH != model_path:
|
if FS_MODEL is None or CURRENT_FS_MODEL_PATH is None or CURRENT_FS_MODEL_PATH != model_path:
|
||||||
CURRENT_FS_MODEL_PATH = model_path
|
CURRENT_FS_MODEL_PATH = model_path
|
||||||
FS_MODEL = unload_model(FS_MODEL)
|
FS_MODEL = unload_model(FS_MODEL)
|
||||||
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
|
|
||||||
|
model_filename = os.path.basename(model_path)
|
||||||
|
if "hyperswap" in model_filename.lower(): # Если это Hyperswap
|
||||||
|
model_path = os.path.join(folder_paths.models_dir, "hyperswap", model_filename)
|
||||||
|
FS_MODEL = HyperSwapper(model_path, providers=providers)
|
||||||
|
else: # Если это INSwapper / Reswapper
|
||||||
|
if "reswapper" in model_filename.lower():
|
||||||
|
model_path = os.path.join(folder_paths.models_dir, "reswapper", model_filename)
|
||||||
|
FS_MODEL = INSwapper(model_path, providers=providers)
|
||||||
|
|
||||||
return FS_MODEL
|
return FS_MODEL
|
||||||
|
|
||||||
|
|
||||||
def sort_by_order(face, order: str):
|
def sort_by_order(face, order: str):
|
||||||
if order == "left-right":
|
if order == "left-right":
|
||||||
return sorted(face, key=lambda x: x.bbox[0])
|
return sorted(face, key=lambda x: x.bbox[0])
|
||||||
@@ -136,42 +132,47 @@ def sort_by_order(face, order: str):
|
|||||||
return sorted(face, key=lambda x: x.bbox[1], reverse = True)
|
return sorted(face, key=lambda x: x.bbox[1], reverse = True)
|
||||||
if order == "small-large":
|
if order == "small-large":
|
||||||
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
|
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
|
||||||
# if order == "large-small":
|
|
||||||
# return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
|
|
||||||
# by default "large-small":
|
# by default "large-small":
|
||||||
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
|
return sorted(face, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)
|
||||||
|
|
||||||
def get_face_gender(
|
def get_face_gender(
|
||||||
face,
|
face,
|
||||||
face_index,
|
face_index,
|
||||||
gender_condition,
|
gender_condition,
|
||||||
operated: str,
|
operated: str,
|
||||||
order: str,
|
order: str,
|
||||||
):
|
):
|
||||||
gender = [
|
# 1. Сортируем ВСЕ найденные лица (без фильтрации!)
|
||||||
x.sex
|
faces_sorted = sort_by_order(face, order)
|
||||||
for x in face
|
|
||||||
]
|
# 2. Проверяем, существует ли вообще лицо с таким визуальным индексом
|
||||||
gender.reverse()
|
if face_index >= len(faces_sorted):
|
||||||
# If index is outside of bounds, return None, avoid exception
|
logger.info("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(faces_sorted) - 1)
|
||||||
if face_index >= len(gender):
|
return None, 0, None
|
||||||
logger.status("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(gender))
|
|
||||||
return None, 0
|
# 3. Берем конкретное лицо по его позиции на фото (например, второе справа)
|
||||||
face_gender = gender[face_index]
|
face_selected = faces_sorted[face_index]
|
||||||
logger.status("%s Face %s: Detected Gender -%s-", operated, face_index, face_gender)
|
|
||||||
if (gender_condition == 1 and face_gender == "F") or (gender_condition == 2 and face_gender == "M"):
|
# Если фильтр по полу отключен (no) - сразу отдаем лицо в работу
|
||||||
logger.status("OK - Detected Gender matches Condition")
|
if gender_condition == 0:
|
||||||
try:
|
return face_selected, 0, face_index
|
||||||
faces_sorted = sort_by_order(face, order)
|
|
||||||
return faces_sorted[face_index], 0
|
# 4. Проверяем пол выбранного лица
|
||||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
|
# face.gender: 0 = female, 1 = male
|
||||||
except IndexError:
|
# gender_condition: 1 = female, 2 = male
|
||||||
return None, 0
|
expected_gender = 0 if gender_condition == 1 else 1
|
||||||
else:
|
actual_gender = getattr(face_selected, 'gender', -1)
|
||||||
logger.status("WRONG - Detected Gender doesn't match Condition")
|
|
||||||
faces_sorted = sort_by_order(face, order)
|
sel_gender_str = "Male" if actual_gender == 1 else "Female" if actual_gender == 0 else "Unknown"
|
||||||
return faces_sorted[face_index], 1
|
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, sel_gender_str)
|
||||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 1
|
|
||||||
|
# Если пол не совпадает с тем, что заказал юзер
|
||||||
|
if actual_gender != expected_gender:
|
||||||
|
logger.info(f"{operated} Face {face_index}: WRONG gender ({sel_gender_str})")
|
||||||
|
return face_selected, 1, face_index # 1 означает флаг wrong_gender = True (цикл его пропустит)
|
||||||
|
|
||||||
|
# Если всё идеально
|
||||||
|
return face_selected, 0, face_index
|
||||||
|
|
||||||
def half_det_size(det_size):
|
def half_det_size(det_size):
|
||||||
logger.status("Trying to halve 'det_size' parameter")
|
logger.status("Trying to halve 'det_size' parameter")
|
||||||
@@ -179,7 +180,15 @@ def half_det_size(det_size):
|
|||||||
|
|
||||||
def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
|
def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
|
||||||
face_analyser = getAnalysisModel(det_size)
|
face_analyser = getAnalysisModel(det_size)
|
||||||
faces = face_analyser.get(img_data)
|
|
||||||
|
faces = []
|
||||||
|
try:
|
||||||
|
faces = face_analyser.get(img_data)
|
||||||
|
except Exception as e:
|
||||||
|
# import traceback
|
||||||
|
# traceback.print_exc()
|
||||||
|
# logger.error(f"Error during face analysis: {e}")
|
||||||
|
logger.error("No faces found")
|
||||||
|
|
||||||
# Try halving det_size if no faces are found
|
# Try halving det_size if no faces are found
|
||||||
if len(faces) == 0 and det_size[0] > 320 and det_size[1] > 320:
|
if len(faces) == 0 and det_size[0] > 320 and det_size[1] > 320:
|
||||||
@@ -212,10 +221,9 @@ def get_face_single(img_data: np.ndarray, face, face_index=0, det_size=(640, 640
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
faces_sorted = sort_by_order(face, order)
|
faces_sorted = sort_by_order(face, order)
|
||||||
return faces_sorted[face_index], 0
|
return faces_sorted[face_index], 0, face_index
|
||||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None, 0
|
return None, 0, None
|
||||||
|
|
||||||
|
|
||||||
def swap_face(
|
def swap_face(
|
||||||
@@ -236,6 +244,8 @@ def swap_face(
|
|||||||
):
|
):
|
||||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH
|
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH
|
||||||
result_image = target_img
|
result_image = target_img
|
||||||
|
bbox = []
|
||||||
|
swapped_indexes = []
|
||||||
|
|
||||||
if model is not None:
|
if model is not None:
|
||||||
|
|
||||||
@@ -312,82 +322,76 @@ def swap_face(
|
|||||||
logger.status("Using Hashed Target Face(s) Model...")
|
logger.status("Using Hashed Target Face(s) Model...")
|
||||||
target_faces = TARGET_FACES
|
target_faces = TARGET_FACES
|
||||||
|
|
||||||
# No use in trying to swap faces if no faces are found, enhancement
|
|
||||||
if len(target_faces) == 0:
|
if len(target_faces) == 0:
|
||||||
logger.status("Cannot detect any Target, skipping swapping...")
|
logger.status("Cannot detect any Target, skipping swapping...")
|
||||||
return result_image
|
return result_image, bbox, swapped_indexes
|
||||||
|
|
||||||
|
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||||
|
|
||||||
|
# 1. Заранее собираем список ТОЛЬКО ВАЛИДНЫХ исходных лиц
|
||||||
|
valid_source_faces = []
|
||||||
if source_img is not None:
|
if source_img is not None:
|
||||||
# separated management of wrong_gender between source and target, enhancement
|
for idx in source_faces_index:
|
||||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||||
|
if sf is not None and src_wrong_gender == 0:
|
||||||
|
valid_source_faces.append(sf)
|
||||||
else:
|
else:
|
||||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
if sf is not None and src_wrong_gender == 0:
|
||||||
src_wrong_gender = 0
|
valid_source_faces.append(sf)
|
||||||
|
|
||||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
if len(valid_source_faces) == 0:
|
||||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||||
elif source_face is not None:
|
else:
|
||||||
result = target_img
|
result = target_img
|
||||||
if "inswapper" in model:
|
if "inswapper" in model:
|
||||||
model_path = os.path.join(insightface_path, model)
|
model_path = os.path.join(insightface_path, model)
|
||||||
elif "reswapper" in model:
|
elif "reswapper" in model:
|
||||||
model_path = os.path.join(reswapper_path, model)
|
model_path = os.path.join(reswapper_path, model)
|
||||||
|
elif "hyperswap" in model:
|
||||||
|
model_path = os.path.join(hyperswap_path, model)
|
||||||
|
|
||||||
face_swapper = getFaceSwapModel(model_path)
|
face_swapper = getFaceSwapModel(model_path)
|
||||||
|
|
||||||
source_face_idx = 0
|
source_face_idx = 0
|
||||||
|
|
||||||
|
# 2. Идем по целевым лицам
|
||||||
for face_num in faces_index:
|
for face_num in faces_index:
|
||||||
# No use in trying to swap faces if no further faces are found, enhancement
|
target_face, wrong_gender, target_face_index = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||||
if face_num >= len(target_faces):
|
|
||||||
logger.status("Checked all existing target faces, skipping swapping...")
|
if target_face is not None and wrong_gender == 0:
|
||||||
break
|
logger.status(f"Swapping...")
|
||||||
|
|
||||||
if len(source_faces_index) > 1 and source_face_idx > 0:
|
# 3. Берем валидное лицо (если их меньше, чем целей — идем по кругу)
|
||||||
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_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||||
source_face_idx += 1
|
|
||||||
|
if face_boost_enabled and "hyperswap" not in model:
|
||||||
if source_face is not None and src_wrong_gender == 0:
|
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
|
||||||
target_face, wrong_gender = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
bgr_fake, M = face_swapper.get(result, target_face, source_face_to_use, paste_back=False)
|
||||||
if target_face is not None and wrong_gender == 0:
|
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||||
logger.status(f"Swapping...")
|
M *= scale
|
||||||
if face_boost_enabled:
|
result = swapper.in_swap(result, bgr_fake, M)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
logger.status(f"No target face found for {face_num}")
|
result = face_swapper.get(result, target_face, source_face_to_use)
|
||||||
elif src_wrong_gender == 1:
|
|
||||||
src_wrong_gender = 0
|
bbox.append(tuple(map(float, target_face.bbox)))
|
||||||
# Keep searching for other faces if wrong gender is detected, enhancement
|
swapped_indexes.append(target_face_index)
|
||||||
#if source_face_idx == len(source_faces_index):
|
|
||||||
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
# Продвигаем индекс исходного лица ТОЛЬКО после УСПЕШНОГО применения
|
||||||
# return result_image
|
if len(valid_source_faces) > 1:
|
||||||
logger.status("Wrong source gender detected")
|
source_face_idx += 1
|
||||||
|
|
||||||
|
elif wrong_gender == 1:
|
||||||
|
logger.status("Wrong target gender detected")
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
logger.info(f"No target face found for {face_num}")
|
||||||
|
|
||||||
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||||
|
|
||||||
else:
|
|
||||||
logger.status("No source face(s) in the provided Index")
|
|
||||||
else:
|
else:
|
||||||
logger.status("No source face(s) found")
|
logger.status("No source face(s) found")
|
||||||
return result_image
|
return result_image, bbox, swapped_indexes
|
||||||
|
|
||||||
def swap_face_many(
|
def swap_face_many(
|
||||||
source_img: Union[Image.Image, None],
|
source_img: Union[Image.Image, None],
|
||||||
@@ -407,28 +411,23 @@ def swap_face_many(
|
|||||||
):
|
):
|
||||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, TARGET_FACES_LIST, TARGET_IMAGE_LIST_HASH
|
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, TARGET_FACES_LIST, TARGET_IMAGE_LIST_HASH
|
||||||
result_images = target_imgs
|
result_images = target_imgs
|
||||||
|
bbox = []
|
||||||
|
swapped_indexes = []
|
||||||
|
|
||||||
if model is not None:
|
if model is not None:
|
||||||
|
if isinstance(source_img, str):
|
||||||
if isinstance(source_img, str): # source_img is a base64 string
|
|
||||||
import base64, io
|
import base64, io
|
||||||
if 'base64,' in source_img: # check if the base64 string has a data URL scheme
|
if 'base64,' in source_img:
|
||||||
# split the base64 string to get the actual base64 encoded image data
|
|
||||||
base64_data = source_img.split('base64,')[-1]
|
base64_data = source_img.split('base64,')[-1]
|
||||||
# decode base64 string to bytes
|
|
||||||
img_bytes = base64.b64decode(base64_data)
|
img_bytes = base64.b64decode(base64_data)
|
||||||
else:
|
else:
|
||||||
# if no data URL scheme, just decode
|
|
||||||
img_bytes = base64.b64decode(source_img)
|
img_bytes = base64.b64decode(source_img)
|
||||||
|
|
||||||
source_img = Image.open(io.BytesIO(img_bytes))
|
source_img = Image.open(io.BytesIO(img_bytes))
|
||||||
|
|
||||||
target_imgs = [cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) for target_img in target_imgs]
|
target_imgs = [cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) for target_img in target_imgs]
|
||||||
|
|
||||||
if source_img is not None:
|
if source_img is not None:
|
||||||
|
|
||||||
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
source_image_md5hash = get_image_md5hash(source_img)
|
source_image_md5hash = get_image_md5hash(source_img)
|
||||||
|
|
||||||
if SOURCE_IMAGE_HASH is None:
|
if SOURCE_IMAGE_HASH is None:
|
||||||
@@ -451,18 +450,22 @@ def swap_face_many(
|
|||||||
source_faces = SOURCE_FACES
|
source_faces = SOURCE_FACES
|
||||||
|
|
||||||
elif face_model is not None:
|
elif face_model is not None:
|
||||||
|
|
||||||
source_faces_index = [0]
|
source_faces_index = [0]
|
||||||
logger.status("Using Loaded Source Face Model...")
|
logger.status("Using Loaded Source Face Model...")
|
||||||
source_face_model = [face_model]
|
source_face_model = [face_model]
|
||||||
source_faces = source_face_model
|
source_faces = source_face_model
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.error("Cannot detect any Source")
|
logger.error("Cannot detect any Source")
|
||||||
|
|
||||||
if source_faces is not None:
|
if source_faces is not None:
|
||||||
|
|
||||||
target_faces = []
|
target_faces = []
|
||||||
|
pbar = progress_bar(len(target_imgs))
|
||||||
|
|
||||||
|
if len(TARGET_IMAGE_LIST_HASH) > 0:
|
||||||
|
logger.status(f"Using Hashed Target Face(s) Model...")
|
||||||
|
else:
|
||||||
|
logger.status(f"Analyzing Target Image...")
|
||||||
|
|
||||||
for i, target_img in enumerate(target_imgs):
|
for i, target_img in enumerate(target_imgs):
|
||||||
if state.interrupted or model_management.processing_interrupted():
|
if state.interrupted or model_management.processing_interrupted():
|
||||||
logger.status("Interrupted by User")
|
logger.status("Interrupted by User")
|
||||||
@@ -484,93 +487,95 @@ def swap_face_many(
|
|||||||
logger.info("(Image %s) Target Image the Same? %s", i, target_image_same)
|
logger.info("(Image %s) Target Image the Same? %s", i, target_image_same)
|
||||||
|
|
||||||
if len(TARGET_FACES_LIST) == 0:
|
if len(TARGET_FACES_LIST) == 0:
|
||||||
logger.status(f"Analyzing Target Image {i}...")
|
|
||||||
target_face = analyze_faces(target_img)
|
target_face = analyze_faces(target_img)
|
||||||
TARGET_FACES_LIST = [target_face]
|
TARGET_FACES_LIST = [target_face]
|
||||||
elif len(TARGET_FACES_LIST) == i and not target_image_same:
|
elif len(TARGET_FACES_LIST) == i and not target_image_same:
|
||||||
logger.status(f"Analyzing Target Image {i}...")
|
|
||||||
target_face = analyze_faces(target_img)
|
target_face = analyze_faces(target_img)
|
||||||
TARGET_FACES_LIST.append(target_face)
|
TARGET_FACES_LIST.append(target_face)
|
||||||
elif len(TARGET_FACES_LIST) != i and not target_image_same:
|
elif len(TARGET_FACES_LIST) != i and not target_image_same:
|
||||||
logger.status(f"Analyzing Target Image {i}...")
|
|
||||||
target_face = analyze_faces(target_img)
|
target_face = analyze_faces(target_img)
|
||||||
TARGET_FACES_LIST[i] = target_face
|
TARGET_FACES_LIST[i] = target_face
|
||||||
elif target_image_same:
|
elif target_image_same:
|
||||||
logger.status("(Image %s) Using Hashed Target Face(s) Model...", i)
|
|
||||||
target_face = TARGET_FACES_LIST[i]
|
target_face = TARGET_FACES_LIST[i]
|
||||||
|
|
||||||
|
|
||||||
# logger.status(f"Analyzing Target Image {i}...")
|
|
||||||
# target_face = analyze_faces(target_img)
|
|
||||||
if target_face is not None:
|
if target_face is not None:
|
||||||
target_faces.append(target_face)
|
target_faces.append(target_face)
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
# No use in trying to swap faces if no faces are found, enhancement
|
progress_bar_reset(pbar)
|
||||||
|
|
||||||
if len(target_faces) == 0:
|
if len(target_faces) == 0:
|
||||||
logger.status("Cannot detect any Target, skipping swapping...")
|
logger.status("Cannot detect any Target, skipping swapping...")
|
||||||
return result_images
|
return result_images, bbox, swapped_indexes
|
||||||
|
|
||||||
|
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||||
|
|
||||||
|
valid_source_faces = []
|
||||||
if source_img is not None:
|
if source_img is not None:
|
||||||
# separated management of wrong_gender between source and target, enhancement
|
for idx in source_faces_index:
|
||||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||||
|
if sf is not None and src_wrong_gender == 0:
|
||||||
|
valid_source_faces.append(sf)
|
||||||
else:
|
else:
|
||||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
if sf is not None and src_wrong_gender == 0:
|
||||||
src_wrong_gender = 0
|
valid_source_faces.append(sf)
|
||||||
|
|
||||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
if len(valid_source_faces) == 0:
|
||||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||||
elif source_face is not None:
|
else:
|
||||||
results = target_imgs
|
results = target_imgs
|
||||||
model_path = model_path = os.path.join(insightface_path, model)
|
if "inswapper" in model:
|
||||||
|
model_path = os.path.join(insightface_path, model)
|
||||||
|
elif "reswapper" in model:
|
||||||
|
model_path = os.path.join(reswapper_path, model)
|
||||||
|
elif "hyperswap" in model:
|
||||||
|
model_path = os.path.join(hyperswap_path, model)
|
||||||
|
|
||||||
face_swapper = getFaceSwapModel(model_path)
|
face_swapper = getFaceSwapModel(model_path)
|
||||||
|
|
||||||
source_face_idx = 0
|
source_face_idx = 0
|
||||||
|
pbar = progress_bar(len(target_imgs))
|
||||||
|
logger.status(f"Swapping...")
|
||||||
|
|
||||||
for face_num in faces_index:
|
for face_num in faces_index:
|
||||||
# No use in trying to swap faces if no further faces are found, enhancement
|
target_used_in_any_image = False
|
||||||
if face_num >= len(target_faces):
|
|
||||||
logger.status("Checked all existing target faces, skipping swapping...")
|
for i, (target_img, target_face_list) in enumerate(zip(results, target_faces)):
|
||||||
break
|
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 len(source_faces_index) > 1 and source_face_idx > 0:
|
if target_face_single is not None and wrong_gender == 0:
|
||||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
|
target_used_in_any_image = True
|
||||||
source_face_idx += 1
|
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||||
|
|
||||||
if source_face is not None and src_wrong_gender == 0:
|
result = target_img
|
||||||
# Reading results to make current face swap on a previous face result
|
if face_boost_enabled and "hyperswap" not in model:
|
||||||
for i, (target_img, target_face) in enumerate(zip(results, target_faces)):
|
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face_to_use, paste_back=False)
|
||||||
target_face_single, wrong_gender = get_face_single(target_img, target_face, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||||
if target_face_single is not None and wrong_gender == 0:
|
M *= scale
|
||||||
result = target_img
|
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
logger.status(f"No target face found for {face_num}")
|
result = face_swapper.get(target_img, target_face_single, source_face_to_use)
|
||||||
elif src_wrong_gender == 1:
|
|
||||||
src_wrong_gender = 0
|
results[i] = result
|
||||||
logger.status("Wrong source gender detected")
|
bbox.append(tuple(map(float, target_face_single.bbox)))
|
||||||
continue
|
swapped_indexes.append(target_face_index)
|
||||||
else:
|
pbar.update(1)
|
||||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
|
||||||
|
elif wrong_gender == 1:
|
||||||
|
logger.status("Wrong target gender detected")
|
||||||
|
pbar.update(1)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger.info(f"{i}: No target face found for {face_num}")
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
if target_used_in_any_image and len(valid_source_faces) > 1:
|
||||||
|
source_face_idx += 1
|
||||||
|
|
||||||
|
progress_bar_reset(pbar)
|
||||||
result_images = [Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) for result in results]
|
result_images = [Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) for result in results]
|
||||||
|
|
||||||
else:
|
|
||||||
logger.status("No source face(s) in the provided Index")
|
|
||||||
else:
|
else:
|
||||||
logger.status("No source face(s) found")
|
logger.status("No source face(s) found")
|
||||||
return result_images
|
return result_images, bbox, swapped_indexes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
app_title = "ReActor Node for ComfyUI"
|
app_title = "ReActor Node for ComfyUI"
|
||||||
version_flag = "v0.5.2"
|
version_flag = "v0.7.1-b3"
|
||||||
|
|
||||||
COLORS = {
|
COLORS = {
|
||||||
"CYAN": "\033[0;36m", # CYAN
|
"CYAN": "\033[0;36m", # CYAN
|
||||||
|
|||||||
Reference in New Issue
Block a user