Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa813002f2 | ||
|
|
e5850cc5c3 | ||
|
|
5ec405e393 | ||
|
|
3725d4bf63 | ||
|
|
d7cabb2921 | ||
|
|
067ba6de36 | ||
|
|
0b377b00d4 | ||
|
|
314ad7fe9e | ||
|
|
3f66ebcba6 | ||
|
|
a12c5b19dc | ||
|
|
ea86b52e0f | ||
|
|
02a3518aa7 | ||
|
|
3746860aa4 | ||
|
|
f92ebeb653 | ||
|
|
f6776fded7 | ||
|
|
7b1233edc0 | ||
|
|
b3a0ac8cf5 | ||
|
|
de7e09db0f | ||
|
|
60b310dd7f | ||
|
|
4351dc4272 | ||
|
|
5f68562b6d | ||
|
|
b9f8683da5 | ||
|
|
78c8844f26 | ||
|
|
b01880c37f | ||
|
|
50fdb4993f | ||
|
|
8fe1c0ec6d | ||
|
|
619eb16a1e | ||
|
|
4dab1f1b0a | ||
|
|
a51e88ca24 | ||
|
|
99e23652a2 | ||
|
|
0aa14fabb4 | ||
|
|
ecd2b3c16d | ||
|
|
6ad6b35a4d | ||
|
|
2e34bf8356 | ||
|
|
be42ca37ee | ||
|
|
a7628ccdff | ||
|
|
59040c1557 | ||
|
|
e0cd862c3d | ||
|
|
fc1691dda0 | ||
|
|
33d0feff2b | ||
|
|
5881d3ee2f | ||
|
|
febdf3027a | ||
|
|
74146c26e0 | ||
|
|
55787ee169 | ||
|
|
ca24587deb | ||
|
|
11db21f666 | ||
|
|
1356f18bf7 | ||
|
|
b60036fc85 | ||
|
|
a2a61fd5e7 | ||
|
|
d93450bd80 | ||
|
|
67ce1186f8 | ||
|
|
e008d3c5ae | ||
|
|
b26892f342 | ||
|
|
b51d34e05a | ||
|
|
4ea6a541ec | ||
|
|
18fc8a303f | ||
|
|
43bafc227e | ||
|
|
85763bc0b7 | ||
|
|
b8d49816be | ||
|
|
95ed7b248f | ||
|
|
d8e3b90c82 | ||
|
|
13d836015b | ||
|
|
fd5f9e1de2 | ||
|
|
a2ad854a09 | ||
|
|
a708f71a2f | ||
|
|
6ee22a8ecb | ||
|
|
9d5dba9801 | ||
|
|
dd44fffd9e | ||
|
|
4eb66b3a8f | ||
|
|
e7dd3bbbc9 | ||
|
|
e9d34b3a91 | ||
|
|
c98dc50515 | ||
|
|
14595e4350 | ||
|
|
1ee4e21d21 | ||
|
|
0a5f8ebffd | ||
|
|
d218c52501 | ||
|
|
ec743a7de4 | ||
|
|
8e41e6c900 |
@@ -3,3 +3,4 @@ __pycache__/
|
||||
.vscode/
|
||||
example
|
||||
input
|
||||
*.dll
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
|
||||
|
||||

|
||||

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

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

|
||||
|
||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
||||
@@ -20,6 +12,9 @@
|
||||
</sup>
|
||||
</a>
|
||||
|
||||
<a href="https://t.me/reactor_faceswap" target="_blank"><img src="https://img.shields.io/badge/ReActor-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
<a href="https://t.me/artgourieff" target="_blank"><img src="https://img.shields.io/badge/ArtGourieff-2CA5E0?style=for-the-badge&logo=Telegram&logoColor=white&labelColor=blue"></img></a>
|
||||
|
||||
<hr>
|
||||
|
||||
[](https://github.com/Gourieff/ComfyUI-ReActor/commits/main)
|
||||
@@ -34,7 +29,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://github.com/Gourieff/comfyui-reactor-node) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
||||
### Ноды (nodes) для быстрой и простой замены лиц на любых изображениях для работы с ComfyUI, основан на [ранее заблокированном РеАкторе](https://web.archive.org/web/20241126185020/https://github.com/Gourieff/comfyui-reactor-node/blob/main/README_RU.md) - теперь имеется встроенный NSFW-детектор, исключающий замену лиц на изображениях с контентом 18+
|
||||
|
||||
> Используя данное ПО, вы понимаете и принимаете [ответственность](#disclaimer)
|
||||
|
||||
@@ -51,6 +46,67 @@
|
||||
|
||||
## Что нового в последнем обновлении
|
||||
|
||||
### 0.7.2 <sub><sup>BETA1</sup></sub>
|
||||
|
||||
- **Новый узел `Cinematic Enhancer`**. Забудьте о пластиковой коже и скучной картинке. Этот узел поможет вдохнуть душу в любое фотореалистичное изображение. Вы можете добавлять разные эффекты (простое и продвинутое зерно, виньетку, размытие, коррекцию цвета, HDR) и применить LUT.<br><u>Требования:</u>
|
||||
- Положите LUT-файлы в `models/luts`, небольшую коллекцию можете найти [тут](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/luts)<br>
|
||||
- `pip install colour-science` в Пайтон вашего Comfy -ИЛИ- просто запустите install.bat (пользователи Win) внутри папки РеАктора
|
||||
|
||||
*Вдохновлено такими узлами как: Image Apply LUT ([essentials](https://github.com/cubiq/ComfyUI_essentials)), LayerFilter: Film V2 ([LayerStyle](https://github.com/chflame163/ComfyUI_LayerStyle)), Color Correction ([post-processing-nodes](https://github.com/EllangoK/ComfyUI-post-processing-nodes)), HDR Effects ([SuperBeasts](https://github.com/SuperBeastsAI/ComfyUI-SuperBeasts))*
|
||||
|
||||
- Разделение узлов на подкатегории.
|
||||
|
||||
### 0.7.1
|
||||
|
||||
- **Новый узел `DLSS5 Frame Enhancer`**. Новейшая технология DLSS 5 от NVIDIA для улучшения качества изображения (кадра целиком или лиц по маске).<br><u>Следуйте [инструкции по установке](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>Интеграция работает в изолированном Host-режиме, что полностью исключает конфликты с CUDA-контекстом ComfyUI.<br>Огромная благодарность автору проекта [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) за C++ обертку (neuroframe_engine.dll и neuroframe_caller.dll), которая легла в основу вычислительного моста для данного узла.<br>Также благодарность Gemini 3.1 Pro (через [Google Gemini](https://gemini.google.com/app)) за ассистирование и помощь.</pre><u>Требования:</u>
|
||||
- Windows 10/11
|
||||
- NVIDIA display driver >= 616.x
|
||||
- NVIDIA RTX 40/50-series GPU
|
||||
(совместимость с более старыми сериями RTX не подтверждена)
|
||||
- Прочие улучшение и исправления
|
||||
|
||||
### 0.7.0
|
||||
|
||||
- 💥 **Важное обновление! ☢ Новое ядро РеАктора!**<br>✅ Библиотека `Insightface` больше не требуется!<br>✅ `C++ Build Tools` больше не требуются!<br>✅ Более простой процесс установки!<br>✅ Поддержка `Numpy 2.x` и `1.x`!<br>⚠ <u>Результат Свапа теперь слегка отличается</u>. Сложно сказать, хуже или лучше — результат просто чуть-чуть другой. По показателям схожести (оценка косинусного сходства векторов эмбеддингов лиц) результат стал немного лучше (хоть и незначительно), чем с Insightface.<br>Подробнее здесь: https://t.me/reactor_faceswap/55
|
||||
- Новый узел "Face Similarity" для проверки схожести лиц после свапа
|
||||
- Исправление "HyperSwap CPU Float normalization" (спасибо @Buumcode, Issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
|
||||
- Исправлено: проблема определения пола и логика индексов лиц (Issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
|
||||
- Улучшен `install.bat` для поддержки запуска установки в Comfy Desktop
|
||||
- Прочие улучшение и исправления
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
|
||||
### 0.6.2
|
||||
|
||||
- Добавлена поддержка моделей HyperSwap от команды FaceFusion Labs (спасибо [@Buumcode](https://github.com/Buumcode) за вариант реализации)<br>Модели можно скачать [отсюда](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>и положить в папку `ComfyUI/models/hyperswap`
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-04-3.jpg?raw=true" alt="0.6.2-whatsnew-04-3" width="100%"/>
|
||||
</center>
|
||||
|
||||
[Сравнение](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2_swapmodels_compare.png) моделей Inswapper, Reswapper, HyperSwap
|
||||
|
||||
- Теперь восстановление лиц затрагивает только заменённые лица
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-01-ru.jpg?raw=true" alt="0.6.2-whatsnew-01" width="100%"/>
|
||||
</center>
|
||||
|
||||
- Новый узел "Restore Face Advanced" с фильтром по лицам, спасибо https://github.com/Buumcode за реализацию "Фильтра восстановления лиц"<br>Этот узел помогает применить восстановление именно к нужному лицу или лицам
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-02.jpg?raw=true" alt="0.6.2-whatsnew-02" width="100%"/>
|
||||
</center>
|
||||
|
||||
- Добавлен выход FACE_MODEL_NAME для узла "Load Face Model"
|
||||
|
||||
<center>
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.2-whatsnew-03.jpg?raw=true" alt="0.6.2-whatsnew-03" width="50%"/>
|
||||
</center>
|
||||
|
||||
- Исправления и улучшения
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Улучшенная логика работы с индексами множества лиц при определении пола
|
||||
@@ -69,9 +125,6 @@
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.6.0-alpha1-01.gif?raw=true" alt="0.6.0-whatsnew-03" width="540px"/>
|
||||
</center>
|
||||
|
||||
<details>
|
||||
<summary><a>Предыдущие версии</a></summary>
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- Поддержка моделей ReSwapper. Несмотря на то, что Inswapper по-прежнему даёт лучшее сходство, но ReSwapper развивается - спасибо @somanchiu https://github.com/somanchiu/ReSwapper за эти модели и проект ReSwapper! Это хороший шаг для Сообщества в создании альтернативы Инсваппера!
|
||||
@@ -111,7 +164,7 @@ https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models
|
||||
- Сортировка моделей лиц по алфавиту
|
||||
- Множество исправлений и улучшений
|
||||
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
### [0.5.0 <sub><sup>BETA4</sup></sub>](https://web.archive.org/web/20241127121952/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.5.0)
|
||||
|
||||
- Поддержка библиотеки Spandrel при работе с GFPGAN
|
||||
|
||||
@@ -159,7 +212,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
- Небольшое улучшение скорости анализа целевых изображений (input)
|
||||
|
||||
### [0.4.2](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
### [0.4.2](https://web.archive.org/web/20241127034727/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.2)
|
||||
|
||||
- Добавлена поддержка GPEN-BFR-512 и RestoreFormer_Plus_Plus моделей восстановления лиц
|
||||
|
||||
@@ -182,12 +235,12 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
Базовый воркфлоу [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/workflows/ReActor--Build-Blended-Face-Model--v1.json)
|
||||
|
||||
### [0.4.1](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
### [0.4.1](https://web.archive.org/web/20241127044707/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.1)
|
||||
|
||||
- Поддержка CUDA 12 - не забудьте запустить (Windows) `install.bat` или (Linux/MacOS) `install.py` для используемого Python окружения или попробуйте установить ORT-GPU для CU12 вручную (https://onnxruntime.ai/docs/install/#install-onnx-runtime-gpu-cuda-12x)
|
||||
- Исправление Issue https://github.com/Gourieff/comfyui-reactor-node/issues/173
|
||||
- Исправление Issue [comfyui-reactor-node/issues/173](https://web.archive.org/web/20240919043728/https://github.com/Gourieff/comfyui-reactor-node/issues/173)
|
||||
|
||||
- Отдельный Нод для восстаноления лиц (FR https://github.com/Gourieff/comfyui-reactor-node/issues/191), располагается внутри меню ReActor (нод RestoreFace)
|
||||
- Отдельный Нод для восстаноления лиц (FR [comfyui-reactor-node/issues/191](https://web.archive.org/web/20241127040848/https://github.com/Gourieff/comfyui-reactor-node/issues/191)), располагается внутри меню ReActor (нод RestoreFace)
|
||||
- (Windows) Установка зависимостей теперь может быть выполнена в Python из PATH ОС
|
||||
- Разные исправления и улучшения
|
||||
|
||||
@@ -195,7 +248,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/0.4.1-whatsnew-01.jpg?raw=true" alt="0.4.1-whatsnew-01" width="100%"/>
|
||||
|
||||
### [0.4.0](https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
### [0.4.0](https://web.archive.org/web/20241119155323/https://github.com/Gourieff/comfyui-reactor-node/releases/tag/v0.4.0)
|
||||
|
||||
- Вход "input_image" теперь идёт первым, это даёт возможность корректного байпаса, а также это правильно с точки зрения расположения входов (главный вход - первый);
|
||||
- Теперь можно сохранять модели лиц в качестве файлов "safetensors" (`ComfyUI\models\reactor\faces`) и загружать их в ReActor, реализуя разные сценарии использования, а также храня супер легкие модели лиц, которые вы чаще всего используете:
|
||||
@@ -218,22 +271,24 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
|
||||
## Установка
|
||||
|
||||
<details>
|
||||
<summary>Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows</summary>
|
||||
### Портативная версия <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI</a> для Windows
|
||||
|
||||
1. Сделайте следующее:
|
||||
- Установите [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Например, версию Community - этот шаг нужен для правильной компиляции библиотеки Insightface)
|
||||
- ИЛИ только [VS C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/), выберите "Desktop Development with C++" в разделе "Workloads -> Desktop & Mobile"
|
||||
- ИЛИ если же вы не хотите устанавливать что-либо из вышеуказанного - выполните [данные шаги (раздел. I)](#insightfacebuild)
|
||||
2. Выберите из двух вариантов:
|
||||
1. Выберите из двух вариантов:
|
||||
- (ComfyUI Manager) Откройте ComfyUI Manager, нажвите "Install Custom Nodes", введите "ReActor" в поле "Search" и далее нажмите "Install". После того, как ComfyUI завершит установку, перезагрузите сервер.
|
||||
- (Вручную) Перейдите в `ComfyUI\custom_nodes`, откройте Консоль и выполните `git clone https://github.com/Gourieff/ComfyUI-ReActor`
|
||||
3. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
||||
4. Если модель "face_yolov8m.pt" у вас отсутствует - можете скачать её [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt) и положить в папку "ComfyUI\models\ultralytics\bbox"<br>
|
||||
То же самое и с "Sams" моделями, скачайте одну или обе [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams) - и положите в папку "ComfyUI\models\sams"
|
||||
5. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
||||
2. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
|
||||
3. Скачайте необходимые модели из Раздела "Модели" ниже
|
||||
4. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
|
||||
|
||||
</details>
|
||||
## Модели
|
||||
|
||||
- buffalo_l: скачиваются при первом запуске в `ComfyUI\models\insightface\models\buffalo_l`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- inswapper_128: скачивается при установке в `ComfyUI\models\insightface`, для ручного скачивания доступны [здесь](https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models)
|
||||
- reswapper_128/256: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models скачать в `ComfyUI\models\reswapper`
|
||||
- hyperswap_256: https://huggingface.co/facefusion/models-3.3.0/tree/main (hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx) скачать в `ComfyUI\models\hyperswap`
|
||||
- Face restoration models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/facerestore_models скачать любые предпочитаемые в `ComfyUI\models\facerestore_models`
|
||||
- Ultralytics model: https://huggingface.co/datasets/Gourieff/ReActor/blob/main/models/detection/bbox/face_yolov8m.pt скачать в `ComfyUI\models\ultralytics\bbox`
|
||||
- SAM models: https://huggingface.co/datasets/Gourieff/ReActor/tree/main/models/sams скачать в `ComfyUI\models\sams`
|
||||
|
||||
<a name="usage">
|
||||
|
||||
@@ -256,13 +311,17 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
- ReActorMakeFaceModelBatch (Создать пачку моделей лиц)
|
||||
- ••• Дополнительные •••
|
||||
- ReActorRestoreFace (Восстановление лиц)
|
||||
- ReActorRestoreFaceAdvanced (Восстановление лиц продвинутое)
|
||||
- ReActorFaceSimilarity (Оценка схожести лиц)
|
||||
- ReActorImageDublicator (Сделать из одного изображения несколько дубликатов)
|
||||
- ImageRGBA2RGB (Конвертировать RGBA в RGB)
|
||||
- ReActorUnload (Выгрузить модели РеАктора из VRAM)
|
||||
- DLSS5FrameEnhancer (Улучшение детализации кадра с NVIDIA DLSS 5)
|
||||
- ReActorCinematicEnhancer (Добавление кинематографических эффектов к изображению)
|
||||
|
||||
Соедините все необходимые слоты (slots) и запустите очередь (query).
|
||||
|
||||
### Входы основного Нода
|
||||
### Входы основного Узла
|
||||
|
||||
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
|
||||
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
|
||||
@@ -275,7 +334,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
|
||||
- `face_boost` - для соединения с ReActorFaceBoost;
|
||||
- Поддерживаемые ноды: "ReActorFaceBoost";
|
||||
|
||||
### Выходы основного Нода
|
||||
### Выходы основного Узла
|
||||
|
||||
- `IMAGE` - выход с готовым изображением (результатом);
|
||||
- Поддерживаемые ноды: любые ноды с изображением на входе;
|
||||
@@ -337,55 +396,19 @@ ReActor заменит только то лицо, которое удовлет
|
||||
|
||||
<a name="insightfacebuild">
|
||||
|
||||
### **I. (Для пользователей Windows) Если вы до сих пор не можете установить пакет Insightface по каким-то причинам или же просто не желаете устанавливать Visual Studio или VS C++ Build Tools - сделайте следующее:**
|
||||
|
||||
1. (ComfyUI Portable) Находясь в корневой директории, проверьте версию Python:<br>запустите CMD и выполните `python_embeded\python.exe -V`<br>Вы должны увидеть версию или 3.10, или 3.11, или 3.12
|
||||
2. Скачайте готовый пакет Insightface [для версии 3.10](https://github.com/Gourieff/sd-webui-reactor/raw/main/example/insightface-0.7.3-cp310-cp310-win_amd64.whl) или [для 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl) (если на предыдущем шаге вы увидели 3.11) или [для 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl) (если на предыдущем шаге вы увидели 3.12) и сохраните его в корневую директорию stable-diffusion-webui (A1111 или SD.Next) - туда, где лежит файл "webui-user.bat" -ИЛИ- в корневую директорию ComfyUI, если вы используете ComfyUI Portable
|
||||
3. Из корневой директории запустите:
|
||||
- (SD WebUI) CMD и `.\venv\Scripts\activate`
|
||||
- (ComfyUI Portable) CMD
|
||||
4. Обновите PIP:
|
||||
- (SD WebUI) `python -m pip install -U pip`
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install -U pip`
|
||||
5. Затем установите Insightface:
|
||||
- (SD WebUI) `pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
- (ComfyUI Portable) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl` (для 3.10) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl` (для 3.11) или `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl` (for 3.12)
|
||||
6. Готово!
|
||||
|
||||
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
|
||||
|
||||
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
|
||||
|
||||
Скачайте вручную по ссылке [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
|
||||
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
|
||||
|
||||
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
|
||||
|
||||
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
|
||||
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
|
||||
|
||||
### **IV. ControlNet Aux Node IMPORT failed - при использовании совместно с нодом ReActor**
|
||||
|
||||
1. Закройте или остановите ComfyUI сервер, если он запущен
|
||||
2. Перейдите в корневую папку ComfyUI, откройте консоль CMD и выполните следующее:
|
||||
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
|
||||
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
|
||||
3. Готово!
|
||||
|
||||
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
|
||||
|
||||
### **V. "ModuleNotFoundError: No module named 'basicsr'" или "subprocess-exited-with-error" при установке пакета future-0.18.3**
|
||||
|
||||
- Скачайте https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
|
||||
- Скопируйте файл в корневую папку ComfyUI и выполните в консоли:
|
||||
|
||||
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
|
||||
|
||||
- Затем:
|
||||
|
||||
python_embeded\python.exe -m pip install basicsr
|
||||
|
||||
### **VI. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
||||
### **III. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
|
||||
|
||||
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
|
||||
|
||||
|
||||
+3
-31
@@ -1,39 +1,11 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Добавляем путь расширения, чтобы Питон видел наши папки (r_facelib, scripts и т.д.)
|
||||
repo_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, repo_dir)
|
||||
original_modules = sys.modules.copy()
|
||||
if repo_dir not in sys.path:
|
||||
sys.path.insert(0, repo_dir)
|
||||
|
||||
# Place aside existing modules if using a1111 web ui
|
||||
modules_used = [
|
||||
"modules",
|
||||
"modules.images",
|
||||
"modules.processing",
|
||||
"modules.scripts_postprocessing",
|
||||
"modules.scripts",
|
||||
"modules.shared",
|
||||
]
|
||||
original_webui_modules = {}
|
||||
for module in modules_used:
|
||||
if module in sys.modules:
|
||||
original_webui_modules[module] = sys.modules.pop(module)
|
||||
|
||||
# Proceed with node setup
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
# Clean up imports
|
||||
# Remove repo directory from path
|
||||
sys.path.remove(repo_dir)
|
||||
# Remove any new modules
|
||||
modules_to_remove = []
|
||||
for module in sys.modules:
|
||||
if module not in original_modules and not module.startswith("google.protobuf") and not module.startswith("onnx") and not module.startswith("cv2"):
|
||||
modules_to_remove.append(module)
|
||||
for module in modules_to_remove:
|
||||
del sys.modules[module]
|
||||
|
||||
# Restore original modules
|
||||
sys.modules.update(original_webui_modules)
|
||||
|
||||
+10
-6
@@ -1,22 +1,25 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Try to use embedded python first
|
||||
if exist ..\..\..\python_embeded\python.exe (
|
||||
:: Use the embedded python
|
||||
:: Try to use the Desktop version's venv python first
|
||||
if exist ..\..\.venv\Scripts\python.exe (
|
||||
:: Use the ComfyUI Desktop venv python
|
||||
set PYTHON=..\..\.venv\Scripts\python.exe
|
||||
) else if exist ..\..\..\python_embeded\python.exe (
|
||||
:: Use the embedded python (portable version)
|
||||
set PYTHON=..\..\..\python_embeded\python.exe
|
||||
) else (
|
||||
:: Embedded python not found, check for python in the PATH
|
||||
:: Neither found, check for python in the PATH
|
||||
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
|
||||
set PYTHON_VERSION=%%F
|
||||
)
|
||||
if errorlevel 1 (
|
||||
echo I couldn't find an embedded version of Python, nor one in the Windows PATH. Please install manually.
|
||||
echo I couldn't find a venv python ^(Desktop^), an embedded python ^(Portable^), nor one in the Windows PATH. Please install manually.
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
:: Use python from the PATH (if it's the right version and the user agrees)
|
||||
echo I couldn't find an embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
||||
echo I couldn't find a venv or embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
|
||||
echo Would you like to proceed with the install using that version? (Y/N^)
|
||||
set /p USE_PYTHON=
|
||||
if /i "!USE_PYTHON!"=="Y" (
|
||||
@@ -30,6 +33,7 @@ if exist ..\..\..\python_embeded\python.exe (
|
||||
)
|
||||
|
||||
:: Install the package
|
||||
echo Using Python: %PYTHON%
|
||||
echo Installing...
|
||||
%PYTHON% install.py
|
||||
echo Done^!
|
||||
|
||||
@@ -7,6 +7,7 @@ import torchvision.transforms as T
|
||||
from torchvision.transforms.functional import normalize
|
||||
from torchvision.ops import masks_to_boxes
|
||||
|
||||
import onnxruntime
|
||||
import numpy as np
|
||||
import cv2
|
||||
import math
|
||||
@@ -14,12 +15,12 @@ from typing import List
|
||||
from PIL import Image
|
||||
import io
|
||||
from scipy import stats
|
||||
from insightface.app.common import Face
|
||||
from reactor_core.face_objects import Face
|
||||
from segment_anything import sam_model_registry
|
||||
|
||||
from modules.processing import StableDiffusionProcessingImg2Img
|
||||
from modules.shared import state
|
||||
# from comfy_extras.chainner_models import model_loading
|
||||
from r_modules.processing import ProcessingImg2Img
|
||||
from r_modules.shared import state
|
||||
|
||||
import comfy.model_management as model_management
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
@@ -55,7 +56,7 @@ from reactor_utils import (
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
from reactor_patcher import apply_patch
|
||||
|
||||
from r_facelib.utils.face_restoration_helper import FaceRestoreHelper
|
||||
from r_basicsr.utils.registry import ARCH_REGISTRY
|
||||
import scripts.r_archs.codeformer_arch
|
||||
@@ -65,6 +66,9 @@ import scripts.r_masking.segs as masking_segs
|
||||
|
||||
import scripts.reactor_sfw as sfw
|
||||
|
||||
from r_dlssnr.dlss5_node import DLSS5FrameEnhancer
|
||||
from r_nodes.cinematic_enhancer import ReActorCinematicEnhancer
|
||||
|
||||
|
||||
models_dir = folder_paths.models_dir
|
||||
REACTOR_MODELS_PATH = os.path.join(models_dir, "reactor")
|
||||
@@ -91,6 +95,17 @@ if "ultralytics" not in folder_paths.folder_names_and_paths:
|
||||
if "sams" not in folder_paths.folder_names_and_paths:
|
||||
add_folder_path_and_extensions("sams", [os.path.join(models_dir, "sams")], folder_paths.supported_pt_extensions)
|
||||
|
||||
def apply_log_level(console_log_level):
|
||||
if console_log_level == 0:
|
||||
logger.setLevel(logging.WARNING)
|
||||
onnxruntime.set_default_logger_severity(3) # Убивает ворнинги ORT
|
||||
elif console_log_level == 1:
|
||||
logger.setLevel(logging.STATUS)
|
||||
onnxruntime.set_default_logger_severity(3)
|
||||
elif console_log_level == 2:
|
||||
logger.setLevel(logging.INFO)
|
||||
onnxruntime.set_default_logger_severity(0)
|
||||
|
||||
def get_facemodels():
|
||||
models_path = os.path.join(FACE_MODELS_PATH, "*")
|
||||
models = glob.glob(models_path)
|
||||
@@ -159,7 +174,7 @@ class reactor:
|
||||
RETURN_TYPES = ("IMAGE","FACE_MODEL","IMAGE")
|
||||
RETURN_NAMES = ("SWAPPED_IMAGE","FACE_MODEL","ORIGINAL_IMAGE")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def __init__(self):
|
||||
# self.face_helper = None
|
||||
@@ -171,16 +186,39 @@ class reactor:
|
||||
self.interpolation = "Bicubic"
|
||||
self.boost_model_visibility = 1
|
||||
self.boost_cf_weight = 0.5
|
||||
self.last_swapped_bboxes = None
|
||||
# self.last_swapped_indices = None
|
||||
self.restore_swapped_only = True
|
||||
|
||||
def restore_face(
|
||||
self,
|
||||
input_image,
|
||||
face_restore_model,
|
||||
face_restore_visibility,
|
||||
codeformer_weight,
|
||||
facedetection,
|
||||
):
|
||||
self,
|
||||
input_image,
|
||||
face_restore_model,
|
||||
face_restore_visibility,
|
||||
codeformer_weight,
|
||||
facedetection,
|
||||
):
|
||||
|
||||
# from datetime import datetime
|
||||
# def _current_time():
|
||||
# current_datetime = datetime.now()
|
||||
# return current_datetime.strftime("%M:%S")
|
||||
|
||||
# локальная функция IoU
|
||||
def _iou(b1, b2):
|
||||
xA = max(b1[0], b2[0])
|
||||
yA = max(b1[1], b2[1])
|
||||
xB = min(b1[2], b2[2])
|
||||
yB = min(b1[3], b2[3])
|
||||
interW = max(0.0, xB - xA)
|
||||
interH = max(0.0, yB - yA)
|
||||
interArea = interW * interH
|
||||
if interArea == 0:
|
||||
return 0.0
|
||||
boxAArea = max(1.0, (b1[2]-b1[0]) * (b1[3]-b1[1]))
|
||||
boxBArea = max(1.0, (b2[2]-b2[0]) * (b2[3]-b2[1]))
|
||||
return interArea / float(boxAArea + boxBArea - interArea)
|
||||
|
||||
result = input_image
|
||||
|
||||
if face_restore_model != "none" and not model_management.processing_interrupted():
|
||||
@@ -256,52 +294,96 @@ class reactor:
|
||||
self.face_helper.read_image(cur_image_np)
|
||||
self.face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5)
|
||||
self.face_helper.align_warp_face()
|
||||
|
||||
# restored_face = None
|
||||
restored_faces = []
|
||||
|
||||
# берем сохранённые bbox из swap (или None)
|
||||
swapped_bboxes = getattr(self, "last_swapped_bboxes", None)
|
||||
# флаги, чтобы одно сохранённое bbox не совпало с несколькими лицами
|
||||
used_swapped = [False] * len(swapped_bboxes) if swapped_bboxes else None
|
||||
|
||||
restored_face = None
|
||||
IOU_THRESHOLD = 0.5
|
||||
|
||||
for idx, cropped_face in enumerate(self.face_helper.cropped_faces):
|
||||
|
||||
# if ".pth" in face_restore_model:
|
||||
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
|
||||
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
||||
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
|
||||
# определяем bbox текущего лица, который дал детектор внутри FaceRestoreHelper
|
||||
current_bbox = None
|
||||
if hasattr(self.face_helper, 'det_faces') and len(self.face_helper.det_faces) > idx:
|
||||
det = self.face_helper.det_faces[idx]
|
||||
# det м.б. [x1,y1,x2,y2,score]
|
||||
current_bbox = (float(det[0]), float(det[1]), float(det[2]), float(det[3]))
|
||||
|
||||
try:
|
||||
# логика: если у нас есть сохранённые bbox — ресторим только если iou с одним из них > порог
|
||||
do_restore = True
|
||||
# if self.last_swapped_indices is not None:
|
||||
# do_restore = idx in self.last_swapped_indices
|
||||
if swapped_bboxes and self.restore_swapped_only:
|
||||
do_restore = False
|
||||
if current_bbox is not None:
|
||||
for s_idx, sbox in enumerate(swapped_bboxes):
|
||||
if used_swapped is not None and used_swapped[s_idx]:
|
||||
continue
|
||||
if _iou(current_bbox, sbox) >= IOU_THRESHOLD:
|
||||
cx1 = (current_bbox[0] + current_bbox[2]) / 2
|
||||
cy1 = (current_bbox[1] + current_bbox[3]) / 2
|
||||
cx2 = (sbox[0] + sbox[2]) / 2
|
||||
cy2 = (sbox[1] + sbox[3]) / 2
|
||||
if abs(cx1 - cx2) < (current_bbox[2] - current_bbox[0]) * 0.25:
|
||||
if abs(cy1 - cy2) < (current_bbox[3] - current_bbox[1]) * 0.25:
|
||||
do_restore = True
|
||||
# do_restore = True
|
||||
if used_swapped is not None:
|
||||
used_swapped[s_idx] = True
|
||||
break
|
||||
|
||||
if do_restore:
|
||||
|
||||
# if ".pth" in face_restore_model:
|
||||
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
|
||||
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
||||
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
try:
|
||||
|
||||
if ".onnx" in face_restore_model: # ONNX models
|
||||
with torch.no_grad():
|
||||
|
||||
for ort_session_input in ort_session.get_inputs():
|
||||
if ort_session_input.name == "input":
|
||||
cropped_face_prep = prepare_cropped_face(cropped_face)
|
||||
ort_session_inputs[ort_session_input.name] = cropped_face_prep
|
||||
if ort_session_input.name == "weight":
|
||||
weight = np.array([ 1 ], dtype = np.double)
|
||||
ort_session_inputs[ort_session_input.name] = weight
|
||||
if ".onnx" in face_restore_model: # ONNX models
|
||||
|
||||
output = ort_session.run(None, ort_session_inputs)[0][0]
|
||||
restored_face = normalize_cropped_face(output)
|
||||
for ort_session_input in ort_session.get_inputs():
|
||||
if ort_session_input.name == "input":
|
||||
cropped_face_prep = prepare_cropped_face(cropped_face)
|
||||
ort_session_inputs[ort_session_input.name] = cropped_face_prep
|
||||
if ort_session_input.name == "weight":
|
||||
weight = np.array([ 1 ], dtype = np.double)
|
||||
ort_session_inputs[ort_session_input.name] = weight
|
||||
|
||||
else: # PTH models
|
||||
output = ort_session.run(None, ort_session_inputs)[0][0]
|
||||
restored_face = normalize_cropped_face(output)
|
||||
|
||||
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
|
||||
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
|
||||
else: # PTH models
|
||||
|
||||
del output
|
||||
torch.cuda.empty_cache()
|
||||
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
|
||||
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
|
||||
|
||||
except Exception as error:
|
||||
del output
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
print(f"\tFailed inference: {error}", file=sys.stderr)
|
||||
restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
|
||||
except Exception as error:
|
||||
|
||||
print(f"\tFailed inference: {error}", file=sys.stderr)
|
||||
# restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
|
||||
restored_face = cropped_face.copy()
|
||||
|
||||
else:
|
||||
restored_face = cropped_face.copy()
|
||||
|
||||
if face_restore_visibility < 1:
|
||||
restored_face = cropped_face * (1 - face_restore_visibility) + restored_face * face_restore_visibility
|
||||
|
||||
restored_face = restored_face.astype("uint8")
|
||||
self.face_helper.add_restored_face(restored_face)
|
||||
|
||||
|
||||
self.face_helper.get_inverse_affine(None)
|
||||
|
||||
restored_img = self.face_helper.paste_faces_to_input_image()
|
||||
@@ -328,8 +410,14 @@ class reactor:
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
# if hasattr(self, "last_swapped_bboxes"):
|
||||
self.last_swapped_bboxes = None
|
||||
# if hasattr(self, "last_swapped_indices"):
|
||||
# self.last_swapped_indices = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def execute(self, enabled, input_image, swap_model, detect_gender_source, detect_gender_input, source_faces_index, input_faces_index, console_log_level, face_restore_model,face_restore_visibility, codeformer_weight, facedetection, source_image=None, face_model=None, faces_order=None, face_boost=None):
|
||||
|
||||
device = model_management.get_torch_device()
|
||||
@@ -350,7 +438,7 @@ class reactor:
|
||||
if faces_order is None:
|
||||
faces_order = self.faces_order
|
||||
|
||||
apply_patch(console_log_level)
|
||||
apply_log_level(console_log_level)
|
||||
|
||||
if not enabled:
|
||||
return (input_image,face_model)
|
||||
@@ -388,7 +476,8 @@ class reactor:
|
||||
source = tensor_to_pil(source_image)
|
||||
else:
|
||||
source = None
|
||||
p = StableDiffusionProcessingImg2Img(pil_images)
|
||||
|
||||
p = ProcessingImg2Img(pil_images)
|
||||
script.process(
|
||||
p=p,
|
||||
img=source,
|
||||
@@ -410,6 +499,10 @@ class reactor:
|
||||
interpolation=self.interpolation,
|
||||
)
|
||||
result = batched_pil_to_tensor(p.init_images)
|
||||
# print(f"bbox={p.bbox}")
|
||||
if len(p.bbox) > 0:
|
||||
self.last_swapped_bboxes = p.bbox
|
||||
# self.last_swapped_indices = p.swapped_indexes
|
||||
original_image = input_image
|
||||
|
||||
if face_model is None:
|
||||
@@ -454,7 +547,7 @@ class ReActorPlusOpt:
|
||||
RETURN_TYPES = ("IMAGE","FACE_MODEL","IMAGE")
|
||||
RETURN_NAMES = ("SWAPPED_IMAGE","FACE_MODEL","ORIGINAL_IMAGE")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def __init__(self):
|
||||
# self.face_helper = None
|
||||
@@ -464,6 +557,7 @@ class ReActorPlusOpt:
|
||||
self.input_faces_index = "0"
|
||||
self.source_faces_index = "0"
|
||||
self.console_log_level = 1
|
||||
self.restore_swapped_only = True
|
||||
# self.face_size = 512
|
||||
self.face_boost_enabled = False
|
||||
self.restore = True
|
||||
@@ -481,6 +575,7 @@ class ReActorPlusOpt:
|
||||
self.detect_gender_source = options["detect_gender_source"]
|
||||
self.input_faces_index = options["input_faces_index"]
|
||||
self.source_faces_index = options["source_faces_index"]
|
||||
self.restore_swapped_only = options["restore_swapped_only"]
|
||||
|
||||
if face_boost is not None:
|
||||
self.face_boost_enabled = face_boost["enabled"]
|
||||
@@ -504,19 +599,21 @@ class LoadFaceModel:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FACE_MODEL",)
|
||||
RETURN_TYPES = ("FACE_MODEL","STRING")
|
||||
RETURN_NAMES = ("FACE_MODEL","FACE_MODEL_NAME")
|
||||
FUNCTION = "load_model"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def load_model(self, face_model):
|
||||
self.face_model = face_model
|
||||
face_model = face_model.split(".safetensors")[0] if ".safetensors" in face_model else face_model
|
||||
self.face_models_path = FACE_MODELS_PATH
|
||||
if self.face_model != "none":
|
||||
face_model_path = os.path.join(self.face_models_path, self.face_model)
|
||||
out = load_face_model(face_model_path)
|
||||
else:
|
||||
out = None
|
||||
return (out, )
|
||||
return (out,face_model)
|
||||
|
||||
|
||||
class ReActorWeight:
|
||||
@@ -539,7 +636,7 @@ class ReActorWeight:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def set_weight(self, input_image, faceswap_weight, face_model=None, source_image=None):
|
||||
|
||||
@@ -584,7 +681,7 @@ class ReActorWeight:
|
||||
|
||||
images_list: List[Image.Image] = []
|
||||
|
||||
apply_patch(1)
|
||||
apply_log_level(0)
|
||||
|
||||
if len(images) > 0:
|
||||
|
||||
@@ -643,7 +740,7 @@ class BuildFaceModel:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def build_face_model(self, image: Image.Image, det_size=(640, 640)):
|
||||
logging.StreamHandler.terminator = "\n"
|
||||
@@ -680,7 +777,7 @@ class BuildFaceModel:
|
||||
faces = []
|
||||
embeddings = []
|
||||
|
||||
apply_patch(1)
|
||||
apply_log_level(0)
|
||||
|
||||
if images is not None:
|
||||
images_list: List[Image.Image] = batch_tensor_to_pil(images)
|
||||
@@ -773,13 +870,13 @@ class SaveFaceModel:
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def save_model(self, save_mode, face_model_name, select_face_index, image=None, face_model=None, det_size=(640, 640)):
|
||||
if save_mode and image is not None:
|
||||
source = tensor_to_pil(image)
|
||||
source = cv2.cvtColor(np.array(source), cv2.COLOR_RGB2BGR)
|
||||
apply_patch(1)
|
||||
apply_log_level(0)
|
||||
logger.status("Building Face Model...")
|
||||
face_model_raw = analyze_faces(source, det_size)
|
||||
if len(face_model_raw) == 0:
|
||||
@@ -814,14 +911,275 @@ class RestoreFace:
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
|
||||
# def __init__(self):
|
||||
# self.face_helper = None
|
||||
# self.face_size = 512
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
|
||||
def execute(self, image, model, visibility, codeformer_weight, facedetection):
|
||||
result = reactor.restore_face(self,image,model,visibility,codeformer_weight,facedetection)
|
||||
result = reactor.restore_face(
|
||||
self, image, model, visibility, codeformer_weight, facedetection
|
||||
)
|
||||
return (result,)
|
||||
|
||||
|
||||
class RestoreFaceAdvanced:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"facedetection": (["retinaface_resnet50", "retinaface_mobile0.25", "YOLOv5l", "YOLOv5n"],),
|
||||
"model": (get_model_names(get_restorers),),
|
||||
"visibility": ("FLOAT", {"default": 1, "min": 0.0, "max": 1, "step": 0.05}),
|
||||
"codeformer_weight": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1, "step": 0.05}),
|
||||
"face_selection": (["all", "filter", "largest"],{"default": "all"}),
|
||||
},
|
||||
"optional": {
|
||||
"sort_by": (["area", "x_position", "y_position", "detection_confidence"],{"default": "area"}),
|
||||
"reverse_order": ("BOOLEAN", {"default": False}),
|
||||
"take_start": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
|
||||
"take_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
|
||||
def execute(
|
||||
self, image, model, visibility, codeformer_weight, facedetection, face_selection, sort_by="area", reverse_order=False, take_start=0, take_count=1
|
||||
):
|
||||
|
||||
min_x_position=0.0
|
||||
max_x_position=1.0
|
||||
min_y_position=0.0
|
||||
max_y_position=1.0
|
||||
|
||||
result = image
|
||||
|
||||
face_restore_model = model
|
||||
|
||||
if face_restore_model != "none" and not model_management.processing_interrupted():
|
||||
|
||||
global FACE_SIZE, FACE_HELPER
|
||||
|
||||
self.face_helper = FACE_HELPER
|
||||
|
||||
faceSize = 512
|
||||
if "1024" in face_restore_model.lower():
|
||||
faceSize = 1024
|
||||
elif "2048" in face_restore_model.lower():
|
||||
faceSize = 2048
|
||||
|
||||
logger.status(f"Restoring with {face_restore_model} | Face Size is set to {faceSize}")
|
||||
|
||||
model_path = folder_paths.get_full_path("facerestore_models", face_restore_model)
|
||||
|
||||
device = model_management.get_torch_device()
|
||||
|
||||
if "codeformer" in face_restore_model.lower():
|
||||
|
||||
codeformer_net = ARCH_REGISTRY.get("CodeFormer")(
|
||||
dim_embd=512,
|
||||
codebook_size=1024,
|
||||
n_head=8,
|
||||
n_layers=9,
|
||||
connect_list=["32", "64", "128", "256"],
|
||||
).to(device)
|
||||
checkpoint = torch.load(model_path)["params_ema"]
|
||||
codeformer_net.load_state_dict(checkpoint)
|
||||
facerestore_model = codeformer_net.eval()
|
||||
|
||||
elif ".onnx" in face_restore_model:
|
||||
|
||||
ort_session = set_ort_session(model_path, providers=providers)
|
||||
ort_session_inputs = {}
|
||||
facerestore_model = ort_session
|
||||
|
||||
else:
|
||||
|
||||
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
|
||||
facerestore_model = model_loading.load_state_dict(sd).eval()
|
||||
facerestore_model.to(device)
|
||||
|
||||
if faceSize != FACE_SIZE or self.face_helper is None:
|
||||
self.face_helper = FaceRestoreHelper(1, face_size=faceSize, crop_ratio=(1, 1), det_model=facedetection, save_ext='png', use_parse=True, device=device)
|
||||
FACE_SIZE = faceSize
|
||||
FACE_HELPER = self.face_helper
|
||||
|
||||
image_np = 255. * result.cpu().numpy()
|
||||
|
||||
total_images = image_np.shape[0]
|
||||
|
||||
out_images = []
|
||||
|
||||
pbar = progress_bar(total_images)
|
||||
|
||||
for i in range(total_images):
|
||||
|
||||
cur_image_np = image_np[i,:, :, ::-1]
|
||||
|
||||
original_resolution = cur_image_np.shape[0:2]
|
||||
|
||||
if facerestore_model is None or self.face_helper is None:
|
||||
return result
|
||||
|
||||
self.face_helper.clean_all()
|
||||
self.face_helper.read_image(cur_image_np)
|
||||
self.face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5)
|
||||
self.face_helper.align_warp_face()
|
||||
|
||||
# Face-Filter Mode
|
||||
|
||||
# Фильтрация лиц
|
||||
if face_selection != "all" and self.face_helper.cropped_faces:
|
||||
# Собираем информацию о лицах для фильтрации
|
||||
face_info = []
|
||||
img_height, img_width = cur_image_np.shape[0:2]
|
||||
|
||||
for j, face in enumerate(self.face_helper.cropped_faces):
|
||||
# Используем центр лица вместо левого верхнего угла
|
||||
if hasattr(self.face_helper, 'det_faces') and len(self.face_helper.det_faces) > j:
|
||||
bbox = self.face_helper.det_faces[j]
|
||||
# Вычисляем центр лица для более точного позиционирования
|
||||
x1 = ((bbox[0] + bbox[2]) / 2) / img_width # центр x
|
||||
y1 = ((bbox[1] + bbox[3]) / 2) / img_height # центр y
|
||||
area = face.shape[0] * face.shape[1]
|
||||
confidence = bbox[4] if len(bbox) > 4 else 1.0
|
||||
else:
|
||||
# Если информация о bbox недоступна, используем приблизительные данные
|
||||
area = face.shape[0] * face.shape[1]
|
||||
x1, y1 = 0.5, 0.5 # центр изображения
|
||||
confidence = 1.0
|
||||
|
||||
face_info.append({
|
||||
'index': j,
|
||||
'area': area,
|
||||
'x_position': x1,
|
||||
'y_position': y1,
|
||||
'detection_confidence': confidence
|
||||
})
|
||||
|
||||
# Сначала сортируем все лица по выбранному критерию
|
||||
all_indices = list(range(len(self.face_helper.cropped_faces)))
|
||||
|
||||
# Вывод для x_position и y_position
|
||||
if sort_by == "y_position":
|
||||
all_positions = [(idx, face_info[idx]['y_position']) for idx in all_indices]
|
||||
elif sort_by == "x_position":
|
||||
all_positions = [(idx, face_info[idx]['x_position']) for idx in all_indices]
|
||||
|
||||
# Сортировка по выбранному критерию
|
||||
sorted_indices = sorted(
|
||||
all_indices,
|
||||
key=lambda idx: face_info[idx][sort_by],
|
||||
reverse=reverse_order
|
||||
)
|
||||
|
||||
# Отладочный вывод после сортировки
|
||||
if sort_by == "y_position":
|
||||
sorted_positions = [(idx, face_info[idx]['y_position']) for idx in sorted_indices]
|
||||
elif sort_by == "x_position":
|
||||
sorted_positions = [(idx, face_info[idx]['x_position']) for idx in sorted_indices]
|
||||
|
||||
# Применяем фильтрацию в зависимости от режима
|
||||
if face_selection == "filter":
|
||||
# Фильтрация по координатам
|
||||
filtered_indices = [
|
||||
idx for idx in sorted_indices
|
||||
if min_x_position <= face_info[idx]['x_position'] <= max_x_position and
|
||||
min_y_position <= face_info[idx]['y_position'] <= max_y_position
|
||||
]
|
||||
|
||||
# Выборка по take_start и take_count
|
||||
selected_indices = filtered_indices[take_start:take_start + take_count]
|
||||
|
||||
elif face_selection == "largest":
|
||||
# При выборе "largest" просто берем take_count лиц с наибольшей площадью, начиная с take_start
|
||||
selected_indices = sorted_indices[take_start:take_start + take_count]
|
||||
|
||||
elif face_selection == "index":
|
||||
# В режиме "index" просто берем лица, начиная с take_start
|
||||
selected_indices = sorted_indices[take_start:take_start + take_count]
|
||||
|
||||
if selected_indices:
|
||||
self.face_helper.cropped_faces = [self.face_helper.cropped_faces[j] for j in selected_indices]
|
||||
if hasattr(self.face_helper, 'restored_faces'):
|
||||
self.face_helper.restored_faces = []
|
||||
if hasattr(self.face_helper, 'affine_matrices'):
|
||||
self.face_helper.affine_matrices = [self.face_helper.affine_matrices[j] for j in selected_indices]
|
||||
if hasattr(self.face_helper, 'det_faces'):
|
||||
self.face_helper.det_faces = [self.face_helper.det_faces[j] for j in selected_indices]
|
||||
|
||||
# Face-Filter Mode END
|
||||
|
||||
restored_face = None
|
||||
|
||||
for idx, cropped_face in enumerate(self.face_helper.cropped_faces):
|
||||
|
||||
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True)
|
||||
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
||||
cropped_face_t = cropped_face_t.unsqueeze(0).to(device)
|
||||
|
||||
try:
|
||||
|
||||
with torch.no_grad():
|
||||
|
||||
if ".onnx" in face_restore_model: # ONNX models
|
||||
|
||||
for ort_session_input in ort_session.get_inputs():
|
||||
if ort_session_input.name == "input":
|
||||
cropped_face_prep = prepare_cropped_face(cropped_face)
|
||||
ort_session_inputs[ort_session_input.name] = cropped_face_prep
|
||||
if ort_session_input.name == "weight":
|
||||
weight = np.array([ 1 ], dtype = np.double)
|
||||
ort_session_inputs[ort_session_input.name] = weight
|
||||
|
||||
output = ort_session.run(None, ort_session_inputs)[0][0]
|
||||
restored_face = normalize_cropped_face(output)
|
||||
|
||||
else: # PTH models
|
||||
|
||||
output = facerestore_model(cropped_face_t, w=codeformer_weight)[0] if "codeformer" in face_restore_model.lower() else facerestore_model(cropped_face_t)[0]
|
||||
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
|
||||
|
||||
del output
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
except Exception as error:
|
||||
|
||||
print(f"\tFailed inference: {error}", file=sys.stderr)
|
||||
restored_face = cropped_face.copy()
|
||||
|
||||
if visibility < 1:
|
||||
restored_face = cropped_face * (1 - visibility) + restored_face * visibility
|
||||
|
||||
restored_face = restored_face.astype("uint8")
|
||||
self.face_helper.add_restored_face(restored_face)
|
||||
|
||||
self.face_helper.get_inverse_affine(None)
|
||||
|
||||
restored_img = self.face_helper.paste_faces_to_input_image()
|
||||
restored_img = restored_img[:, :, ::-1]
|
||||
|
||||
if original_resolution != restored_img.shape[0:2]:
|
||||
restored_img = cv2.resize(restored_img, (0, 0), fx=original_resolution[1]/restored_img.shape[1], fy=original_resolution[0]/restored_img.shape[0], interpolation=cv2.INTER_AREA)
|
||||
|
||||
self.face_helper.clean_all()
|
||||
|
||||
out_images.append(restored_img)
|
||||
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
return image
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
restored_img_np = np.array(out_images).astype(np.float32) / 255.0
|
||||
restored_img_tensor = torch.from_numpy(restored_img_np)
|
||||
|
||||
result = restored_img_tensor
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (result,)
|
||||
|
||||
|
||||
@@ -867,7 +1225,7 @@ class MaskHelper:
|
||||
RETURN_TYPES = ("IMAGE","MASK","IMAGE","IMAGE")
|
||||
RETURN_NAMES = ("IMAGE","MASK","MASK_PREVIEW","SWAPPED_FACE")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Masking"
|
||||
|
||||
def execute(self, image, swapped_image, bbox_model_name, bbox_threshold, bbox_dilation, bbox_crop_factor, bbox_drop_size, sam_model_name, sam_dilation, sam_threshold, bbox_expansion, mask_hint_threshold, mask_hint_use_negative, morphology_operation, morphology_distance, blur_radius, sigma_factor, mask_optional=None):
|
||||
device = model_management.get_torch_device()
|
||||
@@ -1069,8 +1427,11 @@ class MaskHelper:
|
||||
result = image_base.detach().clone()
|
||||
face_segment = mask_image_final
|
||||
|
||||
pbar = progress_bar(MB)
|
||||
|
||||
for i in range(0, MB):
|
||||
if is_empty[i]:
|
||||
pbar.update(1)
|
||||
continue
|
||||
else:
|
||||
image_index = i
|
||||
@@ -1128,12 +1489,17 @@ class MaskHelper:
|
||||
face_segment[...,3] = mask[i]
|
||||
|
||||
result = rgba2rgb_tensor(result)
|
||||
result = result.cpu() # Перемещаем результат обратно на CPU
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
try:
|
||||
torch.cuda.empty_cache()
|
||||
except:
|
||||
pass
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (result, combined_mask, mask_blurred, face_segment)
|
||||
|
||||
def iterative_morphology(self, image, distance, op="dilate"):
|
||||
@@ -1162,7 +1528,7 @@ class ImageDublicator:
|
||||
RETURN_NAMES = ("IMAGES",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Utils"
|
||||
|
||||
def execute(self, image, count):
|
||||
images = [image for i in range(count)]
|
||||
@@ -1180,7 +1546,7 @@ class ImageRGBA2RGB:
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Utils"
|
||||
|
||||
def execute(self, image):
|
||||
out = rgba2rgb_tensor(image)
|
||||
@@ -1211,7 +1577,7 @@ class MakeFaceModelBatch:
|
||||
RETURN_NAMES = ("FACE_MODELS",)
|
||||
FUNCTION = "execute"
|
||||
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def execute(self, **kwargs):
|
||||
if len(kwargs) > 0:
|
||||
@@ -1238,14 +1604,15 @@ class ReActorOptions:
|
||||
"source_faces_index": ("STRING", {"default": "0"}),
|
||||
"detect_gender_source": (["no","female","male"], {"default": "no"}),
|
||||
"console_log_level": ([0, 1, 2], {"default": 1}),
|
||||
"restore_swapped_only": ("BOOLEAN", {"default": True, "label_off": "no", "label_on": "yes"})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("OPTIONS",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def execute(self,input_faces_order, input_faces_index, detect_gender_input, source_faces_order, source_faces_index, detect_gender_source, console_log_level):
|
||||
def execute(self,input_faces_order, input_faces_index, detect_gender_input, source_faces_order, source_faces_index, detect_gender_source, console_log_level, restore_swapped_only):
|
||||
options: dict = {
|
||||
"input_faces_order": input_faces_order,
|
||||
"input_faces_index": input_faces_index,
|
||||
@@ -1254,6 +1621,7 @@ class ReActorOptions:
|
||||
"source_faces_index": source_faces_index,
|
||||
"detect_gender_source": detect_gender_source,
|
||||
"console_log_level": console_log_level,
|
||||
"restore_swapped_only": restore_swapped_only,
|
||||
}
|
||||
return (options, )
|
||||
|
||||
@@ -1274,7 +1642,7 @@ class ReActorFaceBoost:
|
||||
|
||||
RETURN_TYPES = ("FACE_BOOST",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Face Swap"
|
||||
|
||||
def execute(self,enabled,boost_model,interpolation,visibility,codeformer_weight,restore_with_main_after):
|
||||
face_boost: dict = {
|
||||
@@ -1287,6 +1655,7 @@ class ReActorFaceBoost:
|
||||
}
|
||||
return (face_boost, )
|
||||
|
||||
|
||||
class ReActorUnload:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -1298,13 +1667,66 @@ class ReActorUnload:
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor"
|
||||
CATEGORY = "🌌 ReActor/Utils"
|
||||
|
||||
def execute(self, trigger):
|
||||
unload_all_models()
|
||||
return (trigger,)
|
||||
|
||||
|
||||
class ReActorFaceSimilarity:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image1": ("IMAGE",),
|
||||
"image2": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT", "STRING")
|
||||
RETURN_NAMES = ("similarity_float", "similarity_text")
|
||||
FUNCTION = "compare_faces"
|
||||
CATEGORY = "🌌 ReActor/Utils"
|
||||
|
||||
def compare_faces(self, image1, image2):
|
||||
|
||||
apply_log_level(0)
|
||||
|
||||
# 1. Конвертируем тензоры ComfyUI в формат OpenCV (BGR)
|
||||
img1_cv = 255. * image1[0].cpu().numpy()
|
||||
img1_cv = cv2.cvtColor(img1_cv.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
||||
|
||||
img2_cv = 255. * image2[0].cpu().numpy()
|
||||
img2_cv = cv2.cvtColor(img2_cv.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
||||
|
||||
# 2. Ищем лица через
|
||||
faces1 = analyze_faces(img1_cv, det_size=(640, 640))
|
||||
faces2 = analyze_faces(img2_cv, det_size=(640, 640))
|
||||
|
||||
# 3. Защита от отсутствия лиц
|
||||
if not faces1 or not faces2:
|
||||
return (0.0, "Face not found in one or both images")
|
||||
|
||||
# Берем первые найденные лица
|
||||
face1 = faces1[0]
|
||||
face2 = faces2[0]
|
||||
|
||||
# 4. Вычисляем косинусное сходство (Cosine Similarity)
|
||||
emb1 = face1.normed_embedding
|
||||
emb2 = face2.normed_embedding
|
||||
|
||||
# Скалярное произведение нормализованных векторов
|
||||
similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
|
||||
|
||||
# 5. Форматируем результат
|
||||
sim_float = float(similarity)
|
||||
sim_float = max(0.0, min(1.0, sim_float))
|
||||
sim_text = f"{sim_float * 100:.2f}%"
|
||||
|
||||
return (sim_float, sim_text)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
# --- MAIN NODES ---
|
||||
"ReActorFaceSwap": reactor,
|
||||
@@ -1320,9 +1742,13 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ReActorMakeFaceModelBatch": MakeFaceModelBatch,
|
||||
# --- Additional Nodes ---
|
||||
"ReActorRestoreFace": RestoreFace,
|
||||
"ReActorRestoreFaceAdvanced": RestoreFaceAdvanced,
|
||||
"ReActorFaceSimilarity": ReActorFaceSimilarity,
|
||||
"ReActorImageDublicator": ImageDublicator,
|
||||
"ImageRGBA2RGB": ImageRGBA2RGB,
|
||||
"ReActorUnload": ReActorUnload,
|
||||
"DLSS5FrameEnhancer": DLSS5FrameEnhancer,
|
||||
"ReActorCinematicEnhancer": ReActorCinematicEnhancer,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -1340,7 +1766,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ReActorMakeFaceModelBatch": "Make Face Model Batch 🌌 ReActor",
|
||||
# --- Additional Nodes ---
|
||||
"ReActorRestoreFace": "Restore Face 🌌 ReActor",
|
||||
"ReActorRestoreFaceAdvanced": "Restore Face Advanced 🌌 ReActor",
|
||||
"ReActorFaceSimilarity": "Face Similarity 🌌 ReActor",
|
||||
"ReActorImageDublicator": "Image Dublicator (List) 🌌 ReActor",
|
||||
"ImageRGBA2RGB": "Convert RGBA to RGB 🌌 ReActor",
|
||||
"ReActorUnload": "Unload ReActor Models 🌌 ReActor",
|
||||
"DLSS5FrameEnhancer": "DLSS5 Frame Enhancer 🌌 ReActor",
|
||||
"ReActorCinematicEnhancer": "Cinematic Enhancer 🌌 ReActor",
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
[project]
|
||||
name = "comfyui-reactor"
|
||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension Node for ComfyUI, based on ReActor SD-WebUI Face Swap Extension"
|
||||
version = "0.6.1"
|
||||
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
|
||||
version = "0.7.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["insightface==0.7.3", "onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy==1.26.4", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||
dependencies = ["onnx>=1.14.0", "opencv-python>=4.7.0.72", "numpy", "segment_anything", "albumentations>=1.4.16", "ultralytics"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/Gourieff/ComfyUI-ReActor"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "gourieff"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
## Third-party DLLs
|
||||
|
||||
1. Download [neuroframe_dlls.zip](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/neuroframe_dlls.zip) and place `neuroframe_caller.dll`\* and `neuroframe_engine.dll`* here (in `r_dlssnr/dll`)
|
||||
2. Place your `nvngx_dlssnr.dll`** here (in `r_dlssnr/dll`)
|
||||
|
||||
<sub>* Author [Merserk](https://github.com/Merserk), [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-Merserk.txt)
|
||||
<br>
|
||||
** Public distribution of this file is prohibited by NVIDIA, [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-NVIDIA-DLSS.txt)</sub>
|
||||
@@ -0,0 +1,147 @@
|
||||
import ctypes
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
import numpy as np
|
||||
|
||||
# --- КОНСТАНТЫ ---
|
||||
BRIDGE_ABI_VERSION = 6
|
||||
MEMORY_HOST = 0
|
||||
MEMORY_CUDA = 1
|
||||
MEMORY_NONE = 2
|
||||
|
||||
class NeuralBridgeError(Exception):
|
||||
pass
|
||||
|
||||
# --- C-СТРУКТУРЫ ДЛЯ ВЗАИМОДЕЙСТВИЯ С DLL ---
|
||||
|
||||
class RenderParameters(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("struct_size", ctypes.c_uint32),
|
||||
("abi_version", ctypes.c_uint32),
|
||||
("style", ctypes.c_int32),
|
||||
("intensity", ctypes.c_float),
|
||||
("tone", ctypes.c_float),
|
||||
("structure", ctypes.c_float),
|
||||
("skin", ctypes.c_float),
|
||||
("automask", ctypes.c_int32),
|
||||
("reset", ctypes.c_int32),
|
||||
("color_strength", ctypes.c_float),
|
||||
("tone_preservation", ctypes.c_float),
|
||||
("mask_memory_type", ctypes.c_uint32),
|
||||
("mask_width", ctypes.c_uint32),
|
||||
("mask_height", ctypes.c_uint32),
|
||||
("mask_stride", ctypes.c_uint32),
|
||||
("mask_plane", ctypes.c_uint64), # Из-за uint64 здесь будет 4 байта системного отступа
|
||||
("face_skin_protection", ctypes.c_float),
|
||||
("grain_preservation", ctypes.c_float),
|
||||
("nr_passes", ctypes.c_int32),
|
||||
("shimmer_suppression", ctypes.c_float),
|
||||
("prefer_nvof", ctypes.c_int32),
|
||||
]
|
||||
|
||||
|
||||
class DLSSStandaloneManager:
|
||||
def __init__(self, dll_dir: str):
|
||||
self._lock = threading.RLock()
|
||||
self._library = None
|
||||
self.dll_dir = dll_dir
|
||||
|
||||
def initialize(self, ordinal: int):
|
||||
with self._lock:
|
||||
if self._library is not None:
|
||||
return True
|
||||
|
||||
import os
|
||||
|
||||
if hasattr(os, 'add_dll_directory'):
|
||||
os.add_dll_directory(self.dll_dir)
|
||||
|
||||
engine_path = os.path.join(self.dll_dir, "neuroframe_engine.dll")
|
||||
if not os.path.exists(engine_path):
|
||||
raise NeuralBridgeError(f"Missing DLL: {engine_path}")
|
||||
|
||||
loader = getattr(ctypes, "WinDLL", ctypes.CDLL)
|
||||
try:
|
||||
self._library = loader(engine_path)
|
||||
except OSError as exc:
|
||||
raise NeuralBridgeError(f"DLL load failed: {exc}")
|
||||
|
||||
# Сигнатура инициализации
|
||||
self._library.dlss5nr_init.argtypes = [
|
||||
ctypes.c_int, ctypes.c_wchar_p, ctypes.c_char_p, ctypes.c_int
|
||||
]
|
||||
self._library.dlss5nr_init.restype = ctypes.c_int
|
||||
|
||||
# Сигнатура HOST-рендера (process_v6 вместо process_cuda_v6)
|
||||
c_float_p = ctypes.POINTER(ctypes.c_float)
|
||||
self._library.dlss5nr_process_v6.argtypes = [
|
||||
c_float_p, c_float_p, ctypes.c_int, ctypes.c_int,
|
||||
ctypes.POINTER(RenderParameters), ctypes.c_char_p, ctypes.c_int
|
||||
]
|
||||
self._library.dlss5nr_process_v6.restype = ctypes.c_int
|
||||
|
||||
try:
|
||||
self._library.dlss5nr_frame_abi_version.argtypes = []
|
||||
self._library.dlss5nr_frame_abi_version.restype = ctypes.c_uint32
|
||||
self.actual_abi = self._library.dlss5nr_frame_abi_version()
|
||||
except Exception:
|
||||
self.actual_abi = BRIDGE_ABI_VERSION
|
||||
|
||||
error = ctypes.create_string_buffer(4096)
|
||||
ok = self._library.dlss5nr_init(ordinal, self.dll_dir, error, len(error))
|
||||
|
||||
if not ok:
|
||||
err_msg = error.value.decode('utf-8', errors='ignore')
|
||||
raise NeuralBridgeError(f"Bridge initialization failed: {err_msg}")
|
||||
|
||||
return True
|
||||
|
||||
def process_host(self, source: np.ndarray, destination: np.ndarray, settings: dict, reset: bool, mask: np.ndarray = None):
|
||||
with self._lock:
|
||||
error = ctypes.create_string_buffer(4096)
|
||||
|
||||
params = RenderParameters()
|
||||
params.struct_size = ctypes.sizeof(RenderParameters)
|
||||
params.abi_version = getattr(self, "actual_abi", BRIDGE_ABI_VERSION)
|
||||
|
||||
params.style = int(settings.get("style"))
|
||||
params.intensity = float(settings.get("intensity"))
|
||||
params.tone = float(settings.get("local_tone"))
|
||||
params.structure = float(settings.get("local_structure"))
|
||||
params.skin = float(settings.get("skin_structure"))
|
||||
params.automask = int(bool(settings.get("auto_mask")))
|
||||
params.reset = int(bool(reset))
|
||||
params.color_strength = float(settings.get("color_strength"))
|
||||
params.tone_preservation = float(settings.get("tone_preservation"))
|
||||
params.face_skin_protection = float(settings.get("face_skin_protection"))
|
||||
params.grain_preservation = float(settings.get("grain_preservation"))
|
||||
params.nr_passes = int(settings.get("nr_passes"))
|
||||
params.shimmer_suppression = float(settings.get("shimmer_suppression", 0.0))
|
||||
params.prefer_nvof = int(bool(settings.get("prefer_nvof", False)))
|
||||
|
||||
# Обработка маски через HOST память
|
||||
params.mask_memory_type = MEMORY_NONE
|
||||
if mask is not None:
|
||||
params.mask_memory_type = MEMORY_HOST
|
||||
params.mask_width = int(mask.shape[1])
|
||||
params.mask_height = int(mask.shape[0])
|
||||
params.mask_stride = int(mask.strides[0])
|
||||
params.mask_plane = int(mask.ctypes.data) # Передаем указатель RAM
|
||||
|
||||
c_float_p = ctypes.POINTER(ctypes.c_float)
|
||||
|
||||
# Вызываем HOST функцию (DLL сама разберется с видеокартой)
|
||||
ok = self._library.dlss5nr_process_v6(
|
||||
source.ctypes.data_as(c_float_p),
|
||||
destination.ctypes.data_as(c_float_p),
|
||||
source.shape[1],
|
||||
source.shape[0],
|
||||
ctypes.byref(params),
|
||||
error,
|
||||
len(error)
|
||||
)
|
||||
|
||||
if not ok:
|
||||
err_msg = error.value.decode('utf-8', errors='ignore')
|
||||
raise NeuralBridgeError(f"DLSS-5 process failed: {err_msg}")
|
||||
@@ -0,0 +1,113 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
import os
|
||||
import comfy.model_management as model_management
|
||||
from scripts.reactor_logger import logger
|
||||
from .dlss5_core import DLSSStandaloneManager
|
||||
from r_modules.shared import state
|
||||
from reactor_utils import (
|
||||
batch_tensor_to_pil,
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
|
||||
class DLSS5FrameEnhancer:
|
||||
def __init__(self):
|
||||
self.device = model_management.get_torch_device()
|
||||
self.manager = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"style": (["Default", "Nature", "Cinematic"],),
|
||||
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
|
||||
"local_tone": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 0.0"}),
|
||||
"local_structure": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
|
||||
"skin_structure": ("FLOAT", {"default": 0.5, "min": -1.0, "max": 2.0, "step": 0.05, "tooltip": "-1..2, def: 0.5"}),
|
||||
"color_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
|
||||
"tone_preservation": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
|
||||
"face_skin_protection": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
|
||||
"grain_preservation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
|
||||
"nr_passes": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "0..4, def: 1"}),
|
||||
"auto_mask": ("BOOLEAN", {"default": False, "label_off": "OFF", "label_on": "ON", "tooltip": "AI Automatic masking of complex areas to prevent over-detailing"}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("enhanced_image",)
|
||||
FUNCTION = "enhance"
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
DESCRIPTION = (
|
||||
"Requirements:\n"
|
||||
"- NVIDIA display driver >= 616.x\n"
|
||||
"- NVIDIA RTX 40/50-series GPU\n"
|
||||
"(compatibility with older RTX series is unconfirmed)\n"
|
||||
"- DLLs: neuroframe_caller.dll, neuroframe_engine.dll, nvngx_dlssnr.dll\nin custom_nodes/ComfyUI-ReActor/r_dlssnr/dll\nor custom_nodes/comfyui-reactor-node/r_dlssnr/dll\n"
|
||||
"(see README.md in dll folder for instructions)"
|
||||
)
|
||||
|
||||
def load_bridge(self):
|
||||
if self.manager is None:
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
dll_dir = os.path.join(current_dir, "dll")
|
||||
self.manager = DLSSStandaloneManager(dll_dir)
|
||||
ordinal = getattr(self.device, 'index', 0) if self.device.index is not None else 0
|
||||
self.manager.initialize(ordinal)
|
||||
logger.status(f"DLSS-5 Bridge initialized on GPU {ordinal}")
|
||||
|
||||
def enhance(self, image, style, intensity, local_tone, local_structure,
|
||||
skin_structure, color_strength, tone_preservation,
|
||||
face_skin_protection, grain_preservation, nr_passes, auto_mask, mask=None):
|
||||
|
||||
self.load_bridge()
|
||||
|
||||
style_map = {"Default": 0, "Nature": 1, "Cinematic": 2}
|
||||
settings = {
|
||||
"style": style_map[style], "intensity": intensity, "local_tone": local_tone,
|
||||
"local_structure": local_structure, "skin_structure": skin_structure,
|
||||
"color_strength": color_strength, "tone_preservation": tone_preservation,
|
||||
"face_skin_protection": face_skin_protection, "grain_preservation": grain_preservation,
|
||||
"nr_passes": nr_passes, "auto_mask": auto_mask,
|
||||
"shimmer_suppression": 0.0, "prefer_nvof": False
|
||||
}
|
||||
|
||||
enhanced_batch = []
|
||||
|
||||
pil_images = batch_tensor_to_pil(image)
|
||||
pbar = progress_bar(len(pil_images))
|
||||
|
||||
for i in range(len(image)):
|
||||
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
break
|
||||
|
||||
img_np = np.ascontiguousarray(image[i].cpu().numpy().astype(np.float32))
|
||||
dest_np = np.ascontiguousarray(np.zeros_like(img_np))
|
||||
|
||||
mask_np = None
|
||||
if mask is not None:
|
||||
mask_np = np.ascontiguousarray(mask[i].cpu().numpy().astype(np.float32))
|
||||
|
||||
# Вызываем Host-обработчик (без CUDA конфликтов)
|
||||
self.manager.process_host(
|
||||
source=img_np,
|
||||
destination=dest_np,
|
||||
settings=settings,
|
||||
reset=True,
|
||||
mask=mask_np
|
||||
)
|
||||
|
||||
out_tensor = torch.from_numpy(dest_np).to(self.device)
|
||||
enhanced_batch.append(out_tensor)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (torch.stack(enhanced_batch),)
|
||||
@@ -6,7 +6,7 @@ import torch.nn.functional as F
|
||||
from PIL import Image
|
||||
from torchvision.models._utils import IntermediateLayerGetter as IntermediateLayerGetter
|
||||
|
||||
from modules import shared
|
||||
from r_modules import shared
|
||||
|
||||
from r_facelib.detection.align_trans import get_reference_facial_points, warp_and_crop_face
|
||||
from r_facelib.detection.retinaface.retinaface_net import FPN, SSH, MobileNetV1, make_bbox_head, make_class_head, make_landmark_head
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
class StableDiffusionProcessing:
|
||||
class Processing:
|
||||
|
||||
def __init__(self, init_imgs):
|
||||
self.init_images = init_imgs
|
||||
self.width = init_imgs[0].width
|
||||
self.height = init_imgs[0].height
|
||||
self.extra_generation_params = {}
|
||||
self.bbox = []
|
||||
self.swapped_indexes = []
|
||||
|
||||
|
||||
class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
|
||||
class ProcessingImg2Img(Processing):
|
||||
|
||||
def __init__(self, init_img):
|
||||
super().__init__(init_img)
|
||||
@@ -0,0 +1,307 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
import folder_paths
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageEnhance, ImageCms
|
||||
import comfy.model_management as model_management
|
||||
from scripts.reactor_logger import logger
|
||||
from r_modules.shared import state
|
||||
from reactor_utils import (
|
||||
progress_bar,
|
||||
progress_bar_reset
|
||||
)
|
||||
try:
|
||||
from colour.io.luts.iridas_cube import read_LUT_IridasCube
|
||||
AVAILABLE = True
|
||||
except Exception as e:
|
||||
AVAILABLE = False
|
||||
logger.error(f"{e}, ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
|
||||
|
||||
folder_paths.add_model_folder_path("luts", os.path.join(folder_paths.models_dir, "luts"))
|
||||
|
||||
# Профили для HDR (создаются один раз при импорте для экономии ресурсов)
|
||||
sRGB_profile = ImageCms.createProfile('sRGB')
|
||||
Lab_profile = ImageCms.createProfile('LAB')
|
||||
|
||||
def get_luts():
|
||||
luts = folder_paths.get_filename_list("luts")
|
||||
luts = [x for x in luts if x.lower().endswith(".cube") or x.lower().endswith(".spi1d") or x.lower().endswith(".spi3d") or x.lower().endswith(".spimtx") or x.lower().endswith(".csp")]
|
||||
luts.sort(key=str.lower)
|
||||
luts.insert(0, "none")
|
||||
return luts
|
||||
|
||||
class ReActorCinematicEnhancer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", ),
|
||||
# --- Global Blend ---
|
||||
"intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.5"}),
|
||||
# --- LUT ---
|
||||
"lut_file": (get_luts(),),
|
||||
"lut_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1, "tooltip": "0..1, def: 0.5"}),
|
||||
"lut_gamma_correction": ("BOOLEAN", { "default": True }),
|
||||
# --- Film Effect ---
|
||||
"film_grain_method": (["advanced","fast"], {"default": "advanced"}),
|
||||
"film_grain_power": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
"film_grain_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.1, "tooltip": "0.1..1, def: 0.1"}),
|
||||
"film_grain_sat": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.15"}),
|
||||
"film_grain_shadows": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
|
||||
"film_grain_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01, "tooltip": "(advanced only) 0..1, def: 0.2"}),
|
||||
"film_blur_strength": ("INT", {"default": 1, "min": 0, "max": 256, "step": 1, "tooltip": "(by depth_map) 0..256, def: 1"}),
|
||||
"film_focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01, "tooltip": "(by depth_map) 0..1, def: 0.9"}),
|
||||
"film_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1, "tooltip": "(by depth_map) 0..8, def: 2.2"}),
|
||||
"film_vignette_intensity": ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
# --- Color Correct ---
|
||||
"cc_temperature": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_hue": ("FLOAT", {"default": 0, "min": -90, "max": 90, "step": 5, "tooltip": "-90..90, def: 0"}),
|
||||
"cc_brightness": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_contrast": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_saturation": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 5, "tooltip": "-100..100, def: 0"}),
|
||||
"cc_gamma": ("FLOAT", {"default": 1, "min": 0.2, "max": 2.2, "step": 0.1, "tooltip": "0.2..2.2, def: 1.0"}),
|
||||
# --- HDR Effects ---
|
||||
"hdr_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "0..5, def: 0.5"}),
|
||||
"hdr_shadow_intensity": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
|
||||
"hdr_highlight_intensity": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.75"}),
|
||||
"hdr_contrast": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.1"}),
|
||||
"hdr_enhance_color": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0..1, def: 0.25"}),
|
||||
},
|
||||
"optional": {
|
||||
"depth_map": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🌌 ReActor/PostProcessing"
|
||||
DESCRIPTION = (
|
||||
"Requirements:\n"
|
||||
"- pip install colour-science\n"
|
||||
"- put LUT-files in models/luts\n"
|
||||
"depth_map is optional"
|
||||
)
|
||||
|
||||
def execute(self, image, intensity, lut_file, lut_strength, lut_gamma_correction,
|
||||
film_grain_method, film_grain_power, film_grain_scale, film_grain_sat, film_grain_shadows, film_grain_highs,film_blur_strength, film_focal_depth, film_focus_spread, film_vignette_intensity,
|
||||
cc_temperature, cc_hue, cc_brightness, cc_contrast, cc_saturation, cc_gamma,
|
||||
hdr_intensity, hdr_shadow_intensity, hdr_highlight_intensity, hdr_contrast, hdr_enhance_color,
|
||||
depth_map=None):
|
||||
|
||||
if not AVAILABLE:
|
||||
logger.error("ReActorCinematicEnhancer is not available, please install `colour` library: `pip install colour-science`")
|
||||
return (image,)
|
||||
|
||||
# 1. Загрузка LUT
|
||||
|
||||
if lut_file != "none":
|
||||
lut_file_path = folder_paths.get_full_path("luts", lut_file)
|
||||
lut = read_LUT_IridasCube(lut_file_path) if lut_file_path and Path(lut_file_path).exists() else None
|
||||
if lut:
|
||||
lut.name = lut_file
|
||||
logger.status(f"LUT {lut_file} selected")
|
||||
else:
|
||||
lut = None
|
||||
|
||||
batch_size = image.shape[0]
|
||||
result_batch = []
|
||||
|
||||
pbar = progress_bar(batch_size)
|
||||
|
||||
logger.status(f"Enhancing...")
|
||||
|
||||
for b in range(batch_size):
|
||||
|
||||
if state.interrupted or model_management.processing_interrupted():
|
||||
logger.status("Interrupted by User")
|
||||
break
|
||||
|
||||
# --- БАЗА: NumPy (0-1) ---
|
||||
orig_np = image[b].cpu().numpy().copy()
|
||||
img_np = orig_np.copy()
|
||||
|
||||
# --- 1. LUT ---
|
||||
if lut:
|
||||
lut_img = img_np.copy()
|
||||
if lut_gamma_correction:
|
||||
lut_img = np.clip(lut_img ** (1/2.2), 0, 1)
|
||||
lut_img = lut.apply(lut_img)
|
||||
if lut_gamma_correction:
|
||||
lut_img = np.clip(lut_img ** 2.2, 0, 1)
|
||||
|
||||
if lut_strength < 1.0:
|
||||
img_np = lut_strength * lut_img + (1 - lut_strength) * img_np
|
||||
else:
|
||||
img_np = lut_img
|
||||
|
||||
# Подготовка маски глубины (если есть)
|
||||
depth_np = None
|
||||
if depth_map is not None:
|
||||
d_tensor = depth_map[b if depth_map.shape[0] > 1 else 0].cpu().numpy()
|
||||
if d_tensor.shape[:2] != img_np.shape[:2]:
|
||||
d_tensor = cv2.resize(d_tensor, (img_np.shape[1], img_np.shape[0]), interpolation=cv2.INTER_LINEAR)
|
||||
# Если глубина RGB, берем один канал
|
||||
depth_np = d_tensor[:,:,0] if d_tensor.ndim == 3 else d_tensor
|
||||
|
||||
# --- 2. FILM EFFECT ---
|
||||
# Виньетка (OpenCV)
|
||||
if film_vignette_intensity > 0:
|
||||
h, w = img_np.shape[:2]
|
||||
Y, X = np.ogrid[:h, :w]
|
||||
center_y, center_x = h / 2, w / 2
|
||||
dist_from_center = np.sqrt((X - center_x)**2 + (Y - center_y)**2)
|
||||
max_dist = np.sqrt(center_x**2 + center_y**2)
|
||||
vignette = 1 - (dist_from_center / max_dist) * film_vignette_intensity
|
||||
img_np = np.clip(img_np * vignette[..., np.newaxis], 0, 1)
|
||||
|
||||
# Глубинное размытие (пошаговая реализация)
|
||||
if film_blur_strength > 0 and depth_np is not None:
|
||||
# Нормализуем разницу глубины относительно точки фокуса
|
||||
depth_diff = np.abs(depth_np - film_focal_depth) * film_focus_spread
|
||||
depth_diff = np.clip(depth_diff, 0, 1)
|
||||
|
||||
# Пошаговый блендинг (5 шагов для плавности)
|
||||
steps = 5
|
||||
blurred_fg = np.zeros_like(img_np)
|
||||
for step in range(steps + 1):
|
||||
step_blur = int((film_blur_strength * step / steps) // 2 * 2 + 1)
|
||||
if step_blur > 1:
|
||||
blur_layer = cv2.GaussianBlur(img_np, (step_blur, step_blur), 0)
|
||||
else:
|
||||
blur_layer = img_np
|
||||
|
||||
# Маска весов для текущего шага
|
||||
mask = np.clip(1.0 - np.abs(depth_diff * steps - step), 0, 1)
|
||||
blurred_fg += blur_layer * mask[..., np.newaxis]
|
||||
img_np = blurred_fg
|
||||
|
||||
# --- 3. COLOR CORRECT ---
|
||||
fg_img = Image.fromarray((img_np * 255).astype(np.uint8))
|
||||
|
||||
# Яркость и Контраст (PIL)
|
||||
if cc_brightness != 0:
|
||||
fg_img = ImageEnhance.Brightness(fg_img).enhance(1 + (cc_brightness / 100))
|
||||
if cc_contrast != 0:
|
||||
fg_img = ImageEnhance.Contrast(fg_img).enhance(1 + (cc_contrast / 100))
|
||||
|
||||
fg_cv = np.array(fg_img).astype(np.float32) / 255.0
|
||||
|
||||
# Температура
|
||||
temp_val = cc_temperature / 100
|
||||
if temp_val > 0:
|
||||
fg_cv[:, :, 0] = np.clip(fg_cv[:, :, 0] * (1 + temp_val), 0, 1)
|
||||
fg_cv[:, :, 1] = np.clip(fg_cv[:, :, 1] * (1 + temp_val * 0.4), 0, 1)
|
||||
elif temp_val < 0:
|
||||
fg_cv[:, :, 2] = np.clip(fg_cv[:, :, 2] * (1 - temp_val), 0, 1)
|
||||
|
||||
# Гамма
|
||||
if cc_gamma != 1.0:
|
||||
fg_cv = np.clip(np.power(fg_cv, cc_gamma), 0, 1)
|
||||
|
||||
# HSV для Hue и Saturation
|
||||
if cc_hue != 0 or cc_saturation != 0:
|
||||
hsv = cv2.cvtColor((fg_cv * 255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
|
||||
if cc_hue != 0:
|
||||
hsv[:, :, 0] = (hsv[:, :, 0] + (cc_hue / 2)) % 180 # В OpenCV Hue до 180
|
||||
if cc_saturation != 0:
|
||||
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * (1 + cc_saturation / 100), 0, 255)
|
||||
fg_cv = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB).astype(np.float32) / 255.0
|
||||
|
||||
img_np = fg_cv
|
||||
|
||||
# --- 4. FILM GRAIN ---
|
||||
if film_grain_power > 0:
|
||||
|
||||
# --- 4.1 ADVANCED NATIVE GRAIN ---
|
||||
if film_grain_method == "advanced":
|
||||
# 1) Генерируем базовый цветной шум
|
||||
noise_rgb = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
|
||||
|
||||
# 2) Генерируем монохромный шум (усреднение по каналам)
|
||||
noise_mono = np.mean(noise_rgb, axis=-1, keepdims=True)
|
||||
|
||||
# 3) Смешиваем цветность зерна (grain_sat)
|
||||
noise = noise_rgb * film_grain_sat + noise_mono * (1.0 - film_grain_sat)
|
||||
|
||||
# 4) Масштабирование зерна
|
||||
if film_grain_scale != 1.0:
|
||||
h, w = img_np.shape[:2]
|
||||
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
|
||||
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# 5) Кинематографичная маскировка по яркости (Luminance)
|
||||
# Вычисляем воспринимаемую яркость пикселей
|
||||
lum = 0.299 * img_np[:,:,0] + 0.587 * img_np[:,:,1] + 0.114 * img_np[:,:,2]
|
||||
lum = lum[..., np.newaxis]
|
||||
|
||||
# Создаем карту весов шума:
|
||||
# Средние тона (0.5) получают вес 1.0.
|
||||
# Тени (0.0) получают вес film_grain_shadows.
|
||||
# Блики (1.0) получают вес film_grain_highs.
|
||||
grain_mask = np.where(
|
||||
lum < 0.5,
|
||||
film_grain_shadows + (1.0 - film_grain_shadows) * (lum * 2.0), # Плавный переход от теней к средним
|
||||
1.0 - (1.0 - film_grain_highs) * ((lum - 0.5) * 2.0) # Плавный переход от средних к бликам
|
||||
)
|
||||
|
||||
# Накладываем шум с учетом яркостной маски
|
||||
img_np = np.clip(img_np + noise * grain_mask, 0, 1)
|
||||
|
||||
# --- 4.2 FAST GRAIN ---
|
||||
else:
|
||||
noise = np.random.normal(0, film_grain_power, img_np.shape).astype(np.float32)
|
||||
# Масштабирование шума
|
||||
if film_grain_scale != 1.0:
|
||||
h, w = img_np.shape[:2]
|
||||
small_noise = cv2.resize(noise, (int(w / film_grain_scale), int(h / film_grain_scale)))
|
||||
noise = cv2.resize(small_noise, (w, h), interpolation=cv2.INTER_LINEAR)
|
||||
img_np = np.clip(img_np + noise * 0.5, 0, 1)
|
||||
|
||||
# --- 5. HDR EFFECTS ---
|
||||
if hdr_intensity > 0:
|
||||
final_pil = Image.fromarray((img_np * 255).astype(np.uint8))
|
||||
|
||||
# Основная математика тонирования LAB
|
||||
img_lab = ImageCms.profileToProfile(final_pil, sRGB_profile, Lab_profile, outputMode='LAB')
|
||||
luminance, a, b = img_lab.split()
|
||||
base = np.array(luminance, dtype=np.float32)
|
||||
|
||||
scaled_shadow = (hdr_shadow_intensity ** 2) * hdr_intensity
|
||||
scaled_highlight = (hdr_highlight_intensity ** 2) * hdr_intensity
|
||||
|
||||
shadow_mask = np.clip((1 - (base / 255)) ** 2, 0, 1)
|
||||
highlight_mask = np.clip((base / 255) ** 2, 0, 1)
|
||||
|
||||
adj_shadows = np.clip(base * (1 - shadow_mask * scaled_shadow), 0, 255)
|
||||
adj_highlights = np.clip(base + (255 - base) * highlight_mask * scaled_highlight, 0, 255)
|
||||
|
||||
adj_lum = np.clip(adj_shadows + adj_highlights - base, 0, 255)
|
||||
final_lum = np.clip(base * (1 - hdr_intensity) + adj_lum * hdr_intensity, 0, 255).astype(np.uint8)
|
||||
|
||||
merged_lab = Image.merge('LAB', (Image.fromarray(final_lum), a, b))
|
||||
final_pil = ImageCms.profileToProfile(merged_lab, Lab_profile, sRGB_profile, outputMode='RGB')
|
||||
|
||||
if hdr_contrast > 0:
|
||||
final_pil = ImageEnhance.Contrast(final_pil).enhance(1 + hdr_contrast)
|
||||
if hdr_enhance_color > 0:
|
||||
final_pil = ImageEnhance.Color(final_pil).enhance(1 + hdr_enhance_color * 0.2)
|
||||
|
||||
# Возвращаем HDR в рабочий массив для финального смешивания
|
||||
img_np = np.array(final_pil).astype(np.float32) / 255.0
|
||||
|
||||
# --- 6. GLOBAL INTENSITY BLEND ---
|
||||
if intensity < 1.0:
|
||||
img_np = orig_np * (1.0 - intensity) + img_np * intensity
|
||||
|
||||
# Возврат в тензор
|
||||
out_tensor = torch.from_numpy(img_np)
|
||||
result_batch.append(out_tensor)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
return (torch.stack(result_batch),)
|
||||
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import zipfile
|
||||
from reactor_utils import download # Твоя функция скачивания из utils
|
||||
from .face_objects import Face
|
||||
from .inswap import SCRFD, ArcFaceONNX, Attribute, Landmark
|
||||
from scripts.reactor_logger import logger
|
||||
|
||||
class ReActorFaceAnalysis:
|
||||
"""
|
||||
Главный класс-оркестратор.
|
||||
Берет картинку, находит лица, определяет пол/возраст и вычисляет эмбеддинги.
|
||||
"""
|
||||
def __init__(self, name="buffalo_l", root="./models/insightface", providers=None):
|
||||
self.name = name
|
||||
self.root = root
|
||||
self.providers = providers or ["CPUExecutionProvider"]
|
||||
self.models = {}
|
||||
|
||||
model_dir = os.path.join(root, "models", name)
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
|
||||
det_file = os.path.join(model_dir, "det_10g.onnx")
|
||||
rec_file = os.path.join(model_dir, "w600k_r50.onnx")
|
||||
attr_file = os.path.join(model_dir, "genderage.onnx")
|
||||
lmk2d_file = os.path.join(model_dir, "2d106det.onnx")
|
||||
lmk3d_file = os.path.join(model_dir, "1k3d68.onnx")
|
||||
|
||||
# Если файлов нет - качаем архив
|
||||
if not (os.path.exists(det_file) and os.path.exists(rec_file) and os.path.exists(attr_file)):
|
||||
zip_url = "https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/buffalo_l.zip"
|
||||
zip_path = os.path.join(model_dir, f"{name}.zip")
|
||||
|
||||
logger.status(f"Downloading {name} models archive...")
|
||||
download(zip_url, zip_path, f"{name}.zip")
|
||||
|
||||
logger.status(f"Extracting {name} models...")
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(model_dir)
|
||||
logger.status("Extraction completed!")
|
||||
except zipfile.BadZipFile:
|
||||
logger.error("Downloaded zip file is corrupted. Please try again.")
|
||||
finally:
|
||||
# В любом случае пытаемся удалить архив, чтобы не занимать место
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
|
||||
# Инициализируем только те модели, которые физически есть в папке
|
||||
if os.path.exists(det_file):
|
||||
self.models["detection"] = SCRFD(det_file, providers=self.providers)
|
||||
if os.path.exists(rec_file):
|
||||
self.models["recognition"] = ArcFaceONNX(rec_file, providers=self.providers)
|
||||
if os.path.exists(attr_file):
|
||||
self.models["attribute"] = Attribute(attr_file, providers=self.providers)
|
||||
if os.path.exists(lmk2d_file):
|
||||
self.models["landmark_2d"] = Landmark(lmk2d_file, providers=self.providers)
|
||||
if os.path.exists(lmk3d_file):
|
||||
self.models["landmark_3d"] = Landmark(lmk3d_file, providers=self.providers)
|
||||
|
||||
if "detection" not in self.models:
|
||||
raise FileNotFoundError(
|
||||
f"Detection model (det_10g.onnx) not found at {det_file}. "
|
||||
"Please ensure the buffalo_l models are downloaded and extracted properly."
|
||||
)
|
||||
|
||||
def prepare(self, ctx_id=0, det_size=(640, 640), det_thresh=0.5):
|
||||
self.det_size = det_size
|
||||
self.det_thresh = det_thresh
|
||||
|
||||
def get(self, img, max_num=0):
|
||||
bboxes, kpss = self.models["detection"].detect(
|
||||
img,
|
||||
det_thresh=self.det_thresh,
|
||||
input_size=self.det_size,
|
||||
max_num=max_num
|
||||
)
|
||||
|
||||
if bboxes.shape[0] == 0:
|
||||
return []
|
||||
|
||||
ret = []
|
||||
for i in range(bboxes.shape[0]):
|
||||
bbox = bboxes[i, 0:4]
|
||||
det_score = bboxes[i, 4]
|
||||
kps = kpss[i] if kpss is not None else None
|
||||
|
||||
face = Face(bbox=bbox, kps=kps, det_score=det_score)
|
||||
|
||||
if "attribute" in self.models:
|
||||
self.models["attribute"].get(img, face)
|
||||
|
||||
if "recognition" in self.models:
|
||||
self.models["recognition"].get(img, face)
|
||||
|
||||
if "landmark_2d" in self.models:
|
||||
self.models["landmark_2d"].get(img, face)
|
||||
|
||||
if "landmark_3d" in self.models:
|
||||
self.models["landmark_3d"].get(img, face)
|
||||
|
||||
ret.append(face)
|
||||
|
||||
return ret
|
||||
@@ -0,0 +1,82 @@
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import os
|
||||
|
||||
class Face(dict):
|
||||
"""
|
||||
Класс-хранилище данных о лице.
|
||||
Наследуется от dict для полной обратной совместимости с кодом,
|
||||
который ожидает доступ по ключу (например, face['bbox']).
|
||||
"""
|
||||
def __init__(self, d=None, **kwargs):
|
||||
if d is None:
|
||||
d = {}
|
||||
if kwargs:
|
||||
d.update(**kwargs)
|
||||
for k, v in d.items():
|
||||
setattr(self, k, v)
|
||||
# Инициализируем родительский словарь
|
||||
super().__init__(d)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# Если это массив, делаем копию, чтобы избежать багов с мутацией по ссылке
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = [x for x in value]
|
||||
elif isinstance(value, np.ndarray):
|
||||
value = value.copy()
|
||||
|
||||
super().__setattr__(name, value)
|
||||
super().__setitem__(name, value)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, value)
|
||||
super().__setattr__(key, value)
|
||||
|
||||
@property
|
||||
def sex(self):
|
||||
"""Возвращает 'M' или 'F' на основе числового значения gender"""
|
||||
gender = self.get('gender', None)
|
||||
if gender is None:
|
||||
return None
|
||||
return 'M' if gender == 1 else 'F'
|
||||
|
||||
@property
|
||||
def normed_embedding(self):
|
||||
"""Автоматически нормализует эмбеддинг для ArcFace / INSwapper"""
|
||||
embedding = self.get('embedding', None)
|
||||
if embedding is None:
|
||||
return None
|
||||
norm = np.linalg.norm(embedding)
|
||||
if norm == 0:
|
||||
return embedding
|
||||
return embedding / norm
|
||||
|
||||
|
||||
class BaseONNXModel:
|
||||
"""
|
||||
Базовый класс для всех моделей (Детектор, ArcFace, INSwapper).
|
||||
Берет на себя рутину по открытию сессий и чтению входов/выходов.
|
||||
"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
self.model_file = model_file
|
||||
self.providers = providers or ["CPUExecutionProvider"]
|
||||
|
||||
if not os.path.exists(self.model_file):
|
||||
raise FileNotFoundError(f"Model file not found: {self.model_file}")
|
||||
|
||||
self.session = ort.InferenceSession(self.model_file, providers=self.providers)
|
||||
|
||||
# Получаем параметры входов
|
||||
self.inputs = self.session.get_inputs()
|
||||
self.input_names = [inp.name for inp in self.inputs]
|
||||
|
||||
# Обычно нас интересует шейп первого входа (например, батч, каналы, высота, ширина)
|
||||
self.input_shape = self.inputs[0].shape
|
||||
|
||||
# Получаем параметры выходов
|
||||
self.outputs = self.session.get_outputs()
|
||||
self.output_names = [out.name for out in self.outputs]
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
"""Этот метод будет переопределен в классах-наследниках"""
|
||||
raise NotImplementedError("Forward method must be implemented by subclasses.")
|
||||
@@ -0,0 +1,181 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from .face_objects import BaseONNXModel
|
||||
|
||||
class HyperSwapper(BaseONNXModel):
|
||||
"""Класс для работы с моделями семейства Hyperswap"""
|
||||
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
|
||||
# Функция для получения 5 ключевых точек из объекта Face
|
||||
def get_landmarks_5(self, face):
|
||||
if hasattr(face, 'landmark_5') and face.landmark_5 is not None:
|
||||
return face.landmark_5
|
||||
elif hasattr(face, 'kps') and face.kps is not None:
|
||||
return face.kps
|
||||
elif hasattr(face, 'landmark') and face.landmark is not None:
|
||||
if face.landmark.shape[0] >= 68:
|
||||
idxs = [36, 45, 30, 48, 54]
|
||||
return face.landmark[idxs]
|
||||
return None
|
||||
|
||||
# Функция для вычисления аффинного преобразования
|
||||
def get_affine_transform(self, src_pts, dst_pts):
|
||||
M, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
|
||||
return M
|
||||
|
||||
# Создаём градиентную маску овальной формы без обрезки
|
||||
def create_gradient_mask(self, crop_size=256):
|
||||
# 1. Создаём пустую маску (все пиксели = 0)
|
||||
mask = np.zeros((crop_size, crop_size), dtype=np.float32)
|
||||
|
||||
# 2. Определяем центр и размеры эллипса
|
||||
center = (crop_size // 2, crop_size // 2)
|
||||
axes = (int(crop_size * 0.35), int(crop_size * 0.4))
|
||||
|
||||
# 3. Рисуем эллипс (заполняем белым цветом, значение=1.0)
|
||||
cv2.ellipse(
|
||||
mask, # Массив для рисования
|
||||
center, # Центр эллипса
|
||||
axes, # Полуоси (ширина, высота)
|
||||
angle=0, # Угол поворота
|
||||
startAngle=0, # Начальный угол дуги
|
||||
endAngle=360, # Конечный угол дуги (360 = полный эллипс)
|
||||
color=1.0, # Значение для заполнения (белый = 1.0)
|
||||
thickness=-1 # -1 = заполнить всю область эллипса
|
||||
)
|
||||
|
||||
# 4. Применяем размытие для плавных краёв
|
||||
blur_ksize = 15 # Нечётное число, чтобы ядро было симметричным
|
||||
mask = cv2.GaussianBlur(mask, (blur_ksize, blur_ksize), 0)
|
||||
|
||||
# 5. Ограничим значения в диапазоне [0, 1]
|
||||
mask = np.clip(mask, 0, 1)
|
||||
|
||||
return mask
|
||||
|
||||
def paste_back(self, target_img, swapped_face, M, crop_size=256):
|
||||
|
||||
# 1. Создание мягкой маски (Эрозия + Размытие)
|
||||
mask = self.create_gradient_mask(crop_size)
|
||||
|
||||
# Преобразуем в трехканальную маску
|
||||
mask_3c = np.stack([mask] * 3, axis=2)
|
||||
|
||||
# 2. Получаем размеры целевого изображения
|
||||
h, w = target_img.shape[:2]
|
||||
|
||||
# 3. Нормализация swapped_face к float32 [0,1] для warp
|
||||
swapped_face_norm = swapped_face.astype(np.float32) / 255.0
|
||||
mask_norm = mask_3c.astype(np.float32) # Маска уже [0,1]
|
||||
|
||||
# 4. Обратное преобразование (WARP_INVERSE_MAP) для лица И маски
|
||||
# Используем BORDER_CONSTANT с borderValue=0.5 (серый, чтобы избежать синих/зеленых артефактов)
|
||||
warped_face = cv2.warpAffine(
|
||||
swapped_face_norm,
|
||||
M,
|
||||
(w, h),
|
||||
flags=cv2.INTER_LANCZOS4 | cv2.WARP_INVERSE_MAP,
|
||||
borderMode=cv2.BORDER_CONSTANT,
|
||||
borderValue=0.5
|
||||
)
|
||||
|
||||
# Для маски (INTER_CUBIC — плавные границы)
|
||||
warped_mask = cv2.warpAffine(
|
||||
mask_norm,
|
||||
M,
|
||||
(w, h),
|
||||
flags=cv2.INTER_CUBIC | cv2.WARP_INVERSE_MAP,
|
||||
borderMode=cv2.BORDER_CONSTANT,
|
||||
borderValue=0.0 # Маска: 0 за пределами
|
||||
)
|
||||
|
||||
# 5. Обработка после warp: Clip, NaN fix
|
||||
warped_face = np.clip(warped_face, 0, 1) # Убираем отрицательные
|
||||
warped_face = np.nan_to_num(warped_face, nan=0.5) # NaN -> серый
|
||||
|
||||
warped_mask = np.clip(warped_mask, 0, 1)
|
||||
warped_mask = np.nan_to_num(warped_mask, nan=0.0)
|
||||
|
||||
# 6. Дополнительное размытие для устранения артефактов
|
||||
warped_mask = cv2.GaussianBlur(warped_mask, (3, 3), 0)
|
||||
|
||||
# 7. Плавное наложение в float32
|
||||
target_float = target_img.astype(np.float32) / 255.0
|
||||
result_float = target_float * (1.0 - warped_mask) + warped_face * warped_mask
|
||||
|
||||
# 8. Обратная нормализация к uint8
|
||||
result = (result_float * 255).clip(0, 255).astype(np.uint8)
|
||||
|
||||
return result
|
||||
|
||||
def visualize_points(self, img, points, color=(0, 255, 0)):
|
||||
img = img.copy()
|
||||
for p in points:
|
||||
cv2.circle(img, tuple(p.astype(int)), 3, color, -1)
|
||||
|
||||
# Итоговая функция run_hyperswap (get) с аффинным преобразованием
|
||||
def get(self, img, target_face, source_face, paste_back=True):
|
||||
# 1. Подготовка эмбеддинга
|
||||
source_embedding = source_face.normed_embedding.reshape(1, -1).astype(np.float32)
|
||||
|
||||
# 2. Получаем 5 точек target
|
||||
target_landmarks_5 = self.get_landmarks_5(target_face)
|
||||
# self.visualize_points(img, target_landmarks_5, (0, 255, 0)) # не для продакшена
|
||||
|
||||
if target_landmarks_5 is None:
|
||||
return img if paste_back else (None, None)
|
||||
|
||||
# 3. Определение эталонных точек для выравнивания 256x256 (FFHQ Alignment)
|
||||
std_landmarks_256 = np.array([
|
||||
[ 84.87, 105.94], # Левый глаз
|
||||
[171.13, 105.94], # Правый глаз
|
||||
[128.00, 146.66], # Кончик носа
|
||||
[ 96.95, 188.64], # Левый уголок рта
|
||||
[159.05, 188.64] # Правый уголок рта
|
||||
], dtype=np.float32)
|
||||
|
||||
# Вычисляем аффинную матрицу
|
||||
M = self.get_affine_transform(target_landmarks_5.astype(np.float32), std_landmarks_256)
|
||||
|
||||
# Применяем аффинное преобразование с новой матрицей M
|
||||
crop = cv2.warpAffine(img, M, (256, 256), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
|
||||
|
||||
# 4. Преобразуем crop для модели
|
||||
crop_input = crop[:, :, ::-1].astype(np.float32) / 255.0 # RGB -> [0,1]
|
||||
crop_input = (crop_input - 0.5) / 0.5 # Нормализация
|
||||
crop_input = crop_input.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32)
|
||||
|
||||
# 5. Инференс
|
||||
try:
|
||||
output = self.session.run(None, {'source': source_embedding, 'target': crop_input})[0][0]
|
||||
except:
|
||||
return img if paste_back else (None, None)
|
||||
|
||||
if isinstance(output, np.ndarray):
|
||||
# устранение NaN и бесконечностей
|
||||
output = np.nan_to_num(output, nan=0.0, posinf=255.0, neginf=0.0)
|
||||
|
||||
# если диапазон похож на [-1,1] → нормализуем в [0,255]
|
||||
if output.min() < 0.0 or output.max() <= 1.5:
|
||||
output = ((output + 1.0) / 2.0 * 255.0)
|
||||
# жёсткое ограничение диапазона и тип для OpenCV
|
||||
output = np.clip(output, 0, 255).astype(np.uint8).copy()
|
||||
|
||||
# защита от повторного использования буфера (inplace CPU bug)
|
||||
try:
|
||||
output.setflags(write=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 6. Обратная нормализация
|
||||
output = output.transpose(1, 2, 0) # CHW -> HWC
|
||||
output = output[:, :, ::-1] # BGR -> RGB
|
||||
|
||||
# 7. Возвращаем результат в зависимости от флага paste_back
|
||||
if not paste_back:
|
||||
return output, M # Возвращаем только кроп лица (256x256) и матрицу M
|
||||
|
||||
# Если нужна полная вклейка в исходное изображение:
|
||||
return self.paste_back(img, output, M, crop_size=256)
|
||||
@@ -0,0 +1,440 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from .face_objects import BaseONNXModel
|
||||
from .meanshape_68 import MEANSHAPE_68
|
||||
|
||||
# --- Математика для 3D позы ---
|
||||
|
||||
def estimate_affine_matrix_3d23d(X, Y):
|
||||
''' Вычисляет аффинную матрицу трансформации 3D -> 3D '''
|
||||
X_homo = np.hstack((X, np.ones([X.shape[0], 1])))
|
||||
P = np.linalg.lstsq(X_homo, Y, rcond=None)[0].T
|
||||
return P
|
||||
|
||||
def P2sRt(P):
|
||||
''' Разбивает матрицу проекции '''
|
||||
t1 = np.linalg.norm(P[:,0])
|
||||
t2 = np.linalg.norm(P[:,1])
|
||||
t3 = np.linalg.norm(P[:,2])
|
||||
s = (t1 + t2 + t3) / 3.0
|
||||
P1 = P / s
|
||||
R = P1[:, 0:3]
|
||||
t = P1[:, 3]
|
||||
return s, R, t
|
||||
|
||||
def matrix2angle(R):
|
||||
''' Превращает матрицу поворота в углы Эйлера (pitch, yaw, roll) '''
|
||||
if R[2,0] != 1 and R[2,0] != -1:
|
||||
pitch = -np.arcsin(R[2,0])
|
||||
yaw = np.arctan2(R[2,1]/np.cos(pitch), R[2,2]/np.cos(pitch))
|
||||
roll = np.arctan2(R[1,0]/np.cos(pitch), R[0,0]/np.cos(pitch))
|
||||
else:
|
||||
yaw = 0
|
||||
if R[2,0] == -1:
|
||||
pitch = np.pi/2
|
||||
roll = yaw + np.arctan2(R[0,1], R[0,2])
|
||||
else:
|
||||
pitch = -np.pi/2
|
||||
roll = -yaw + np.arctan2(-R[0,1], -R[0,2])
|
||||
return pitch, yaw, roll
|
||||
|
||||
# --- Вспомогательные функции ---
|
||||
|
||||
def distance2bbox(points, distance, max_shape=None):
|
||||
x1 = points[:, 0] - distance[:, 0]
|
||||
y1 = points[:, 1] - distance[:, 1]
|
||||
x2 = points[:, 0] + distance[:, 2]
|
||||
y2 = points[:, 1] + distance[:, 3]
|
||||
if max_shape is not None:
|
||||
x1 = np.clip(x1, 0, max_shape[1])
|
||||
y1 = np.clip(y1, 0, max_shape[0])
|
||||
x2 = np.clip(x2, 0, max_shape[1])
|
||||
y2 = np.clip(y2, 0, max_shape[0])
|
||||
return np.stack([x1, y1, x2, y2], axis=-1)
|
||||
|
||||
def distance2kps(points, distance, max_shape=None):
|
||||
preds = []
|
||||
for i in range(0, distance.shape[1], 2):
|
||||
px = points[:, i%2] + distance[:, i]
|
||||
py = points[:, i%2+1] + distance[:, i+1]
|
||||
if max_shape is not None:
|
||||
px = np.clip(px, 0, max_shape[1])
|
||||
py = np.clip(py, 0, max_shape[0])
|
||||
preds.append(px)
|
||||
preds.append(py)
|
||||
return np.stack(preds, axis=-1)
|
||||
|
||||
# Стандартные точки FFHQ/ArcFace для матрицы трансформации 112x112
|
||||
ARCFACE_STD_POINTS = np.array([
|
||||
[38.2946, 51.6963],
|
||||
[73.5318, 51.5014],
|
||||
[56.0252, 71.7366],
|
||||
[41.5493, 92.3655],
|
||||
[70.7299, 92.2041]
|
||||
], dtype=np.float32)
|
||||
|
||||
def norm_crop(img, landmark, image_size=112):
|
||||
"""Выравнивает и обрезает лицо (для ArcFace)"""
|
||||
M, _ = cv2.estimateAffinePartial2D(landmark, ARCFACE_STD_POINTS)
|
||||
warped = cv2.warpAffine(img, M, (image_size, image_size), borderValue=0.0)
|
||||
return warped
|
||||
|
||||
# --- Модели ---
|
||||
|
||||
class SCRFD(BaseONNXModel):
|
||||
"""Детектор лиц (находит bbox и 5 ключевых точек)"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
self.batched = len(self.outputs[0].shape) == 3
|
||||
self.input_mean = 127.5
|
||||
self.input_std = 128.0
|
||||
self.use_kps = len(self.outputs) in [9, 15]
|
||||
self.fmc = 5 if len(self.outputs) in [10, 15] else 3
|
||||
self._feat_stride_fpn = [8, 16, 32, 64, 128][:self.fmc]
|
||||
self._num_anchors = 2 if self.fmc == 3 else 1
|
||||
self.center_cache = {}
|
||||
|
||||
def forward(self, img, threshold):
|
||||
scores_list, bboxes_list, kpss_list = [], [], []
|
||||
input_size = tuple(img.shape[0:2][::-1])
|
||||
blob = cv2.dnn.blobFromImage(img, 1.0/self.input_std, input_size,
|
||||
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
|
||||
|
||||
net_outs = self.session.run(self.output_names, {self.input_names[0]: blob})
|
||||
input_height, input_width = blob.shape[2], blob.shape[3]
|
||||
|
||||
for idx, stride in enumerate(self._feat_stride_fpn):
|
||||
if self.batched:
|
||||
scores = net_outs[idx][0]
|
||||
bbox_preds = net_outs[idx + self.fmc][0] * stride
|
||||
if self.use_kps:
|
||||
kps_preds = net_outs[idx + self.fmc * 2][0] * stride
|
||||
else:
|
||||
scores = net_outs[idx]
|
||||
bbox_preds = net_outs[idx + self.fmc] * stride
|
||||
if self.use_kps:
|
||||
kps_preds = net_outs[idx + self.fmc * 2] * stride
|
||||
|
||||
height, width = input_height // stride, input_width // stride
|
||||
key = (height, width, stride)
|
||||
|
||||
if key in self.center_cache:
|
||||
anchor_centers = self.center_cache[key]
|
||||
else:
|
||||
anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
|
||||
anchor_centers = (anchor_centers * stride).reshape((-1, 2))
|
||||
if self._num_anchors > 1:
|
||||
anchor_centers = np.stack([anchor_centers] * self._num_anchors, axis=1).reshape((-1, 2))
|
||||
if len(self.center_cache) < 100:
|
||||
self.center_cache[key] = anchor_centers
|
||||
|
||||
pos_inds = np.where(scores >= threshold)[0]
|
||||
bboxes = distance2bbox(anchor_centers, bbox_preds)
|
||||
|
||||
scores_list.append(scores[pos_inds])
|
||||
bboxes_list.append(bboxes[pos_inds])
|
||||
|
||||
if self.use_kps:
|
||||
kpss = distance2kps(anchor_centers, kps_preds)
|
||||
kpss = kpss.reshape((kpss.shape[0], -1, 2))
|
||||
kpss_list.append(kpss[pos_inds])
|
||||
|
||||
return scores_list, bboxes_list, kpss_list
|
||||
|
||||
def detect(self, img, det_thresh=0.5, input_size=(640, 640), max_num=0):
|
||||
im_ratio = float(img.shape[0]) / img.shape[1]
|
||||
model_ratio = float(input_size[1]) / input_size[0]
|
||||
|
||||
if im_ratio > model_ratio:
|
||||
new_height = input_size[1]
|
||||
new_width = int(new_height / im_ratio)
|
||||
else:
|
||||
new_width = input_size[0]
|
||||
new_height = int(new_width * im_ratio)
|
||||
|
||||
det_scale = float(new_height) / img.shape[0]
|
||||
resized_img = cv2.resize(img, (new_width, new_height))
|
||||
det_img = np.zeros((input_size[1], input_size[0], 3), dtype=np.uint8)
|
||||
det_img[:new_height, :new_width, :] = resized_img
|
||||
|
||||
scores_list, bboxes_list, kpss_list = self.forward(det_img, det_thresh)
|
||||
|
||||
scores = np.vstack(scores_list).ravel()
|
||||
order = scores.argsort()[::-1]
|
||||
bboxes = np.vstack(bboxes_list) / det_scale
|
||||
|
||||
if self.use_kps:
|
||||
kpss = np.vstack(kpss_list) / det_scale
|
||||
|
||||
pre_det = np.hstack((bboxes, scores[:, None])).astype(np.float32, copy=False)
|
||||
pre_det = pre_det[order, :]
|
||||
keep = self.nms(pre_det)
|
||||
|
||||
det = pre_det[keep, :]
|
||||
kpss = kpss[order, :, :][keep, :, :] if self.use_kps else None
|
||||
|
||||
if max_num > 0 and det.shape[0] > max_num:
|
||||
area = (det[:, 2] - det[:, 0]) * (det[:, 3] - det[:, 1])
|
||||
img_center = img.shape[0] // 2, img.shape[1] // 2
|
||||
offsets = np.vstack([
|
||||
(det[:, 0] + det[:, 2]) / 2 - img_center[1],
|
||||
(det[:, 1] + det[:, 3]) / 2 - img_center[0]
|
||||
])
|
||||
offset_dist_squared = np.sum(np.power(offsets, 2.0), 0)
|
||||
values = area - offset_dist_squared * 2.0
|
||||
bindex = np.argsort(values)[::-1][:max_num]
|
||||
det = det[bindex, :]
|
||||
if kpss is not None:
|
||||
kpss = kpss[bindex, :]
|
||||
|
||||
return det, kpss
|
||||
|
||||
def nms(self, dets, nms_thresh=0.4):
|
||||
x1, y1, x2, y2, scores = dets[:, 0], dets[:, 1], dets[:, 2], dets[:, 3], dets[:, 4]
|
||||
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
||||
order = scores.argsort()[::-1]
|
||||
keep = []
|
||||
while order.size > 0:
|
||||
i = order[0]
|
||||
keep.append(i)
|
||||
xx1 = np.maximum(x1[i], x1[order[1:]])
|
||||
yy1 = np.maximum(y1[i], y1[order[1:]])
|
||||
xx2 = np.minimum(x2[i], x2[order[1:]])
|
||||
yy2 = np.minimum(y2[i], y2[order[1:]])
|
||||
w = np.maximum(0.0, xx2 - xx1 + 1)
|
||||
h = np.maximum(0.0, yy2 - yy1 + 1)
|
||||
inter = w * h
|
||||
ovr = inter / (areas[i] + areas[order[1:]] - inter)
|
||||
inds = np.where(ovr <= nms_thresh)[0]
|
||||
order = order[inds + 1]
|
||||
return keep
|
||||
|
||||
|
||||
class ArcFaceONNX(BaseONNXModel):
|
||||
"""Распознаватель лиц (выдает вектор/эмбеддинг)"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
self.input_mean = 127.5
|
||||
self.input_std = 127.5
|
||||
self.input_size = tuple(self.input_shape[2:4][::-1])
|
||||
|
||||
def get(self, img, face):
|
||||
aimg = norm_crop(img, landmark=face.kps, image_size=self.input_size[0])
|
||||
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
|
||||
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
|
||||
net_out = self.session.run(self.output_names, {self.input_names[0]: blob})[0]
|
||||
face.embedding = net_out.flatten()
|
||||
return face.embedding
|
||||
|
||||
|
||||
class Attribute(BaseONNXModel):
|
||||
"""Анализатор атрибутов (выдает пол и возраст)"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
self.input_mean = 0.0
|
||||
self.input_std = 1.0
|
||||
self.input_size = tuple(self.input_shape[2:4][::-1])
|
||||
|
||||
def get(self, img, face):
|
||||
bbox = face.bbox
|
||||
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
|
||||
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
|
||||
_scale = self.input_size[0] / (max(w, h) * 1.5)
|
||||
|
||||
# Простая трансформация для Attribute (не требует 5 точек, только центр и масштаб)
|
||||
M = np.array([
|
||||
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
|
||||
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
|
||||
], dtype=np.float32)
|
||||
|
||||
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
|
||||
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
|
||||
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
|
||||
|
||||
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
|
||||
|
||||
# Получаем гендер и возраст
|
||||
gender = int(np.argmax(pred[:2]))
|
||||
age = int(np.round(pred[2] * 100))
|
||||
face.gender = gender
|
||||
face.age = age
|
||||
return gender, age
|
||||
|
||||
class INSwapper(BaseONNXModel):
|
||||
"""Свопер лиц (модели inswapper_128, reswapper)"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
self.input_mean = 0.0
|
||||
self.input_std = 255.0
|
||||
self.input_size = tuple(self.input_shape[2:4][::-1])
|
||||
|
||||
# Хак для экономии памяти: импортируем onnx только здесь,
|
||||
# читаем нужную матрицу emap и сразу выгружаем тяжелую модель из RAM.
|
||||
import onnx
|
||||
from onnx import numpy_helper
|
||||
model = onnx.load(self.model_file, load_external_data=False)
|
||||
self.emap = numpy_helper.to_array(model.graph.initializer[-1])
|
||||
del model
|
||||
|
||||
def get(self, img, target_face, source_face, paste_back=True):
|
||||
# 1. Идеальное позиционирование (1 в 1 как в оригинальном C++ Insightface)
|
||||
# ВАЖНО: Insightface центрирует лицо для INSwapper ТОЛЬКО по оси X!
|
||||
# По оси Y оно остается прижатым выше, сохраняя оригинальные пропорции.
|
||||
ratio = float(self.input_size[0]) / 128.0
|
||||
diff_x = 8.0 * ratio
|
||||
|
||||
src_pts = ARCFACE_STD_POINTS.copy() * ratio
|
||||
src_pts[:, 0] += diff_x # Смещаем ТОЛЬКО координаты X!
|
||||
|
||||
# 2. Вычисляем аффинную матрицу родным методом OpenCV
|
||||
M, _ = cv2.estimateAffinePartial2D(target_face.kps, src_pts)
|
||||
|
||||
# 3. Кропаем и выравниваем лицо
|
||||
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
|
||||
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
|
||||
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
|
||||
|
||||
# 4. Подготавливаем эмбеддинг донора
|
||||
latent = source_face.normed_embedding.reshape((1, -1))
|
||||
latent = np.dot(latent, self.emap)
|
||||
latent /= np.linalg.norm(latent)
|
||||
|
||||
# 5. Инференс
|
||||
pred = self.session.run(self.output_names, {
|
||||
self.input_names[0]: blob,
|
||||
self.input_names[1]: latent.astype(np.float32)
|
||||
})[0]
|
||||
|
||||
img_fake = pred.transpose((0, 2, 3, 1))[0]
|
||||
bgr_fake = np.clip(255 * img_fake, 0, 255).astype(np.uint8)[:, :, ::-1]
|
||||
|
||||
if not paste_back:
|
||||
return bgr_fake, M
|
||||
|
||||
# 6. Обратная вклейка (Paste Back)
|
||||
target_img = img
|
||||
fake_diff = bgr_fake.astype(np.float32) - aimg.astype(np.float32)
|
||||
fake_diff = np.abs(fake_diff).mean(axis=2)
|
||||
|
||||
# Обрезаем края
|
||||
fake_diff[:2, :] = 0
|
||||
fake_diff[-2:, :] = 0
|
||||
fake_diff[:, :2] = 0
|
||||
fake_diff[:, -2:] = 0
|
||||
|
||||
IM = cv2.invertAffineTransform(M)
|
||||
img_white = np.full((aimg.shape[0], aimg.shape[1]), 255, dtype=np.float32)
|
||||
|
||||
# Возвращаем в исходную перспективу
|
||||
bgr_fake_warped = cv2.warpAffine(bgr_fake, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
|
||||
img_white_warped = cv2.warpAffine(img_white, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
|
||||
fake_diff_warped = cv2.warpAffine(fake_diff, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
|
||||
|
||||
img_white_warped[img_white_warped > 20] = 255
|
||||
fthresh = 10
|
||||
fake_diff_warped[fake_diff_warped < fthresh] = 0
|
||||
fake_diff_warped[fake_diff_warped >= fthresh] = 255
|
||||
|
||||
img_mask = img_white_warped
|
||||
mask_h_inds, mask_w_inds = np.where(img_mask == 255)
|
||||
|
||||
# Защита от пустой маски
|
||||
if len(mask_h_inds) > 0 and len(mask_w_inds) > 0:
|
||||
mask_h = np.max(mask_h_inds) - np.min(mask_h_inds)
|
||||
mask_w = np.max(mask_w_inds) - np.min(mask_w_inds)
|
||||
mask_size = int(np.sqrt(mask_h * mask_w))
|
||||
|
||||
k = max(mask_size // 10, 10)
|
||||
kernel = np.ones((k, k), np.uint8)
|
||||
img_mask = cv2.erode(img_mask, kernel, iterations=1)
|
||||
|
||||
kernel = np.ones((2, 2), np.uint8)
|
||||
fake_diff_warped = cv2.dilate(fake_diff_warped, kernel, iterations=1)
|
||||
|
||||
k = max(mask_size // 20, 5)
|
||||
blur_size = (k * 2 + 1, k * 2 + 1)
|
||||
img_mask = cv2.GaussianBlur(img_mask, blur_size, 0)
|
||||
|
||||
k = 5
|
||||
blur_size = (k * 2 + 1, k * 2 + 1)
|
||||
fake_diff_warped = cv2.GaussianBlur(fake_diff_warped, blur_size, 0)
|
||||
|
||||
img_mask /= 255.0
|
||||
img_mask = np.reshape(img_mask, [img_mask.shape[0], img_mask.shape[1], 1])
|
||||
|
||||
fake_merged = img_mask * bgr_fake_warped + (1.0 - img_mask) * target_img.astype(np.float32)
|
||||
return fake_merged.astype(np.uint8)
|
||||
|
||||
class Landmark(BaseONNXModel):
|
||||
"""Извлекает 106 (2D) или 68 (3D) точек лица"""
|
||||
def __init__(self, model_file, providers=None):
|
||||
super().__init__(model_file, providers)
|
||||
self.input_mean = 127.5
|
||||
self.input_std = 128.0
|
||||
self.input_size = tuple(self.input_shape[2:4][::-1])
|
||||
|
||||
output_shape = self.outputs[0].shape
|
||||
# Определяем, какая это модель (3D или 2D) по размеру выхода
|
||||
if output_shape[1] == 3309:
|
||||
self.lmk_dim = 3
|
||||
self.lmk_num = 68
|
||||
self.taskname = 'landmark_3d_68'
|
||||
else:
|
||||
self.lmk_dim = 2
|
||||
self.lmk_num = output_shape[1] // self.lmk_dim
|
||||
self.taskname = f'landmark_2d_{self.lmk_num}'
|
||||
|
||||
def get(self, img, face):
|
||||
bbox = face.bbox
|
||||
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
|
||||
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
|
||||
_scale = self.input_size[0] / (max(w, h) * 1.5)
|
||||
|
||||
# Матрица трансформации (выравнивание по центру bbox)
|
||||
M = np.array([
|
||||
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
|
||||
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
|
||||
], dtype=np.float32)
|
||||
|
||||
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
|
||||
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
|
||||
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
|
||||
|
||||
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
|
||||
|
||||
if pred.shape[0] >= 3000:
|
||||
pred = pred.reshape((-1, 3))
|
||||
else:
|
||||
pred = pred.reshape((-1, 2))
|
||||
|
||||
if self.lmk_num < pred.shape[0]:
|
||||
pred = pred[-self.lmk_num:, :]
|
||||
|
||||
# Денормализация точек в размер модели
|
||||
pred[:, 0:2] += 1
|
||||
pred[:, 0:2] *= (self.input_size[0] // 2)
|
||||
if pred.shape[1] == 3:
|
||||
pred[:, 2] *= (self.input_size[0] // 2)
|
||||
|
||||
# Обратная трансформация точек на оригинальное изображение
|
||||
IM = cv2.invertAffineTransform(M)
|
||||
pred_xy = pred[:, 0:2]
|
||||
pred_xy = np.hstack((pred_xy, np.ones((pred_xy.shape[0], 1)))) # Добавляем гомогенную координату
|
||||
pred_xy = np.dot(IM, pred_xy.T).T
|
||||
|
||||
if pred.shape[1] == 3:
|
||||
pred = np.hstack((pred_xy, pred[:, 2:3])) # Возвращаем Z
|
||||
else:
|
||||
pred = pred_xy
|
||||
|
||||
# Сохраняем в объект Face под правильным именем
|
||||
setattr(face, self.taskname, pred)
|
||||
|
||||
# Честный расчет 3D позы
|
||||
if self.taskname == 'landmark_3d_68':
|
||||
P = estimate_affine_matrix_3d23d(MEANSHAPE_68, pred)
|
||||
_, R, _ = P2sRt(P)
|
||||
rx, ry, rz = matrix2angle(R)
|
||||
face.pose = np.array([rx, ry, rz], dtype=np.float32)
|
||||
|
||||
return pred
|
||||
@@ -0,0 +1,348 @@
|
||||
import numpy as np
|
||||
|
||||
# эталонная 3D-модель "усредненного" человеческого лица (68 точек в 3D-пространстве)
|
||||
|
||||
matrix = [
|
||||
[
|
||||
-0.6266950368881226,
|
||||
-0.2926996946334839,
|
||||
-0.3140018582344055
|
||||
],
|
||||
[
|
||||
-0.5996649265289307,
|
||||
-0.12250272184610367,
|
||||
-0.2924409508705139
|
||||
],
|
||||
[
|
||||
-0.571026086807251,
|
||||
0.05118739977478981,
|
||||
-0.2549465000629425
|
||||
],
|
||||
[
|
||||
-0.5338566899299622,
|
||||
0.21284838020801544,
|
||||
-0.1866333782672882
|
||||
],
|
||||
[
|
||||
-0.4797332286834717,
|
||||
0.3506459593772888,
|
||||
-0.047699932008981705
|
||||
],
|
||||
[
|
||||
-0.3957556486129761,
|
||||
0.4465116560459137,
|
||||
0.07328663021326065
|
||||
],
|
||||
[
|
||||
-0.29880842566490173,
|
||||
0.5106605887413025,
|
||||
0.17977793514728546
|
||||
],
|
||||
[
|
||||
-0.18838681280612946,
|
||||
0.554440438747406,
|
||||
0.3166600465774536
|
||||
],
|
||||
[
|
||||
0.0014708322705700994,
|
||||
0.584439218044281,
|
||||
0.38841578364372253
|
||||
],
|
||||
[
|
||||
0.19099053740501404,
|
||||
0.5517070889472961,
|
||||
0.31433114409446716
|
||||
],
|
||||
[
|
||||
0.3269285559654236,
|
||||
0.48957568407058716,
|
||||
0.16839952766895294
|
||||
],
|
||||
[
|
||||
0.4400261342525482,
|
||||
0.4023579955101013,
|
||||
0.03596242889761925
|
||||
],
|
||||
[
|
||||
0.5068787336349487,
|
||||
0.3116249442100525,
|
||||
-0.09476063400506973
|
||||
],
|
||||
[
|
||||
0.540894627571106,
|
||||
0.20452618598937988,
|
||||
-0.20267142355442047
|
||||
],
|
||||
[
|
||||
0.574118435382843,
|
||||
0.04570329561829567,
|
||||
-0.2841764986515045
|
||||
],
|
||||
[
|
||||
0.5991416573524475,
|
||||
-0.14585931599140167,
|
||||
-0.296495646238327
|
||||
],
|
||||
[
|
||||
0.6275436282157898,
|
||||
-0.30774804949760437,
|
||||
-0.30199483036994934
|
||||
],
|
||||
[
|
||||
-0.47466811537742615,
|
||||
-0.4376046359539032,
|
||||
0.23648710548877716
|
||||
],
|
||||
[
|
||||
-0.4166599214076996,
|
||||
-0.4717560112476349,
|
||||
0.3159925639629364
|
||||
],
|
||||
[
|
||||
-0.3475387990474701,
|
||||
-0.4840780198574066,
|
||||
0.366113543510437
|
||||
],
|
||||
[
|
||||
-0.26063987612724304,
|
||||
-0.4763909876346588,
|
||||
0.3992317318916321
|
||||
],
|
||||
[
|
||||
-0.16712301969528198,
|
||||
-0.45777544379234314,
|
||||
0.4166091978549957
|
||||
],
|
||||
[
|
||||
0.1231740415096283,
|
||||
-0.45874011516571045,
|
||||
0.4250619411468506
|
||||
],
|
||||
[
|
||||
0.2063615769147873,
|
||||
-0.4804193377494812,
|
||||
0.41578391194343567
|
||||
],
|
||||
[
|
||||
0.28667303919792175,
|
||||
-0.4901494085788727,
|
||||
0.3919537663459778
|
||||
],
|
||||
[
|
||||
0.3623969256877899,
|
||||
-0.4768601655960083,
|
||||
0.3527863025665283
|
||||
],
|
||||
[
|
||||
0.425568163394928,
|
||||
-0.4500580430030823,
|
||||
0.2953187823295593
|
||||
],
|
||||
[
|
||||
-0.007627937477082014,
|
||||
-0.3230886459350586,
|
||||
0.46194377541542053
|
||||
],
|
||||
[
|
||||
-0.007876846939325333,
|
||||
-0.255738765001297,
|
||||
0.5104694366455078
|
||||
],
|
||||
[
|
||||
-0.007687545381486416,
|
||||
-0.19917990267276764,
|
||||
0.5525456666946411
|
||||
],
|
||||
[
|
||||
-0.007345120422542095,
|
||||
-0.14261691272258759,
|
||||
0.5986667275428772
|
||||
],
|
||||
[
|
||||
-0.144961878657341,
|
||||
0.033126939088106155,
|
||||
0.4196909964084625
|
||||
],
|
||||
[
|
||||
-0.08434253931045532,
|
||||
0.03127360716462135,
|
||||
0.4731740951538086
|
||||
],
|
||||
[
|
||||
-0.005499685648828745,
|
||||
0.03975145146250725,
|
||||
0.5146695971488953
|
||||
],
|
||||
[
|
||||
0.06346292793750763,
|
||||
0.04613516479730606,
|
||||
0.4792240560054779
|
||||
],
|
||||
[
|
||||
0.13398391008377075,
|
||||
0.02204025723040104,
|
||||
0.41907867789268494
|
||||
],
|
||||
[
|
||||
-0.38675081729888916,
|
||||
-0.3133975565433502,
|
||||
0.259631872177124
|
||||
],
|
||||
[
|
||||
-0.3166607916355133,
|
||||
-0.35007160902023315,
|
||||
0.3285270035266876
|
||||
],
|
||||
[
|
||||
-0.2341379076242447,
|
||||
-0.35491427779197693,
|
||||
0.3334933817386627
|
||||
],
|
||||
[
|
||||
-0.15516234934329987,
|
||||
-0.31524932384490967,
|
||||
0.3143281042575836
|
||||
],
|
||||
[
|
||||
-0.23092176020145416,
|
||||
-0.28427040576934814,
|
||||
0.3255828619003296
|
||||
],
|
||||
[
|
||||
-0.3175090253353119,
|
||||
-0.28516536951065063,
|
||||
0.3098825216293335
|
||||
],
|
||||
[
|
||||
0.13895708322525024,
|
||||
-0.30982404947280884,
|
||||
0.3182835876941681
|
||||
],
|
||||
[
|
||||
0.21945592761039734,
|
||||
-0.35319215059280396,
|
||||
0.33802759647369385
|
||||
],
|
||||
[
|
||||
0.30174651741981506,
|
||||
-0.349665105342865,
|
||||
0.333102285861969
|
||||
],
|
||||
[
|
||||
0.37665316462516785,
|
||||
-0.31351813673973083,
|
||||
0.26322856545448303
|
||||
],
|
||||
[
|
||||
0.2966947853565216,
|
||||
-0.2871439754962921,
|
||||
0.3220130503177643
|
||||
],
|
||||
[
|
||||
0.21462441980838776,
|
||||
-0.2905276417732239,
|
||||
0.33124178647994995
|
||||
],
|
||||
[
|
||||
-0.20143844187259674,
|
||||
0.23736143112182617,
|
||||
0.37953662872314453
|
||||
],
|
||||
[
|
||||
-0.13732077181339264,
|
||||
0.18578563630580902,
|
||||
0.46525245904922485
|
||||
],
|
||||
[
|
||||
-0.07648587226867676,
|
||||
0.15119342505931854,
|
||||
0.5035716891288757
|
||||
],
|
||||
[
|
||||
-0.002535885199904442,
|
||||
0.16872699558734894,
|
||||
0.5164376497268677
|
||||
],
|
||||
[
|
||||
0.06442102789878845,
|
||||
0.15088020265102386,
|
||||
0.5045241117477417
|
||||
],
|
||||
[
|
||||
0.126465305685997,
|
||||
0.17947596311569214,
|
||||
0.4685916602611542
|
||||
],
|
||||
[
|
||||
0.21824784576892853,
|
||||
0.23899227380752563,
|
||||
0.37567368149757385
|
||||
],
|
||||
[
|
||||
0.13288259506225586,
|
||||
0.28392839431762695,
|
||||
0.4400508999824524
|
||||
],
|
||||
[
|
||||
0.06802233308553696,
|
||||
0.2973543107509613,
|
||||
0.4774041771888733
|
||||
],
|
||||
[
|
||||
-0.0004690653004217893,
|
||||
0.30004069209098816,
|
||||
0.4871094226837158
|
||||
],
|
||||
[
|
||||
-0.06934267282485962,
|
||||
0.29696860909461975,
|
||||
0.4805404841899872
|
||||
],
|
||||
[
|
||||
-0.14252015948295593,
|
||||
0.2743033170700073,
|
||||
0.43808019161224365
|
||||
],
|
||||
[
|
||||
-0.1781347393989563,
|
||||
0.23059049248695374,
|
||||
0.3963589668273926
|
||||
],
|
||||
[
|
||||
-0.07403063774108887,
|
||||
0.2147187888622284,
|
||||
0.4653262794017792
|
||||
],
|
||||
[
|
||||
-0.002636224264279008,
|
||||
0.21414154767990112,
|
||||
0.4832296073436737
|
||||
],
|
||||
[
|
||||
0.05981616675853729,
|
||||
0.21076396107673645,
|
||||
0.47224411368370056
|
||||
],
|
||||
[
|
||||
0.16690002381801605,
|
||||
0.23127099871635437,
|
||||
0.396789014339447
|
||||
],
|
||||
[
|
||||
0.059809282422065735,
|
||||
0.22376428544521332,
|
||||
0.46641337871551514
|
||||
],
|
||||
[
|
||||
-0.0014343614457175136,
|
||||
0.2257590889930725,
|
||||
0.4752439558506012
|
||||
],
|
||||
[
|
||||
-0.07522077113389969,
|
||||
0.23065608739852905,
|
||||
0.4671475291252136
|
||||
]
|
||||
]
|
||||
|
||||
MEANSHAPE_68 = np.array(matrix, dtype=np.float32)
|
||||
@@ -1,161 +0,0 @@
|
||||
import os.path as osp
|
||||
import glob
|
||||
import logging
|
||||
import insightface
|
||||
from insightface.model_zoo.model_zoo import ModelRouter, PickableInferenceSession
|
||||
from insightface.model_zoo.retinaface import RetinaFace
|
||||
from insightface.model_zoo.landmark import Landmark
|
||||
from insightface.model_zoo.attribute import Attribute
|
||||
from insightface.model_zoo.inswapper import INSwapper
|
||||
from insightface.model_zoo.arcface_onnx import ArcFaceONNX
|
||||
from insightface.app import FaceAnalysis
|
||||
from insightface.utils import DEFAULT_MP_NAME, ensure_available
|
||||
from insightface.model_zoo import model_zoo
|
||||
import onnxruntime
|
||||
import onnx
|
||||
from onnx import numpy_helper
|
||||
from scripts.reactor_logger import logger
|
||||
|
||||
|
||||
def patched_get_model_log(self, **kwargs):
|
||||
session = PickableInferenceSession(self.onnx_file, **kwargs)
|
||||
print(f'Applied providers: {session._providers}, with options: {session._provider_options}')
|
||||
inputs = session.get_inputs()
|
||||
input_cfg = inputs[0]
|
||||
input_shape = input_cfg.shape
|
||||
outputs = session.get_outputs()
|
||||
|
||||
if len(outputs) >= 5:
|
||||
return RetinaFace(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == 192 and input_shape[3] == 192:
|
||||
return Landmark(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == 96 and input_shape[3] == 96:
|
||||
return Attribute(model_file=self.onnx_file, session=session)
|
||||
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
|
||||
return INSwapper(model_file=self.onnx_file, session=session)
|
||||
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
|
||||
return INSwapper(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
|
||||
return ArcFaceONNX(model_file=self.onnx_file, session=session)
|
||||
else:
|
||||
return None
|
||||
|
||||
def patched_get_model(self, **kwargs):
|
||||
session = PickableInferenceSession(self.onnx_file, **kwargs)
|
||||
inputs = session.get_inputs()
|
||||
input_cfg = inputs[0]
|
||||
input_shape = input_cfg.shape
|
||||
outputs = session.get_outputs()
|
||||
|
||||
if len(outputs) >= 5:
|
||||
return RetinaFace(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == 192 and input_shape[3] == 192:
|
||||
return Landmark(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == 96 and input_shape[3] == 96:
|
||||
return Attribute(model_file=self.onnx_file, session=session)
|
||||
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
|
||||
return INSwapper(model_file=self.onnx_file, session=session)
|
||||
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
|
||||
return INSwapper(model_file=self.onnx_file, session=session)
|
||||
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
|
||||
return ArcFaceONNX(model_file=self.onnx_file, session=session)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def patched_faceanalysis_init(self, name=DEFAULT_MP_NAME, root='~/.insightface', allowed_modules=None, **kwargs):
|
||||
onnxruntime.set_default_logger_severity(3)
|
||||
self.models = {}
|
||||
self.model_dir = ensure_available('models', name, root=root)
|
||||
onnx_files = glob.glob(osp.join(self.model_dir, '*.onnx'))
|
||||
onnx_files = sorted(onnx_files)
|
||||
for onnx_file in onnx_files:
|
||||
model = model_zoo.get_model(onnx_file, **kwargs)
|
||||
if model is None:
|
||||
print('model not recognized:', onnx_file)
|
||||
elif allowed_modules is not None and model.taskname not in allowed_modules:
|
||||
print('model ignore:', onnx_file, model.taskname)
|
||||
del model
|
||||
elif model.taskname not in self.models and (allowed_modules is None or model.taskname in allowed_modules):
|
||||
self.models[model.taskname] = model
|
||||
else:
|
||||
print('duplicated model task type, ignore:', onnx_file, model.taskname)
|
||||
del model
|
||||
assert 'detection' in self.models
|
||||
self.det_model = self.models['detection']
|
||||
|
||||
|
||||
def patched_faceanalysis_prepare(self, ctx_id, det_thresh=0.5, det_size=(640, 640)):
|
||||
self.det_thresh = det_thresh
|
||||
assert det_size is not None
|
||||
self.det_size = det_size
|
||||
for taskname, model in self.models.items():
|
||||
if taskname == 'detection':
|
||||
model.prepare(ctx_id, input_size=det_size, det_thresh=det_thresh)
|
||||
else:
|
||||
model.prepare(ctx_id)
|
||||
|
||||
|
||||
def patched_inswapper_init(self, model_file=None, session=None):
|
||||
self.model_file = model_file
|
||||
self.session = session
|
||||
model = onnx.load(self.model_file)
|
||||
graph = model.graph
|
||||
self.emap = numpy_helper.to_array(graph.initializer[-1])
|
||||
self.input_mean = 0.0
|
||||
self.input_std = 255.0
|
||||
if self.session is None:
|
||||
self.session = onnxruntime.InferenceSession(self.model_file, None)
|
||||
inputs = self.session.get_inputs()
|
||||
self.input_names = []
|
||||
for inp in inputs:
|
||||
self.input_names.append(inp.name)
|
||||
outputs = self.session.get_outputs()
|
||||
output_names = []
|
||||
for out in outputs:
|
||||
output_names.append(out.name)
|
||||
self.output_names = output_names
|
||||
assert len(self.output_names) == 1
|
||||
input_cfg = inputs[0]
|
||||
input_shape = input_cfg.shape
|
||||
self.input_shape = input_shape
|
||||
self.input_size = tuple(input_shape[2:4][::-1])
|
||||
|
||||
|
||||
def pathced_retinaface_prepare(self, ctx_id, **kwargs):
|
||||
if ctx_id<0:
|
||||
self.session.set_providers(['CPUExecutionProvider'])
|
||||
nms_thresh = kwargs.get('nms_thresh', None)
|
||||
if nms_thresh is not None:
|
||||
self.nms_thresh = nms_thresh
|
||||
det_thresh = kwargs.get('det_thresh', None)
|
||||
if det_thresh is not None:
|
||||
self.det_thresh = det_thresh
|
||||
input_size = kwargs.get('input_size', None)
|
||||
if input_size is not None and self.input_size is None:
|
||||
self.input_size = input_size
|
||||
|
||||
|
||||
def patch_insightface(get_model, faceanalysis_init, faceanalysis_prepare, inswapper_init, retinaface_prepare):
|
||||
insightface.model_zoo.model_zoo.ModelRouter.get_model = get_model
|
||||
insightface.app.FaceAnalysis.__init__ = faceanalysis_init
|
||||
insightface.app.FaceAnalysis.prepare = faceanalysis_prepare
|
||||
insightface.model_zoo.inswapper.INSwapper.__init__ = inswapper_init
|
||||
insightface.model_zoo.retinaface.RetinaFace.prepare = retinaface_prepare
|
||||
|
||||
|
||||
# original_functions = [ModelRouter.get_model, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
|
||||
original_functions = [patched_get_model_log, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
|
||||
patched_functions = [patched_get_model, patched_faceanalysis_init, patched_faceanalysis_prepare, patched_inswapper_init, pathced_retinaface_prepare]
|
||||
|
||||
|
||||
def apply_patch(console_log_level):
|
||||
if console_log_level == 0:
|
||||
patch_insightface(*patched_functions)
|
||||
logger.setLevel(logging.WARNING)
|
||||
elif console_log_level == 1:
|
||||
patch_insightface(*patched_functions)
|
||||
logger.setLevel(logging.STATUS)
|
||||
elif console_log_level == 2:
|
||||
patch_insightface(*original_functions)
|
||||
logger.setLevel(logging.INFO)
|
||||
+1
-1
@@ -7,7 +7,7 @@ import cv2
|
||||
import math
|
||||
import logging
|
||||
import hashlib
|
||||
from insightface.app.common import Face
|
||||
from reactor_core.face_objects import Face
|
||||
from safetensors.torch import save_file, safe_open
|
||||
from tqdm import tqdm
|
||||
import urllib.request
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
albumentations>=1.4.16
|
||||
insightface==0.7.3
|
||||
onnx>=1.14.0
|
||||
opencv-python>=4.7.0.72
|
||||
numpy==1.26.4
|
||||
numpy
|
||||
segment_anything
|
||||
ultralytics
|
||||
colour-science
|
||||
|
||||
+16
-58
@@ -2,14 +2,14 @@ import os, glob
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import modules.scripts as scripts
|
||||
import r_modules.scripts as scripts
|
||||
# from modules.upscaler import Upscaler, UpscalerData
|
||||
from modules import scripts, scripts_postprocessing
|
||||
from modules.processing import (
|
||||
StableDiffusionProcessing,
|
||||
StableDiffusionProcessingImg2Img,
|
||||
from r_modules import scripts, scripts_postprocessing
|
||||
from r_modules.processing import (
|
||||
Processing,
|
||||
ProcessingImg2Img,
|
||||
)
|
||||
from modules.shared import state
|
||||
from r_modules.shared import state
|
||||
from scripts.reactor_logger import logger
|
||||
from scripts.reactor_swapper import (
|
||||
swap_face,
|
||||
@@ -26,7 +26,8 @@ import comfy.model_management as model_management
|
||||
def get_models():
|
||||
swappers = [
|
||||
"insightface",
|
||||
"reswapper"
|
||||
"reswapper",
|
||||
"hyperswap"
|
||||
]
|
||||
models_list = []
|
||||
for folder in swappers:
|
||||
@@ -42,7 +43,7 @@ class FaceSwapScript(scripts.Script):
|
||||
|
||||
def process(
|
||||
self,
|
||||
p: StableDiffusionProcessing,
|
||||
p: Processing,
|
||||
img,
|
||||
enable,
|
||||
source_faces_index,
|
||||
@@ -101,12 +102,12 @@ class FaceSwapScript(scripts.Script):
|
||||
self.gender_target = 2
|
||||
|
||||
# if self.source is not None:
|
||||
if isinstance(p, StableDiffusionProcessingImg2Img) and swap_in_source:
|
||||
if isinstance(p, ProcessingImg2Img) and swap_in_source:
|
||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
||||
|
||||
if len(p.init_images) == 1:
|
||||
|
||||
result = swap_face(
|
||||
result, bbox, swapped_indexes = swap_face(
|
||||
self.source,
|
||||
p.init_images[0],
|
||||
source_faces_index=self.source_faces_index,
|
||||
@@ -123,27 +124,11 @@ class FaceSwapScript(scripts.Script):
|
||||
interpolation=self.interpolation,
|
||||
)
|
||||
p.init_images[0] = result
|
||||
|
||||
# for i in range(len(p.init_images)):
|
||||
# if state.interrupted or model_management.processing_interrupted():
|
||||
# logger.status("Interrupted by User")
|
||||
# break
|
||||
# if len(p.init_images) > 1:
|
||||
# logger.status(f"Swap in %s", i)
|
||||
# result = swap_face(
|
||||
# self.source,
|
||||
# p.init_images[i],
|
||||
# source_faces_index=self.source_faces_index,
|
||||
# faces_index=self.faces_index,
|
||||
# model=self.model,
|
||||
# gender_source=self.gender_source,
|
||||
# gender_target=self.gender_target,
|
||||
# face_model=self.face_model,
|
||||
# )
|
||||
# p.init_images[i] = result
|
||||
p.bbox = bbox
|
||||
p.swapped_indexes = swapped_indexes
|
||||
|
||||
elif len(p.init_images) > 1:
|
||||
result = swap_face_many(
|
||||
result, bbox, swapped_indexes = swap_face_many(
|
||||
self.source,
|
||||
p.init_images,
|
||||
source_faces_index=self.source_faces_index,
|
||||
@@ -160,34 +145,7 @@ class FaceSwapScript(scripts.Script):
|
||||
interpolation=self.interpolation,
|
||||
)
|
||||
p.init_images = result
|
||||
p.bbox = bbox
|
||||
p.swapped_indexes = swapped_indexes
|
||||
|
||||
logger.status("--Done!--")
|
||||
# else:
|
||||
# logger.error(f"Please provide a source face")
|
||||
|
||||
def postprocess_batch(self, p, *args, **kwargs):
|
||||
if self.enable:
|
||||
images = kwargs["images"]
|
||||
|
||||
def postprocess_image(self, p, script_pp: scripts.PostprocessImageArgs, *args):
|
||||
if self.enable and self.swap_in_generated:
|
||||
if self.source is not None:
|
||||
logger.status(f"Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index)
|
||||
image: Image.Image = script_pp.image
|
||||
result = swap_face(
|
||||
self.source,
|
||||
image,
|
||||
source_faces_index=self.source_faces_index,
|
||||
faces_index=self.faces_index,
|
||||
model=self.model,
|
||||
upscale_options=self.upscale_options,
|
||||
gender_source=self.gender_source,
|
||||
gender_target=self.gender_target,
|
||||
)
|
||||
try:
|
||||
pp = scripts_postprocessing.PostprocessedImage(result)
|
||||
pp.info = {}
|
||||
p.extra_generation_params.update(pp.info)
|
||||
script_pp.image = pp.image
|
||||
except:
|
||||
logger.error(f"Cannot create a result image")
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from modules import shared
|
||||
from r_modules import shared
|
||||
from reactor_utils import addLoggingLevel
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def ensure_nsfw_model(nsfwdet_model_path):
|
||||
MODEL_EXISTS = True if downloaded == 3 else False
|
||||
return MODEL_EXISTS
|
||||
|
||||
SCORE = 0.96
|
||||
SCORE = 0.979
|
||||
|
||||
logging.getLogger("transformers").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
+153
-178
@@ -6,17 +6,15 @@ import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
import insightface
|
||||
from insightface.app.common import Face
|
||||
# try:
|
||||
# import torch.cuda as cuda
|
||||
# except:
|
||||
# cuda = None
|
||||
from reactor_core.analyzer import ReActorFaceAnalysis
|
||||
from reactor_core.face_objects import Face
|
||||
from reactor_core.inswap import INSwapper
|
||||
from reactor_core.hyperswap import HyperSwapper
|
||||
import torch
|
||||
|
||||
import folder_paths
|
||||
import comfy.model_management as model_management
|
||||
from modules.shared import state
|
||||
from r_modules.shared import state
|
||||
|
||||
from scripts.reactor_logger import logger
|
||||
from reactor_utils import (
|
||||
@@ -45,13 +43,6 @@ try:
|
||||
except Exception as e:
|
||||
logger.debug(f"ExecutionProviderError: {e}.\nEP is set to CPU.")
|
||||
providers = ["CPUExecutionProvider"]
|
||||
# if cuda is not None:
|
||||
# if cuda.is_available():
|
||||
# providers = ["CUDAExecutionProvider"]
|
||||
# else:
|
||||
# providers = ["CPUExecutionProvider"]
|
||||
# else:
|
||||
# providers = ["CPUExecutionProvider"]
|
||||
|
||||
models_path_old = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models")
|
||||
insightface_path_old = os.path.join(models_path_old, "insightface")
|
||||
@@ -61,6 +52,7 @@ models_path = folder_paths.models_dir
|
||||
insightface_path = os.path.join(models_path, "insightface")
|
||||
insightface_models_path = os.path.join(insightface_path, "models")
|
||||
reswapper_path = os.path.join(models_path, "reswapper")
|
||||
hyperswap_path = os.path.join(models_path, "hyperswap")
|
||||
|
||||
if os.path.exists(models_path_old):
|
||||
move_path(insightface_models_path_old, insightface_models_path)
|
||||
@@ -88,11 +80,6 @@ TARGET_IMAGE_LIST_HASH = []
|
||||
|
||||
def unload_model(model):
|
||||
if model is not None:
|
||||
# check if model has unload method
|
||||
# if "unload" in model:
|
||||
# model.unload()
|
||||
# if "model_unload" in model:
|
||||
# model.model_unload()
|
||||
del model
|
||||
return None
|
||||
|
||||
@@ -110,7 +97,7 @@ def getAnalysisModel(det_size = (640, 640)):
|
||||
global ANALYSIS_MODELS
|
||||
ANALYSIS_MODEL = ANALYSIS_MODELS[str(det_size[0])]
|
||||
if ANALYSIS_MODEL is None:
|
||||
ANALYSIS_MODEL = insightface.app.FaceAnalysis(
|
||||
ANALYSIS_MODEL = ReActorFaceAnalysis(
|
||||
name="buffalo_l", providers=providers, root=insightface_path
|
||||
)
|
||||
ANALYSIS_MODEL.prepare(ctx_id=0, det_size=det_size)
|
||||
@@ -122,11 +109,18 @@ def getFaceSwapModel(model_path: str):
|
||||
if FS_MODEL is None or CURRENT_FS_MODEL_PATH is None or CURRENT_FS_MODEL_PATH != model_path:
|
||||
CURRENT_FS_MODEL_PATH = model_path
|
||||
FS_MODEL = unload_model(FS_MODEL)
|
||||
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
|
||||
|
||||
model_filename = os.path.basename(model_path)
|
||||
if "hyperswap" in model_filename.lower(): # Если это Hyperswap
|
||||
model_path = os.path.join(folder_paths.models_dir, "hyperswap", model_filename)
|
||||
FS_MODEL = HyperSwapper(model_path, providers=providers)
|
||||
else: # Если это INSwapper / Reswapper
|
||||
if "reswapper" in model_filename.lower():
|
||||
model_path = os.path.join(folder_paths.models_dir, "reswapper", model_filename)
|
||||
FS_MODEL = INSwapper(model_path, providers=providers)
|
||||
|
||||
return FS_MODEL
|
||||
|
||||
|
||||
def sort_by_order(face, order: str):
|
||||
if order == "left-right":
|
||||
return sorted(face, key=lambda x: x.bbox[0])
|
||||
@@ -148,36 +142,37 @@ def get_face_gender(
|
||||
operated: str,
|
||||
order: str,
|
||||
):
|
||||
filtered_faces = [
|
||||
f for f in face
|
||||
if (gender_condition == 0) or
|
||||
(gender_condition == 1 and f.sex == "F") or
|
||||
(gender_condition == 2 and f.sex == "M")
|
||||
]
|
||||
|
||||
gender = "Female" if gender_condition == 1 else "Male" if gender_condition == 0 else ""
|
||||
|
||||
if len(filtered_faces) == 0:
|
||||
if gender_condition != 0:
|
||||
logger.status(f"No faces found for -{gender}-")
|
||||
return None, 0 # treat as "wrong gender" to skip
|
||||
|
||||
faces_sorted = sort_by_order(filtered_faces, order)
|
||||
# 1. Сортируем ВСЕ найденные лица (без фильтрации!)
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
|
||||
# 2. Проверяем, существует ли вообще лицо с таким визуальным индексом
|
||||
if face_index >= len(faces_sorted):
|
||||
logger.info("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(faces_sorted))
|
||||
return None, 0
|
||||
logger.info("Requested face index (%s) is out of bounds (max available index is %s)", face_index, len(faces_sorted) - 1)
|
||||
return None, 0, None
|
||||
|
||||
# 3. Берем конкретное лицо по его позиции на фото (например, второе справа)
|
||||
face_selected = faces_sorted[face_index]
|
||||
|
||||
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, face_selected.sex)
|
||||
# Если фильтр по полу отключен (no) - сразу отдаем лицо в работу
|
||||
if gender_condition == 0:
|
||||
return face_selected, 0, face_index
|
||||
|
||||
expected_gender = "F" if gender_condition == 1 else "M"
|
||||
if gender_condition != 0 and face_selected.sex != expected_gender:
|
||||
logger.info(f"{operated} Face {face_index}: WRONG gender ({face_selected.sex})")
|
||||
return face_selected, 1 # <-- есть, но не тот пол
|
||||
# 4. Проверяем пол выбранного лица
|
||||
# face.gender: 0 = female, 1 = male
|
||||
# gender_condition: 1 = female, 2 = male
|
||||
expected_gender = 0 if gender_condition == 1 else 1
|
||||
actual_gender = getattr(face_selected, 'gender', -1)
|
||||
|
||||
sel_gender_str = "Male" if actual_gender == 1 else "Female" if actual_gender == 0 else "Unknown"
|
||||
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, sel_gender_str)
|
||||
|
||||
return face_selected, 0
|
||||
# Если пол не совпадает с тем, что заказал юзер
|
||||
if actual_gender != expected_gender:
|
||||
logger.info(f"{operated} Face {face_index}: WRONG gender ({sel_gender_str})")
|
||||
return face_selected, 1, face_index # 1 означает флаг wrong_gender = True (цикл его пропустит)
|
||||
|
||||
# Если всё идеально
|
||||
return face_selected, 0, face_index
|
||||
|
||||
def half_det_size(det_size):
|
||||
logger.status("Trying to halve 'det_size' parameter")
|
||||
@@ -189,7 +184,10 @@ def analyze_faces(img_data: np.ndarray, det_size=(640, 640)):
|
||||
faces = []
|
||||
try:
|
||||
faces = face_analyser.get(img_data)
|
||||
except:
|
||||
except Exception as e:
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
# logger.error(f"Error during face analysis: {e}")
|
||||
logger.error("No faces found")
|
||||
|
||||
# Try halving det_size if no faces are found
|
||||
@@ -223,10 +221,9 @@ def get_face_single(img_data: np.ndarray, face, face_index=0, det_size=(640, 640
|
||||
|
||||
try:
|
||||
faces_sorted = sort_by_order(face, order)
|
||||
return faces_sorted[face_index], 0
|
||||
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
|
||||
return faces_sorted[face_index], 0, face_index
|
||||
except IndexError:
|
||||
return None, 0
|
||||
return None, 0, None
|
||||
|
||||
|
||||
def swap_face(
|
||||
@@ -247,6 +244,8 @@ def swap_face(
|
||||
):
|
||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH
|
||||
result_image = target_img
|
||||
bbox = []
|
||||
swapped_indexes = []
|
||||
|
||||
if model is not None:
|
||||
|
||||
@@ -323,82 +322,76 @@ def swap_face(
|
||||
logger.status("Using Hashed Target Face(s) Model...")
|
||||
target_faces = TARGET_FACES
|
||||
|
||||
# No use in trying to swap faces if no faces are found, enhancement
|
||||
if len(target_faces) == 0:
|
||||
logger.status("Cannot detect any Target, skipping swapping...")
|
||||
return result_image
|
||||
return result_image, bbox, swapped_indexes
|
||||
|
||||
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||
|
||||
# 1. Заранее собираем список ТОЛЬКО ВАЛИДНЫХ исходных лиц
|
||||
valid_source_faces = []
|
||||
if source_img is not None:
|
||||
# separated management of wrong_gender between source and target, enhancement
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
for idx in source_faces_index:
|
||||
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
else:
|
||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
||||
src_wrong_gender = 0
|
||||
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
|
||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
||||
elif source_face is not None:
|
||||
if len(valid_source_faces) == 0:
|
||||
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||
else:
|
||||
result = target_img
|
||||
if "inswapper" in model:
|
||||
model_path = os.path.join(insightface_path, model)
|
||||
elif "reswapper" in model:
|
||||
model_path = os.path.join(reswapper_path, model)
|
||||
elif "hyperswap" in model:
|
||||
model_path = os.path.join(hyperswap_path, model)
|
||||
|
||||
face_swapper = getFaceSwapModel(model_path)
|
||||
|
||||
source_face_idx = 0
|
||||
|
||||
# 2. Идем по целевым лицам
|
||||
for face_num in faces_index:
|
||||
# No use in trying to swap faces if no further faces are found, enhancement
|
||||
if face_num >= len(target_faces):
|
||||
logger.status("Checked all existing target faces, skipping swapping...")
|
||||
break
|
||||
|
||||
if len(source_faces_index) > 1 and source_face_idx > 0:
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
|
||||
source_face_idx += 1
|
||||
|
||||
if source_face is not None and src_wrong_gender == 0:
|
||||
target_face, wrong_gender = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
if target_face is not None and wrong_gender == 0:
|
||||
logger.status(f"Swapping...")
|
||||
if face_boost_enabled:
|
||||
logger.status(f"Face Boost is enabled")
|
||||
bgr_fake, M = face_swapper.get(result, target_face, source_face, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
# logger.status(f"Swapping as-is")
|
||||
result = face_swapper.get(result, target_face, source_face)
|
||||
elif wrong_gender == 1:
|
||||
wrong_gender = 0
|
||||
# Keep searching for other faces if wrong gender is detected, enhancement
|
||||
#if source_face_idx == len(source_faces_index):
|
||||
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
# return result_image
|
||||
logger.status("Wrong target gender detected")
|
||||
continue
|
||||
target_face, wrong_gender, target_face_index = get_face_single(target_img, target_faces, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
|
||||
if target_face is not None and wrong_gender == 0:
|
||||
logger.status(f"Swapping...")
|
||||
|
||||
# 3. Берем валидное лицо (если их меньше, чем целей — идем по кругу)
|
||||
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||
|
||||
if face_boost_enabled and "hyperswap" not in model:
|
||||
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
|
||||
bgr_fake, M = face_swapper.get(result, target_face, source_face_to_use, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(result, bgr_fake, M)
|
||||
else:
|
||||
logger.info(f"No target face found for {face_num}")
|
||||
elif src_wrong_gender == 1:
|
||||
src_wrong_gender = 0
|
||||
# Keep searching for other faces if wrong gender is detected, enhancement
|
||||
#if source_face_idx == len(source_faces_index):
|
||||
# result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
# return result_image
|
||||
logger.status("Wrong source gender detected")
|
||||
result = face_swapper.get(result, target_face, source_face_to_use)
|
||||
|
||||
bbox.append(tuple(map(float, target_face.bbox)))
|
||||
swapped_indexes.append(target_face_index)
|
||||
|
||||
# Продвигаем индекс исходного лица ТОЛЬКО после УСПЕШНОГО применения
|
||||
if len(valid_source_faces) > 1:
|
||||
source_face_idx += 1
|
||||
|
||||
elif wrong_gender == 1:
|
||||
logger.status("Wrong target gender detected")
|
||||
continue
|
||||
else:
|
||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
||||
logger.info(f"No target face found for {face_num}")
|
||||
|
||||
result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
|
||||
|
||||
else:
|
||||
logger.status("No source face(s) in the provided Index")
|
||||
else:
|
||||
logger.status("No source face(s) found")
|
||||
return result_image
|
||||
return result_image, bbox, swapped_indexes
|
||||
|
||||
def swap_face_many(
|
||||
source_img: Union[Image.Image, None],
|
||||
@@ -418,28 +411,23 @@ def swap_face_many(
|
||||
):
|
||||
global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, TARGET_FACES_LIST, TARGET_IMAGE_LIST_HASH
|
||||
result_images = target_imgs
|
||||
bbox = []
|
||||
swapped_indexes = []
|
||||
|
||||
if model is not None:
|
||||
|
||||
if isinstance(source_img, str): # source_img is a base64 string
|
||||
if isinstance(source_img, str):
|
||||
import base64, io
|
||||
if 'base64,' in source_img: # check if the base64 string has a data URL scheme
|
||||
# split the base64 string to get the actual base64 encoded image data
|
||||
if 'base64,' in source_img:
|
||||
base64_data = source_img.split('base64,')[-1]
|
||||
# decode base64 string to bytes
|
||||
img_bytes = base64.b64decode(base64_data)
|
||||
else:
|
||||
# if no data URL scheme, just decode
|
||||
img_bytes = base64.b64decode(source_img)
|
||||
|
||||
source_img = Image.open(io.BytesIO(img_bytes))
|
||||
|
||||
target_imgs = [cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) for target_img in target_imgs]
|
||||
|
||||
if source_img is not None:
|
||||
|
||||
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
||||
|
||||
source_image_md5hash = get_image_md5hash(source_img)
|
||||
|
||||
if SOURCE_IMAGE_HASH is None:
|
||||
@@ -462,17 +450,14 @@ def swap_face_many(
|
||||
source_faces = SOURCE_FACES
|
||||
|
||||
elif face_model is not None:
|
||||
|
||||
source_faces_index = [0]
|
||||
logger.status("Using Loaded Source Face Model...")
|
||||
source_face_model = [face_model]
|
||||
source_faces = source_face_model
|
||||
|
||||
else:
|
||||
logger.error("Cannot detect any Source")
|
||||
|
||||
if source_faces is not None:
|
||||
|
||||
target_faces = []
|
||||
pbar = progress_bar(len(target_imgs))
|
||||
|
||||
@@ -502,105 +487,95 @@ def swap_face_many(
|
||||
logger.info("(Image %s) Target Image the Same? %s", i, target_image_same)
|
||||
|
||||
if len(TARGET_FACES_LIST) == 0:
|
||||
# logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST = [target_face]
|
||||
elif len(TARGET_FACES_LIST) == i and not target_image_same:
|
||||
# logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST.append(target_face)
|
||||
elif len(TARGET_FACES_LIST) != i and not target_image_same:
|
||||
# logger.status(f"Analyzing Target Image {i}...")
|
||||
target_face = analyze_faces(target_img)
|
||||
TARGET_FACES_LIST[i] = target_face
|
||||
elif target_image_same:
|
||||
# logger.status("(Image %s) Using Hashed Target Face(s) Model...", i)
|
||||
target_face = TARGET_FACES_LIST[i]
|
||||
|
||||
|
||||
# logger.status(f"Analyzing Target Image {i}...")
|
||||
# target_face = analyze_faces(target_img)
|
||||
if target_face is not None:
|
||||
target_faces.append(target_face)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
# No use in trying to swap faces if no faces are found, enhancement
|
||||
if len(target_faces) == 0:
|
||||
logger.status("Cannot detect any Target, skipping swapping...")
|
||||
return result_images
|
||||
return result_images, bbox, swapped_indexes
|
||||
|
||||
# --- НОВАЯ ИДЕАЛЬНАЯ ЛОГИКА СОРТИРОВКИ ---
|
||||
|
||||
valid_source_faces = []
|
||||
if source_img is not None:
|
||||
# separated management of wrong_gender between source and target, enhancement
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
for idx in source_faces_index:
|
||||
sf, src_wrong_gender, _ = get_face_single(source_img, source_faces, face_index=idx, gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
else:
|
||||
# source_face = sorted(source_faces, key=lambda x: x.bbox[0])[source_faces_index[0]]
|
||||
source_face = sorted(source_faces, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]), reverse = True)[source_faces_index[0]]
|
||||
src_wrong_gender = 0
|
||||
sf, src_wrong_gender, _ = get_face_single(None, source_faces, face_index=source_faces_index[0], gender_source=gender_source, order=faces_order[1])
|
||||
if sf is not None and src_wrong_gender == 0:
|
||||
valid_source_faces.append(sf)
|
||||
|
||||
if len(source_faces_index) != 0 and len(source_faces_index) != 1 and len(source_faces_index) != len(faces_index):
|
||||
logger.status(f'Source Faces must have no entries (default=0), one entry, or same number of entries as target faces.')
|
||||
elif source_face is not None:
|
||||
if len(valid_source_faces) == 0:
|
||||
logger.status("No valid source face(s) found in the provided Index after gender filter")
|
||||
else:
|
||||
results = target_imgs
|
||||
model_path = model_path = os.path.join(insightface_path, model)
|
||||
if "inswapper" in model:
|
||||
model_path = os.path.join(insightface_path, model)
|
||||
elif "reswapper" in model:
|
||||
model_path = os.path.join(reswapper_path, model)
|
||||
elif "hyperswap" in model:
|
||||
model_path = os.path.join(hyperswap_path, model)
|
||||
|
||||
face_swapper = getFaceSwapModel(model_path)
|
||||
|
||||
source_face_idx = 0
|
||||
|
||||
pbar = progress_bar(len(target_imgs))
|
||||
|
||||
logger.status(f"Swapping...")
|
||||
|
||||
for face_num in faces_index:
|
||||
# No use in trying to swap faces if no further faces are found, enhancement
|
||||
if face_num >= len(target_faces):
|
||||
logger.status("Checked all existing target faces, skipping swapping...")
|
||||
break
|
||||
|
||||
if len(source_faces_index) > 1 and source_face_idx > 0:
|
||||
source_face, src_wrong_gender = get_face_single(source_img, source_faces, face_index=source_faces_index[source_face_idx], gender_source=gender_source, order=faces_order[1])
|
||||
source_face_idx += 1
|
||||
|
||||
if source_face is not None and src_wrong_gender == 0:
|
||||
# Reading results to make current face swap on a previous face result
|
||||
# logger.status(f"Swapping...")
|
||||
for i, (target_img, target_face) in enumerate(zip(results, target_faces)):
|
||||
target_face_single, wrong_gender = get_face_single(target_img, target_face, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
if target_face_single is not None and wrong_gender == 0:
|
||||
result = target_img
|
||||
if face_boost_enabled:
|
||||
logger.status(f"Face Boost is enabled")
|
||||
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
# logger.status(f"Swapping as-is")
|
||||
result = face_swapper.get(target_img, target_face_single, source_face)
|
||||
results[i] = result
|
||||
pbar.update(1)
|
||||
elif wrong_gender == 1:
|
||||
wrong_gender = 0
|
||||
logger.status("Wrong target gender detected")
|
||||
pbar.update(1)
|
||||
continue
|
||||
target_used_in_any_image = False
|
||||
|
||||
for i, (target_img, target_face_list) in enumerate(zip(results, target_faces)):
|
||||
target_face_single, wrong_gender, target_face_index = get_face_single(target_img, target_face_list, face_index=face_num, gender_target=gender_target, order=faces_order[0])
|
||||
|
||||
if target_face_single is not None and wrong_gender == 0:
|
||||
target_used_in_any_image = True
|
||||
source_face_to_use = valid_source_faces[source_face_idx % len(valid_source_faces)]
|
||||
|
||||
result = target_img
|
||||
if face_boost_enabled and "hyperswap" not in model:
|
||||
bgr_fake, M = face_swapper.get(target_img, target_face_single, source_face_to_use, paste_back=False)
|
||||
bgr_fake, scale = restorer.get_restored_face(bgr_fake, face_restore_model, face_restore_visibility, codeformer_weight, interpolation)
|
||||
M *= scale
|
||||
result = swapper.in_swap(target_img, bgr_fake, M)
|
||||
else:
|
||||
logger.info(f"{i}: No target face found for {face_num}")
|
||||
pbar.update(1)
|
||||
elif src_wrong_gender == 1:
|
||||
src_wrong_gender = 0
|
||||
logger.status("Wrong source gender detected")
|
||||
continue
|
||||
else:
|
||||
logger.status(f"No source face found for face number {source_face_idx}.")
|
||||
result = face_swapper.get(target_img, target_face_single, source_face_to_use)
|
||||
|
||||
results[i] = result
|
||||
bbox.append(tuple(map(float, target_face_single.bbox)))
|
||||
swapped_indexes.append(target_face_index)
|
||||
pbar.update(1)
|
||||
|
||||
elif wrong_gender == 1:
|
||||
logger.status("Wrong target gender detected")
|
||||
pbar.update(1)
|
||||
continue
|
||||
else:
|
||||
logger.info(f"{i}: No target face found for {face_num}")
|
||||
pbar.update(1)
|
||||
|
||||
if target_used_in_any_image and len(valid_source_faces) > 1:
|
||||
source_face_idx += 1
|
||||
|
||||
progress_bar_reset(pbar)
|
||||
|
||||
result_images = [Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) for result in results]
|
||||
|
||||
else:
|
||||
logger.status("No source face(s) in the provided Index")
|
||||
else:
|
||||
logger.status("No source face(s) found")
|
||||
return result_images
|
||||
return result_images, bbox, swapped_indexes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
app_title = "ReActor Node for ComfyUI"
|
||||
version_flag = "v0.6.1"
|
||||
version_flag = "v0.7.2-b1"
|
||||
|
||||
COLORS = {
|
||||
"CYAN": "\033[0;36m", # CYAN
|
||||
|
||||
Reference in New Issue
Block a user