37 Commits
Author SHA1 Message Date
Gourieff | 古仁 a12c5b19dc UPD: Comfy Registry 2026-09-21 12:09:43 +07:00
Gourieff | 古仁 ea86b52e0f VersionUP
0.7.1 Beta3
2026-09-21 12:08:12 +07:00
Gourieff | 古仁 02a3518aa7 UPD: DLSS5 third-party note 2026-09-21 12:07:06 +07:00
Gourieff | 古仁 3746860aa4 UPD: Comfy Registry 2026-09-20 13:48:13 +07:00
Gourieff | 古仁 f92ebeb653 VersionUP
0.7.1 Beta2
2026-09-20 13:47:33 +07:00
Gourieff | 古仁 f6776fded7 UPD: DLSS5 auto_mask tooltip 2026-09-20 13:45:32 +07:00
Gourieff | 古仁 7b1233edc0 UPD: Comfy Registry 2026-09-20 13:11:43 +07:00
Gourieff | 古仁 b3a0ac8cf5 UPD: What's new sec -> DLSS5 requirements 2026-09-19 02:07:46 +07:00
Gourieff | 古仁 de7e09db0f UPD: Comfy Registry 2026-09-18 19:59:34 +07:00
Gourieff | 古仁 60b310dd7f UPD: Usage sec 2026-09-18 17:49:46 +07:00
Gourieff | 古仁 4351dc4272 FIX: trailing comma 2026-09-18 17:45:16 +07:00
Gourieff | 古仁 5f68562b6d UPD: What's new sec 2026-09-18 15:14:20 +07:00
Gourieff | 古仁 b9f8683da5 UPD: DLSS5FrameEnhancer Description 2026-09-18 14:25:23 +07:00
Gourieff | 古仁 78c8844f26 VersionUP
0.7.1 Beta1
2026-09-17 13:30:42 +07:00
Gourieff | 古仁 b01880c37f Merge branch 'dlss5-support' into evolve 2026-09-17 13:06:35 +07:00
Gourieff | 古仁 50fdb4993f UPD: Comfy Registry 2026-09-17 13:01:47 +07:00
Gourieff | 古仁 8fe1c0ec6d VersionUP
0.7.0 (Beta passed)
2026-09-17 13:00:49 +07:00
Gourieff | 古仁 619eb16a1e UPD: dlssnr dll README 2026-09-17 12:46:49 +07:00
Gourieff | 古仁 4dab1f1b0a ADD: DLSS5 Enhancer Node 2026-09-17 03:04:22 +07:00
Gourieff | 古仁 a51e88ca24 UPD: Comfy Registry 2026-09-04 19:39:15 +07:00
Gourieff | 古仁 99e23652a2 VersionUP
0.7.0 Beta1
2026-08-30 15:13:57 +07:00
Gourieff | 古仁 0aa14fabb4 UPD: Desktop friendly install.bat
https://codeberg.org/Gourieff/comfyui-reactor-node/issues/52
2026-08-30 15:11:22 +07:00
Gourieff | 古仁 ecd2b3c16d FIX: Typo
Issue #245
2026-08-30 15:03:36 +07:00
Gourieff | 古仁 6ad6b35a4d UPD: Comfy Registry 2026-05-13 00:24:34 +07:00
Gourieff | 古仁 2e34bf8356 VersionUP
0.7.0 Alpha2
2026-05-13 00:20:10 +07:00
Gourieff | 古仁 be42ca37ee FIX: Gender detection face index logic
Issue #234
2026-05-13 00:07:42 +07:00
Gourieff | 古仁 a7628ccdff UPD: Comfy Registry 2026-04-25 15:44:33 +07:00
Gourieff | 古仁 59040c1557 VersionUP
0.7.0 Alpha1
2026-04-25 15:34:34 +07:00
Gourieff | 古仁 e0cd862c3d Merge branch 'cleanup_and_refactor' into evolve 2026-04-25 13:37:08 +07:00
Gourieff | 古仁 5881d3ee2f FIX: Utils Face object link 2026-04-23 23:38:58 +07:00
Gourieff | 古仁 febdf3027a DEL: Monkey-patch
Insightface patcher is no need any more
2026-04-23 23:15:05 +07:00
Gourieff | 古仁 74146c26e0 ADD: New node "Face Similarity" 2026-04-23 17:34:36 +07:00
Gourieff | 古仁 55787ee169 ADD: Hyperswap Class 2026-04-23 16:07:20 +07:00
Gourieff | 古仁 ca24587deb UPD: landmark_3d_68 Math 2026-04-23 13:01:17 +07:00
Gourieff | 古仁 11db21f666 FIX: SFW Score correction
Still safe
2026-04-23 12:54:24 +07:00
Gourieff | 古仁 1356f18bf7 UPD: New ReActor Core
- No Insightface required
- Numpy 1.x 2.x friendly
2026-04-23 01:11:47 +07:00
Gourieff | 古仁 b60036fc85 FIX: HyperSwap CPU Float norm
Issue #183
Contributor: @Buumcode
2026-04-22 15:55:38 +07:00
31 changed files with 1727 additions and 648 deletions
+1
View File
@@ -3,3 +3,4 @@ __pycache__/
.vscode/
example
input
*.dll
+34 -56
View File
@@ -2,10 +2,10 @@
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
![Version](https://img.shields.io/badge/node_version-0.6.2-brightgreen?style=for-the-badge&labelColor=darkgreen)
![Version](https://img.shields.io/badge/node_version-0.7.1_beta3-green?style=for-the-badge&labelColor=darkgreen)
<a href="https://boosty.to/artgourieff" target="_blank">
<img src="https://lovemet.ru/img/boosty.png" width="128" alt="Support Me on Boosty"/>
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Support Me on Boosty"/>
<br>
<sup>
Support This Project
@@ -46,6 +46,26 @@
## What's new in the latest update
### 0.7.1 <sub><sup>BETA1</sup></sub>
- **New `DLSS5 Frame Enhancer` node**. NVIDIA's latest DLSS 5 technology for enhancing image quality (both overall frame or faces by mask). <br><u>See [Installation Instructions](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>The integration operates in an isolated Host mode, fully eliminating conflicts with ComfyUI's CUDA context.<br>Huge thanks to the author of the [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) project for the C++ wrapper (neuroframe_engine.dll and neuroframe_caller.dll), which formed the base of the computational bridge for this node.<br>Also thanks Gemini 3.1 Pro (via [Google Gemini](https://gemini.google.com/app)) for the contribution.</pre><u>Requirements:</u><br>
-- Windows 10/11<br>
-- NVIDIA display driver >= 616.x<br>
-- NVIDIA RTX 40/50-series GPU<br>
(compatibility with older RTX series is unconfirmed)
### 0.7.0
- 💥 **Big Update! ☢ New ReActor Core!**<br>✅ No `Insightface` required!<br>✅ No `C++ Build Tools` required!<br>✅ Instalation process is much easier now!<br>✅ `Numpy 2.x` friendly as well as `1.x`!<br>⚠ <u>A swap result is slightly different now</u>. Hard to say if it’s "better" or "worse" — it’s just a bit different. But if we look at the numbers (cosine similarity of face embeddings), the accuracy is actually a little higher than with Insightface.<br>More info you can find here: https://t.me/reactor_faceswap/55
- New Node "Face Similarity" to check face likeness after you make a swap
- HyperSwap CPU Float normalization fix (thanks @Buumcode, issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
- Fixed: gender detection issue and face index logic (issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
- Comfy Desktop friendly `install.bat`
- Other fixes and improvements
<details>
<summary><a>Previous versions</a></summary>
### 0.6.2
- Added support of HyperSwap models by FaceFusion Labs (thanks [@Buumcode](https://github.com/Buumcode) for contribution)<br>You can download them [here](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>and put them into the `ComfyUI\models\hyperswap` directory
@@ -76,9 +96,6 @@
- Fixes and improvements
<details>
<summary><a>Previous versions</a></summary>
### 0.6.1
- Gender detection better logic for many faces and many indexes
@@ -242,28 +259,22 @@ 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_1a_256.onnx) put them into `ComfyUI\models\hyperswap`
- 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`
@@ -287,9 +298,12 @@ 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)
Connect all required slots and run the query.
@@ -365,55 +379,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 according to Python's version you see in the previous step: [for Python 3.10](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp310-cp310-win_amd64.whl), [for Python 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl), [for Python 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl), [for Python 3.13](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp313-cp313-win_amd64.whl) - and put into ComfyUI root folder if you use ComfyUI Portable
3. Update your PIP:<br>
`python_embeded\python.exe -m pip install -U pip`
4. Then install Insightface:
<br>(for 3.10) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl`
<br>(for 3.11) `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl`
<br>(for 3.12) `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl`
<br>(for 3.13) `python_embeded\python.exe -m pip install insightface-0.7.3-cp313-cp313-win_amd64.whl`
5. 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):
+36 -58
View File
@@ -2,10 +2,10 @@
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_RU.png?raw=true" alt="logo" width="180px"/>
![Version](https://img.shields.io/badge/версия_нода-0.6.2-brightgreen?style=for-the-badge&labelColor=darkgreen)
![Version](https://img.shields.io/badge/версия_нода-0.7.1_beta3-green?style=for-the-badge&labelColor=darkgreen)
<a href="https://boosty.to/artgourieff" target="_blank">
<img src="https://lovemet.ru/img/boosty.png" width="128" alt="Поддержать проект на Boosty"/>
<img src="https://lovemet.ru/img/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
<br>
<sup>
Поддержать проект
@@ -46,6 +46,26 @@
## Что нового в последнем обновлении
### 0.7.1 <sub><sup>BETA1</sup></sub>
- **Новый узел `DLSS5 Frame Enhancer`**. Новейшая технология DLSS 5 от NVIDIA для улучшения качества изображения (кадра целиком или лиц по маске).<br><u>Следуйте [инструкции по установке](https://github.com/Gourieff/ComfyUI-ReActor/blob/main/r_dlssnr/dll/README.md).</u><br><pre>Интеграция работает в изолированном Host-режиме, что полностью исключает конфликты с CUDA-контекстом ComfyUI.<br>Огромная благодарность автору проекта [Merserk/dlss5-visual-enhancer](https://github.com/Merserk/dlss5-visual-enhancer) за C++ обертку (neuroframe_engine.dll и neuroframe_caller.dll), которая легла в основу вычислительного моста для данного узла.<br>Также благодарность Gemini 3.1 Pro (через [Google Gemini](https://gemini.google.com/app)) за ассистирование и помощь.</pre><u>Требования:</u><br>
-- Windows 10/11<br>
-- NVIDIA display driver >= 616.x<br>
-- NVIDIA RTX 40/50-series GPU<br>
(совместимость с более старыми сериями RTX не подтверждена)
### 0.7.0
- 💥 **Важное обновление! ☢ Новое ядро РеАктора!**<br>✅ Библиотека `Insightface` больше не требуется!<br>✅ `C++ Build Tools` больше не требуются!<br>✅ Более простой процесс установки!<br>✅ Поддержка `Numpy 2.x` и `1.x`!<br>⚠ <u>Результат Свапа теперь слегка отличается</u>. Сложно сказать, хуже или лучше — результат просто чуть-чуть другой. По показателям схожести (оценка косинусного сходства векторов эмбеддингов лиц) результат стал немного лучше (хоть и незначительно), чем с Insightface.<br>Подробнее здесь: https://t.me/reactor_faceswap/55
- Новый узел "Face Similarity" для проверки схожести лиц после свапа
- Исправление "HyperSwap CPU Float normalization" (спасибо @Buumcode, Issue [#183](https://github.com/Gourieff/ComfyUI-ReActor/issues/183))
- Исправлено: проблема определения пола и логика индексов лиц (Issue [#234](https://github.com/Gourieff/ComfyUI-ReActor/issues/234))
- Улучшен `install.bat` для поддержки запуска установки в Comfy Desktop
- Прочие улучшение и исправления
<details>
<summary><a>Предыдущие версии</a></summary>
### 0.6.2
- Добавлена поддержка моделей HyperSwap от команды FaceFusion Labs (спасибо [@Buumcode](https://github.com/Buumcode) за вариант реализации)<br>Модели можно скачать [отсюда](https://huggingface.co/facefusion/models-3.3.0/tree/main)<br>(hyperswap_1a_256.onnx, hyperswap_1b_256.onnx, hyperswap_1c_256.onnx)<br>и положить в папку `ComfyUI/models/hyperswap`
@@ -76,9 +96,6 @@
- Исправления и улучшения
<details>
<summary><a>Предыдущие версии</a></summary>
### 0.6.1
- Улучшенная логика работы с индексами множества лиц при определении пола
@@ -243,29 +260,21 @@ 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` или через поиск
</details>
2. Перейдите `ComfyUI\custom_nodes\ComfyUI-ReActor` и запустите `install.bat`, дождитесь окончания установки
3. Скачайте необходимые модели из Раздела "Модели" ниже
4. Запустите ComfyUI и найдите ReActor Node внутри меню `ReActor` или через поиск
## Модели
- 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_1a_256.onnx) скачать в `ComfyUI\models\hyperswap`
- 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`
@@ -291,13 +300,16 @@ 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)
Соедините все необходимые слоты (slots) и запустите очередь (query).
### Входы основного Нода
### Входы основного Узла
- `input_image` - это изображение, на котором надо поменять лицо или лица (целевое изображение, аналог "target image" в версии для SD WebUI);
- Поддерживаемые ноды: "Load Image", "Load Video" или любые другие ноды предоставляющие изображение в качестве выхода;
@@ -310,7 +322,7 @@ Basic workflow [💾](https://github.com/Gourieff/Assets/blob/main/comfyui-react
- `face_boost` - для соединения с ReActorFaceBoost;
- Поддерживаемые ноды: "ReActorFaceBoost";
### Выходы основного Нода
### Выходы основного Узла
- `IMAGE` - выход с готовым изображением (результатом);
- Поддерживаемые ноды: любые ноды с изображением на входе;
@@ -372,53 +384,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, или 3.13
2. Скачайте готовый пакет Insightface в соответствии с версией Python из предыдущего шага: [для Python 3.10](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp310-cp310-win_amd64.whl), [для Python 3.11](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp311-cp311-win_amd64.whl), [для Python 3.12](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl), [для Python 3.13](https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp313-cp313-win_amd64.whl) - и сохраните в корневую директорию ComfyUI, если вы используете ComfyUI Portable
3. Обновите PIP:<br>
`python_embeded\python.exe -m pip install -U pip`
4. Затем установите Insightface:
<br>(для 3.10) `python_embeded\python.exe -m pip install insightface-0.7.3-cp310-cp310-win_amd64.whl`
<br>(для 3.11) `python_embeded\python.exe -m pip install insightface-0.7.3-cp311-cp311-win_amd64.whl`
<br>(для 3.12) `python_embeded\python.exe -m pip install insightface-0.7.3-cp312-cp312-win_amd64.whl`
<br>(для 3.13) `python_embeded\python.exe -m pip install insightface-0.7.3-cp313-cp313-win_amd64.whl`
5. Готово!
### **II. "AttributeError: 'NoneType' object has no attribute 'get'"**
### **I. "AttributeError: 'NoneType' object has no attribute 'get'"**
Эта ошибка появляется, если что-то не так с файлом модели `inswapper_128.onnx`
Скачайте вручную по ссылке [отсюда](https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx)
и сохраните в директорию `ComfyUI\models\insightface`, заменив имеющийся файл
### **III. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
### **II. "reactor.execute() got an unexpected keyword argument 'reference_image'"**
Это означает, что поменялось обозначение входных точек (input points) всвязи с последним обновлением<br>
Удалите из вашего рабочего пространства имеющийся ReActor Node и добавьте его снова
### **IV. ControlNet Aux Node IMPORT failed - при использовании совместно с нодом ReActor**
1. Закройте или остановите ComfyUI сервер, если он запущен
2. Перейдите в корневую папку ComfyUI, откройте консоль CMD и выполните следующее:
- `python_embeded\python.exe -m pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless`
- `python_embeded\python.exe -m pip install opencv-python==4.7.0.72`
3. Готово!
<img src="https://github.com/Gourieff/Assets/blob/main/comfyui-reactor-node/uploads/reactor-w-controlnet.png?raw=true" alt="reactor+controlnet" />
### **V. "ModuleNotFoundError: No module named 'basicsr'" или "subprocess-exited-with-error" при установке пакета future-0.18.3**
- Скачайте https://github.com/Gourieff/Assets/raw/main/comfyui-reactor-node/future-0.18.3-py3-none-any.whl<br>
- Скопируйте файл в корневую папку ComfyUI и выполните в консоли:
python_embeded\python.exe -m pip install future-0.18.3-py3-none-any.whl
- Затем:
python_embeded\python.exe -m pip install basicsr
### **VI. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
### **III. "fatal: fetch-pack: invalid index-pack output" при исполнении команды `git clone`"**
Попробуйте клонировать репозиторий с параметром `--depth=1` (только последний коммит):
+3 -31
View File
@@ -1,39 +1,11 @@
import sys
import os
# Добавляем путь расширения, чтобы Питон видел наши папки (r_facelib, scripts и т.д.)
repo_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, repo_dir)
original_modules = sys.modules.copy()
if repo_dir not in sys.path:
sys.path.insert(0, repo_dir)
# Place aside existing modules if using a1111 web ui
modules_used = [
"modules",
"modules.images",
"modules.processing",
"modules.scripts_postprocessing",
"modules.scripts",
"modules.shared",
]
original_webui_modules = {}
for module in modules_used:
if module in sys.modules:
original_webui_modules[module] = sys.modules.pop(module)
# Proceed with node setup
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Clean up imports
# Remove repo directory from path
sys.path.remove(repo_dir)
# Remove any new modules
modules_to_remove = []
for module in sys.modules:
if module not in original_modules and not module.startswith("google.protobuf") and not module.startswith("onnx") and not module.startswith("cv2"):
modules_to_remove.append(module)
for module in modules_to_remove:
del sys.modules[module]
# Restore original modules
sys.modules.update(original_webui_modules)
+10 -6
View File
@@ -1,22 +1,25 @@
@echo off
setlocal enabledelayedexpansion
:: Try to use embedded python first
if exist ..\..\..\python_embeded\python.exe (
:: Use the embedded python
:: Try to use the Desktop version's venv python first
if exist ..\..\.venv\Scripts\python.exe (
:: Use the ComfyUI Desktop venv python
set PYTHON=..\..\.venv\Scripts\python.exe
) else if exist ..\..\..\python_embeded\python.exe (
:: Use the embedded python (portable version)
set PYTHON=..\..\..\python_embeded\python.exe
) else (
:: Embedded python not found, check for python in the PATH
:: Neither found, check for python in the PATH
for /f "tokens=* USEBACKQ" %%F in (`python --version 2^>^&1`) do (
set PYTHON_VERSION=%%F
)
if errorlevel 1 (
echo I couldn't find an embedded version of Python, nor one in the Windows PATH. Please install manually.
echo I couldn't find a venv python ^(Desktop^), an embedded python ^(Portable^), nor one in the Windows PATH. Please install manually.
pause
exit /b 1
) else (
:: Use python from the PATH (if it's the right version and the user agrees)
echo I couldn't find an embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
echo I couldn't find a venv or embedded version of Python, but I did find !PYTHON_VERSION! in your Windows PATH.
echo Would you like to proceed with the install using that version? (Y/N^)
set /p USE_PYTHON=
if /i "!USE_PYTHON!"=="Y" (
@@ -30,6 +33,7 @@ if exist ..\..\..\python_embeded\python.exe (
)
:: Install the package
echo Using Python: %PYTHON%
echo Installing...
%PYTHON% install.py
echo Done^!
+81 -9
View File
@@ -7,6 +7,7 @@ import torchvision.transforms as T
from torchvision.transforms.functional import normalize
from torchvision.ops import masks_to_boxes
import onnxruntime
import numpy as np
import cv2
import math
@@ -14,12 +15,12 @@ from typing import List
from PIL import Image
import io
from scipy import stats
from insightface.app.common import Face
from reactor_core.face_objects import Face
from segment_anything import sam_model_registry
from modules.processing import ProcessingImg2Img
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,8 @@ import scripts.r_masking.segs as masking_segs
import scripts.reactor_sfw as sfw
from r_dlssnr.dlss5_node import DLSS5FrameEnhancer
models_dir = folder_paths.models_dir
REACTOR_MODELS_PATH = os.path.join(models_dir, "reactor")
@@ -91,6 +94,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)
@@ -423,7 +437,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)
@@ -666,7 +680,7 @@ class ReActorWeight:
images_list: List[Image.Image] = []
apply_patch(1)
apply_log_level(0)
if len(images) > 0:
@@ -762,7 +776,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)
@@ -861,7 +875,7 @@ class SaveFaceModel:
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:
@@ -1640,6 +1654,7 @@ class ReActorFaceBoost:
}
return (face_boost, )
class ReActorUnload:
@classmethod
def INPUT_TYPES(s):
@@ -1658,6 +1673,59 @@ class ReActorUnload:
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"
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,
@@ -1674,9 +1742,11 @@ NODE_CLASS_MAPPINGS = {
# --- Additional Nodes ---
"ReActorRestoreFace": RestoreFace,
"ReActorRestoreFaceAdvanced": RestoreFaceAdvanced,
"ReActorFaceSimilarity": ReActorFaceSimilarity,
"ReActorImageDublicator": ImageDublicator,
"ImageRGBA2RGB": ImageRGBA2RGB,
"ReActorUnload": ReActorUnload,
"DLSS5FrameEnhancer": DLSS5FrameEnhancer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -1695,7 +1765,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
# --- 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",
}
+3 -3
View File
@@ -1,9 +1,9 @@
[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.2"
description = "(SFW-Friendly) The Fast and Simple Face Swap Extension for ComfyUI"
version = "0.7.1-b3"
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"
+8
View File
@@ -0,0 +1,8 @@
## Third-party DLLs
1. Download [neuroframe_dlls.zip](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/neuroframe_dlls.zip) and place `neuroframe_caller.dll`\* and `neuroframe_engine.dll`* here (in `r_dlssnr/dll`)
2. Place your `nvngx_dlssnr.dll`** here (in `r_dlssnr/dll`)
<sub>* Author [Merserk](https://github.com/Merserk), [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-Merserk.txt)
<br>
** Public distribution of this file is prohibited by NVIDIA, [LICENSE](https://huggingface.co/datasets/Gourieff/ReActor/blob/main/DLSSNR/LICENSE-NVIDIA-DLSS.txt)</sub>
+147
View File
@@ -0,0 +1,147 @@
import ctypes
import threading
from dataclasses import dataclass
from typing import Any
import numpy as np
# --- КОНСТАНТЫ ---
BRIDGE_ABI_VERSION = 6
MEMORY_HOST = 0
MEMORY_CUDA = 1
MEMORY_NONE = 2
class NeuralBridgeError(Exception):
pass
# --- C-СТРУКТУРЫ ДЛЯ ВЗАИМОДЕЙСТВИЯ С DLL ---
class RenderParameters(ctypes.Structure):
_fields_ = [
("struct_size", ctypes.c_uint32),
("abi_version", ctypes.c_uint32),
("style", ctypes.c_int32),
("intensity", ctypes.c_float),
("tone", ctypes.c_float),
("structure", ctypes.c_float),
("skin", ctypes.c_float),
("automask", ctypes.c_int32),
("reset", ctypes.c_int32),
("color_strength", ctypes.c_float),
("tone_preservation", ctypes.c_float),
("mask_memory_type", ctypes.c_uint32),
("mask_width", ctypes.c_uint32),
("mask_height", ctypes.c_uint32),
("mask_stride", ctypes.c_uint32),
("mask_plane", ctypes.c_uint64), # Из-за uint64 здесь будет 4 байта системного отступа
("face_skin_protection", ctypes.c_float),
("grain_preservation", ctypes.c_float),
("nr_passes", ctypes.c_int32),
("shimmer_suppression", ctypes.c_float),
("prefer_nvof", ctypes.c_int32),
]
class DLSSStandaloneManager:
def __init__(self, dll_dir: str):
self._lock = threading.RLock()
self._library = None
self.dll_dir = dll_dir
def initialize(self, ordinal: int):
with self._lock:
if self._library is not None:
return True
import os
if hasattr(os, 'add_dll_directory'):
os.add_dll_directory(self.dll_dir)
engine_path = os.path.join(self.dll_dir, "neuroframe_engine.dll")
if not os.path.exists(engine_path):
raise NeuralBridgeError(f"Missing DLL: {engine_path}")
loader = getattr(ctypes, "WinDLL", ctypes.CDLL)
try:
self._library = loader(engine_path)
except OSError as exc:
raise NeuralBridgeError(f"DLL load failed: {exc}")
# Сигнатура инициализации
self._library.dlss5nr_init.argtypes = [
ctypes.c_int, ctypes.c_wchar_p, ctypes.c_char_p, ctypes.c_int
]
self._library.dlss5nr_init.restype = ctypes.c_int
# Сигнатура HOST-рендера (process_v6 вместо process_cuda_v6)
c_float_p = ctypes.POINTER(ctypes.c_float)
self._library.dlss5nr_process_v6.argtypes = [
c_float_p, c_float_p, ctypes.c_int, ctypes.c_int,
ctypes.POINTER(RenderParameters), ctypes.c_char_p, ctypes.c_int
]
self._library.dlss5nr_process_v6.restype = ctypes.c_int
try:
self._library.dlss5nr_frame_abi_version.argtypes = []
self._library.dlss5nr_frame_abi_version.restype = ctypes.c_uint32
self.actual_abi = self._library.dlss5nr_frame_abi_version()
except Exception:
self.actual_abi = BRIDGE_ABI_VERSION
error = ctypes.create_string_buffer(4096)
ok = self._library.dlss5nr_init(ordinal, self.dll_dir, error, len(error))
if not ok:
err_msg = error.value.decode('utf-8', errors='ignore')
raise NeuralBridgeError(f"Bridge initialization failed: {err_msg}")
return True
def process_host(self, source: np.ndarray, destination: np.ndarray, settings: dict, reset: bool, mask: np.ndarray = None):
with self._lock:
error = ctypes.create_string_buffer(4096)
params = RenderParameters()
params.struct_size = ctypes.sizeof(RenderParameters)
params.abi_version = getattr(self, "actual_abi", BRIDGE_ABI_VERSION)
params.style = int(settings.get("style"))
params.intensity = float(settings.get("intensity"))
params.tone = float(settings.get("local_tone"))
params.structure = float(settings.get("local_structure"))
params.skin = float(settings.get("skin_structure"))
params.automask = int(bool(settings.get("auto_mask")))
params.reset = int(bool(reset))
params.color_strength = float(settings.get("color_strength"))
params.tone_preservation = float(settings.get("tone_preservation"))
params.face_skin_protection = float(settings.get("face_skin_protection"))
params.grain_preservation = float(settings.get("grain_preservation"))
params.nr_passes = int(settings.get("nr_passes"))
params.shimmer_suppression = float(settings.get("shimmer_suppression", 0.0))
params.prefer_nvof = int(bool(settings.get("prefer_nvof", False)))
# Обработка маски через HOST память
params.mask_memory_type = MEMORY_NONE
if mask is not None:
params.mask_memory_type = MEMORY_HOST
params.mask_width = int(mask.shape[1])
params.mask_height = int(mask.shape[0])
params.mask_stride = int(mask.strides[0])
params.mask_plane = int(mask.ctypes.data) # Передаем указатель RAM
c_float_p = ctypes.POINTER(ctypes.c_float)
# Вызываем HOST функцию (DLL сама разберется с видеокартой)
ok = self._library.dlss5nr_process_v6(
source.ctypes.data_as(c_float_p),
destination.ctypes.data_as(c_float_p),
source.shape[1],
source.shape[0],
ctypes.byref(params),
error,
len(error)
)
if not ok:
err_msg = error.value.decode('utf-8', errors='ignore')
raise NeuralBridgeError(f"DLSS-5 process failed: {err_msg}")
+113
View File
@@ -0,0 +1,113 @@
import torch
import numpy as np
import os
import comfy.model_management as model_management
from scripts.reactor_logger import logger
from .dlss5_core import DLSSStandaloneManager
from r_modules.shared import state
from reactor_utils import (
batch_tensor_to_pil,
progress_bar,
progress_bar_reset
)
class DLSS5FrameEnhancer:
def __init__(self):
self.device = model_management.get_torch_device()
self.manager = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"style": (["Default", "Nature", "Cinematic"],),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
"local_tone": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 0.0"}),
"local_structure": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05, "tooltip": "0..2, def: 1.0"}),
"skin_structure": ("FLOAT", {"default": 0.5, "min": -1.0, "max": 2.0, "step": 0.05, "tooltip": "-1..2, def: 0.5"}),
"color_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
"tone_preservation": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.5"}),
"face_skin_protection": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
"grain_preservation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "tooltip": "0..1, def: 0.0"}),
"nr_passes": ("INT", {"default": 1, "min": 1, "max": 4, "tooltip": "0..4, def: 1"}),
"auto_mask": ("BOOLEAN", {"default": False, "label_off": "OFF", "label_on": "ON", "tooltip": "AI Automatic masking of complex areas to prevent over-detailing"}),
},
"optional": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("enhanced_image",)
FUNCTION = "enhance"
CATEGORY = "🌌 ReActor"
DESCRIPTION = (
"Requirements:\n"
"- NVIDIA display driver >= 616.x\n"
"- NVIDIA RTX 40/50-series GPU\n"
"(compatibility with older RTX series is unconfirmed)\n"
"- DLLs: neuroframe_caller.dll, neuroframe_engine.dll, nvngx_dlssnr.dll\nin custom_nodes/ComfyUI-ReActor/r_dlssnr/dll\nor custom_nodes/comfyui-reactor-node/r_dlssnr/dll\n"
"(see README.md in dll folder for instructions)"
)
def load_bridge(self):
if self.manager is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
dll_dir = os.path.join(current_dir, "dll")
self.manager = DLSSStandaloneManager(dll_dir)
ordinal = getattr(self.device, 'index', 0) if self.device.index is not None else 0
self.manager.initialize(ordinal)
logger.status(f"DLSS-5 Bridge initialized on GPU {ordinal}")
def enhance(self, image, style, intensity, local_tone, local_structure,
skin_structure, color_strength, tone_preservation,
face_skin_protection, grain_preservation, nr_passes, auto_mask, mask=None):
self.load_bridge()
style_map = {"Default": 0, "Nature": 1, "Cinematic": 2}
settings = {
"style": style_map[style], "intensity": intensity, "local_tone": local_tone,
"local_structure": local_structure, "skin_structure": skin_structure,
"color_strength": color_strength, "tone_preservation": tone_preservation,
"face_skin_protection": face_skin_protection, "grain_preservation": grain_preservation,
"nr_passes": nr_passes, "auto_mask": auto_mask,
"shimmer_suppression": 0.0, "prefer_nvof": False
}
enhanced_batch = []
pil_images = batch_tensor_to_pil(image)
pbar = progress_bar(len(pil_images))
for i in range(len(image)):
if state.interrupted or model_management.processing_interrupted():
logger.status("Interrupted by User")
break
img_np = np.ascontiguousarray(image[i].cpu().numpy().astype(np.float32))
dest_np = np.ascontiguousarray(np.zeros_like(img_np))
mask_np = None
if mask is not None:
mask_np = np.ascontiguousarray(mask[i].cpu().numpy().astype(np.float32))
# Вызываем Host-обработчик (без CUDA конфликтов)
self.manager.process_host(
source=img_np,
destination=dest_np,
settings=settings,
reset=True,
mask=mask_np
)
out_tensor = torch.from_numpy(dest_np).to(self.device)
enhanced_batch.append(out_tensor)
pbar.update(1)
progress_bar_reset(pbar)
return (torch.stack(enhanced_batch),)
+1 -1
View File
@@ -6,7 +6,7 @@ import torch.nn.functional as F
from PIL import Image
from 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
View File
+103
View File
@@ -0,0 +1,103 @@
import os
import zipfile
from reactor_utils import download # Твоя функция скачивания из utils
from .face_objects import Face
from .inswap import SCRFD, ArcFaceONNX, Attribute, Landmark
from scripts.reactor_logger import logger
class ReActorFaceAnalysis:
"""
Главный класс-оркестратор.
Берет картинку, находит лица, определяет пол/возраст и вычисляет эмбеддинги.
"""
def __init__(self, name="buffalo_l", root="./models/insightface", providers=None):
self.name = name
self.root = root
self.providers = providers or ["CPUExecutionProvider"]
self.models = {}
model_dir = os.path.join(root, "models", name)
os.makedirs(model_dir, exist_ok=True)
det_file = os.path.join(model_dir, "det_10g.onnx")
rec_file = os.path.join(model_dir, "w600k_r50.onnx")
attr_file = os.path.join(model_dir, "genderage.onnx")
lmk2d_file = os.path.join(model_dir, "2d106det.onnx")
lmk3d_file = os.path.join(model_dir, "1k3d68.onnx")
# Если файлов нет - качаем архив
if not (os.path.exists(det_file) and os.path.exists(rec_file) and os.path.exists(attr_file)):
zip_url = "https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/buffalo_l.zip"
zip_path = os.path.join(model_dir, f"{name}.zip")
logger.status(f"Downloading {name} models archive...")
download(zip_url, zip_path, f"{name}.zip")
logger.status(f"Extracting {name} models...")
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(model_dir)
logger.status("Extraction completed!")
except zipfile.BadZipFile:
logger.error("Downloaded zip file is corrupted. Please try again.")
finally:
# В любом случае пытаемся удалить архив, чтобы не занимать место
if os.path.exists(zip_path):
os.remove(zip_path)
# Инициализируем только те модели, которые физически есть в папке
if os.path.exists(det_file):
self.models["detection"] = SCRFD(det_file, providers=self.providers)
if os.path.exists(rec_file):
self.models["recognition"] = ArcFaceONNX(rec_file, providers=self.providers)
if os.path.exists(attr_file):
self.models["attribute"] = Attribute(attr_file, providers=self.providers)
if os.path.exists(lmk2d_file):
self.models["landmark_2d"] = Landmark(lmk2d_file, providers=self.providers)
if os.path.exists(lmk3d_file):
self.models["landmark_3d"] = Landmark(lmk3d_file, providers=self.providers)
if "detection" not in self.models:
raise FileNotFoundError(
f"Detection model (det_10g.onnx) not found at {det_file}. "
"Please ensure the buffalo_l models are downloaded and extracted properly."
)
def prepare(self, ctx_id=0, det_size=(640, 640), det_thresh=0.5):
self.det_size = det_size
self.det_thresh = det_thresh
def get(self, img, max_num=0):
bboxes, kpss = self.models["detection"].detect(
img,
det_thresh=self.det_thresh,
input_size=self.det_size,
max_num=max_num
)
if bboxes.shape[0] == 0:
return []
ret = []
for i in range(bboxes.shape[0]):
bbox = bboxes[i, 0:4]
det_score = bboxes[i, 4]
kps = kpss[i] if kpss is not None else None
face = Face(bbox=bbox, kps=kps, det_score=det_score)
if "attribute" in self.models:
self.models["attribute"].get(img, face)
if "recognition" in self.models:
self.models["recognition"].get(img, face)
if "landmark_2d" in self.models:
self.models["landmark_2d"].get(img, face)
if "landmark_3d" in self.models:
self.models["landmark_3d"].get(img, face)
ret.append(face)
return ret
+82
View File
@@ -0,0 +1,82 @@
import numpy as np
import onnxruntime as ort
import os
class Face(dict):
"""
Класс-хранилище данных о лице.
Наследуется от dict для полной обратной совместимости с кодом,
который ожидает доступ по ключу (например, face['bbox']).
"""
def __init__(self, d=None, **kwargs):
if d is None:
d = {}
if kwargs:
d.update(**kwargs)
for k, v in d.items():
setattr(self, k, v)
# Инициализируем родительский словарь
super().__init__(d)
def __setattr__(self, name, value):
# Если это массив, делаем копию, чтобы избежать багов с мутацией по ссылке
if isinstance(value, (list, tuple)):
value = [x for x in value]
elif isinstance(value, np.ndarray):
value = value.copy()
super().__setattr__(name, value)
super().__setitem__(name, value)
def __setitem__(self, key, value):
super().__setitem__(key, value)
super().__setattr__(key, value)
@property
def sex(self):
"""Возвращает 'M' или 'F' на основе числового значения gender"""
gender = self.get('gender', None)
if gender is None:
return None
return 'M' if gender == 1 else 'F'
@property
def normed_embedding(self):
"""Автоматически нормализует эмбеддинг для ArcFace / INSwapper"""
embedding = self.get('embedding', None)
if embedding is None:
return None
norm = np.linalg.norm(embedding)
if norm == 0:
return embedding
return embedding / norm
class BaseONNXModel:
"""
Базовый класс для всех моделей (Детектор, ArcFace, INSwapper).
Берет на себя рутину по открытию сессий и чтению входов/выходов.
"""
def __init__(self, model_file, providers=None):
self.model_file = model_file
self.providers = providers or ["CPUExecutionProvider"]
if not os.path.exists(self.model_file):
raise FileNotFoundError(f"Model file not found: {self.model_file}")
self.session = ort.InferenceSession(self.model_file, providers=self.providers)
# Получаем параметры входов
self.inputs = self.session.get_inputs()
self.input_names = [inp.name for inp in self.inputs]
# Обычно нас интересует шейп первого входа (например, батч, каналы, высота, ширина)
self.input_shape = self.inputs[0].shape
# Получаем параметры выходов
self.outputs = self.session.get_outputs()
self.output_names = [out.name for out in self.outputs]
def forward(self, *args, **kwargs):
"""Этот метод будет переопределен в классах-наследниках"""
raise NotImplementedError("Forward method must be implemented by subclasses.")
+181
View File
@@ -0,0 +1,181 @@
import cv2
import numpy as np
from .face_objects import BaseONNXModel
class HyperSwapper(BaseONNXModel):
"""Класс для работы с моделями семейства Hyperswap"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
# Функция для получения 5 ключевых точек из объекта Face
def get_landmarks_5(self, face):
if hasattr(face, 'landmark_5') and face.landmark_5 is not None:
return face.landmark_5
elif hasattr(face, 'kps') and face.kps is not None:
return face.kps
elif hasattr(face, 'landmark') and face.landmark is not None:
if face.landmark.shape[0] >= 68:
idxs = [36, 45, 30, 48, 54]
return face.landmark[idxs]
return None
# Функция для вычисления аффинного преобразования
def get_affine_transform(self, src_pts, dst_pts):
M, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
return M
# Создаём градиентную маску овальной формы без обрезки
def create_gradient_mask(self, crop_size=256):
# 1. Создаём пустую маску (все пиксели = 0)
mask = np.zeros((crop_size, crop_size), dtype=np.float32)
# 2. Определяем центр и размеры эллипса
center = (crop_size // 2, crop_size // 2)
axes = (int(crop_size * 0.35), int(crop_size * 0.4))
# 3. Рисуем эллипс (заполняем белым цветом, значение=1.0)
cv2.ellipse(
mask, # Массив для рисования
center, # Центр эллипса
axes, # Полуоси (ширина, высота)
angle=0, # Угол поворота
startAngle=0, # Начальный угол дуги
endAngle=360, # Конечный угол дуги (360 = полный эллипс)
color=1.0, # Значение для заполнения (белый = 1.0)
thickness=-1 # -1 = заполнить всю область эллипса
)
# 4. Применяем размытие для плавных краёв
blur_ksize = 15 # Нечётное число, чтобы ядро было симметричным
mask = cv2.GaussianBlur(mask, (blur_ksize, blur_ksize), 0)
# 5. Ограничим значения в диапазоне [0, 1]
mask = np.clip(mask, 0, 1)
return mask
def paste_back(self, target_img, swapped_face, M, crop_size=256):
# 1. Создание мягкой маски (Эрозия + Размытие)
mask = self.create_gradient_mask(crop_size)
# Преобразуем в трехканальную маску
mask_3c = np.stack([mask] * 3, axis=2)
# 2. Получаем размеры целевого изображения
h, w = target_img.shape[:2]
# 3. Нормализация swapped_face к float32 [0,1] для warp
swapped_face_norm = swapped_face.astype(np.float32) / 255.0
mask_norm = mask_3c.astype(np.float32) # Маска уже [0,1]
# 4. Обратное преобразование (WARP_INVERSE_MAP) для лица И маски
# Используем BORDER_CONSTANT с borderValue=0.5 (серый, чтобы избежать синих/зеленых артефактов)
warped_face = cv2.warpAffine(
swapped_face_norm,
M,
(w, h),
flags=cv2.INTER_LANCZOS4 | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0.5
)
# Для маски (INTER_CUBIC — плавные границы)
warped_mask = cv2.warpAffine(
mask_norm,
M,
(w, h),
flags=cv2.INTER_CUBIC | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_CONSTANT,
borderValue=0.0 # Маска: 0 за пределами
)
# 5. Обработка после warp: Clip, NaN fix
warped_face = np.clip(warped_face, 0, 1) # Убираем отрицательные
warped_face = np.nan_to_num(warped_face, nan=0.5) # NaN -> серый
warped_mask = np.clip(warped_mask, 0, 1)
warped_mask = np.nan_to_num(warped_mask, nan=0.0)
# 6. Дополнительное размытие для устранения артефактов
warped_mask = cv2.GaussianBlur(warped_mask, (3, 3), 0)
# 7. Плавное наложение в float32
target_float = target_img.astype(np.float32) / 255.0
result_float = target_float * (1.0 - warped_mask) + warped_face * warped_mask
# 8. Обратная нормализация к uint8
result = (result_float * 255).clip(0, 255).astype(np.uint8)
return result
def visualize_points(self, img, points, color=(0, 255, 0)):
img = img.copy()
for p in points:
cv2.circle(img, tuple(p.astype(int)), 3, color, -1)
# Итоговая функция run_hyperswap (get) с аффинным преобразованием
def get(self, img, target_face, source_face, paste_back=True):
# 1. Подготовка эмбеддинга
source_embedding = source_face.normed_embedding.reshape(1, -1).astype(np.float32)
# 2. Получаем 5 точек target
target_landmarks_5 = self.get_landmarks_5(target_face)
# self.visualize_points(img, target_landmarks_5, (0, 255, 0)) # не для продакшена
if target_landmarks_5 is None:
return img if paste_back else (None, None)
# 3. Определение эталонных точек для выравнивания 256x256 (FFHQ Alignment)
std_landmarks_256 = np.array([
[ 84.87, 105.94], # Левый глаз
[171.13, 105.94], # Правый глаз
[128.00, 146.66], # Кончик носа
[ 96.95, 188.64], # Левый уголок рта
[159.05, 188.64] # Правый уголок рта
], dtype=np.float32)
# Вычисляем аффинную матрицу
M = self.get_affine_transform(target_landmarks_5.astype(np.float32), std_landmarks_256)
# Применяем аффинное преобразование с новой матрицей M
crop = cv2.warpAffine(img, M, (256, 256), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
# 4. Преобразуем crop для модели
crop_input = crop[:, :, ::-1].astype(np.float32) / 255.0 # RGB -> [0,1]
crop_input = (crop_input - 0.5) / 0.5 # Нормализация
crop_input = crop_input.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32)
# 5. Инференс
try:
output = self.session.run(None, {'source': source_embedding, 'target': crop_input})[0][0]
except:
return img if paste_back else (None, None)
if isinstance(output, np.ndarray):
# устранение NaN и бесконечностей
output = np.nan_to_num(output, nan=0.0, posinf=255.0, neginf=0.0)
# если диапазон похож на [-1,1] → нормализуем в [0,255]
if output.min() < 0.0 or output.max() <= 1.5:
output = ((output + 1.0) / 2.0 * 255.0)
# жёсткое ограничение диапазона и тип для OpenCV
output = np.clip(output, 0, 255).astype(np.uint8).copy()
# защита от повторного использования буфера (inplace CPU bug)
try:
output.setflags(write=True)
except Exception:
pass
# 6. Обратная нормализация
output = output.transpose(1, 2, 0) # CHW -> HWC
output = output[:, :, ::-1] # BGR -> RGB
# 7. Возвращаем результат в зависимости от флага paste_back
if not paste_back:
return output, M # Возвращаем только кроп лица (256x256) и матрицу M
# Если нужна полная вклейка в исходное изображение:
return self.paste_back(img, output, M, crop_size=256)
+440
View File
@@ -0,0 +1,440 @@
import cv2
import numpy as np
from .face_objects import BaseONNXModel
from .meanshape_68 import MEANSHAPE_68
# --- Математика для 3D позы ---
def estimate_affine_matrix_3d23d(X, Y):
''' Вычисляет аффинную матрицу трансформации 3D -> 3D '''
X_homo = np.hstack((X, np.ones([X.shape[0], 1])))
P = np.linalg.lstsq(X_homo, Y, rcond=None)[0].T
return P
def P2sRt(P):
''' Разбивает матрицу проекции '''
t1 = np.linalg.norm(P[:,0])
t2 = np.linalg.norm(P[:,1])
t3 = np.linalg.norm(P[:,2])
s = (t1 + t2 + t3) / 3.0
P1 = P / s
R = P1[:, 0:3]
t = P1[:, 3]
return s, R, t
def matrix2angle(R):
''' Превращает матрицу поворота в углы Эйлера (pitch, yaw, roll) '''
if R[2,0] != 1 and R[2,0] != -1:
pitch = -np.arcsin(R[2,0])
yaw = np.arctan2(R[2,1]/np.cos(pitch), R[2,2]/np.cos(pitch))
roll = np.arctan2(R[1,0]/np.cos(pitch), R[0,0]/np.cos(pitch))
else:
yaw = 0
if R[2,0] == -1:
pitch = np.pi/2
roll = yaw + np.arctan2(R[0,1], R[0,2])
else:
pitch = -np.pi/2
roll = -yaw + np.arctan2(-R[0,1], -R[0,2])
return pitch, yaw, roll
# --- Вспомогательные функции ---
def distance2bbox(points, distance, max_shape=None):
x1 = points[:, 0] - distance[:, 0]
y1 = points[:, 1] - distance[:, 1]
x2 = points[:, 0] + distance[:, 2]
y2 = points[:, 1] + distance[:, 3]
if max_shape is not None:
x1 = np.clip(x1, 0, max_shape[1])
y1 = np.clip(y1, 0, max_shape[0])
x2 = np.clip(x2, 0, max_shape[1])
y2 = np.clip(y2, 0, max_shape[0])
return np.stack([x1, y1, x2, y2], axis=-1)
def distance2kps(points, distance, max_shape=None):
preds = []
for i in range(0, distance.shape[1], 2):
px = points[:, i%2] + distance[:, i]
py = points[:, i%2+1] + distance[:, i+1]
if max_shape is not None:
px = np.clip(px, 0, max_shape[1])
py = np.clip(py, 0, max_shape[0])
preds.append(px)
preds.append(py)
return np.stack(preds, axis=-1)
# Стандартные точки FFHQ/ArcFace для матрицы трансформации 112x112
ARCFACE_STD_POINTS = np.array([
[38.2946, 51.6963],
[73.5318, 51.5014],
[56.0252, 71.7366],
[41.5493, 92.3655],
[70.7299, 92.2041]
], dtype=np.float32)
def norm_crop(img, landmark, image_size=112):
"""Выравнивает и обрезает лицо (для ArcFace)"""
M, _ = cv2.estimateAffinePartial2D(landmark, ARCFACE_STD_POINTS)
warped = cv2.warpAffine(img, M, (image_size, image_size), borderValue=0.0)
return warped
# --- Модели ---
class SCRFD(BaseONNXModel):
"""Детектор лиц (находит bbox и 5 ключевых точек)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.batched = len(self.outputs[0].shape) == 3
self.input_mean = 127.5
self.input_std = 128.0
self.use_kps = len(self.outputs) in [9, 15]
self.fmc = 5 if len(self.outputs) in [10, 15] else 3
self._feat_stride_fpn = [8, 16, 32, 64, 128][:self.fmc]
self._num_anchors = 2 if self.fmc == 3 else 1
self.center_cache = {}
def forward(self, img, threshold):
scores_list, bboxes_list, kpss_list = [], [], []
input_size = tuple(img.shape[0:2][::-1])
blob = cv2.dnn.blobFromImage(img, 1.0/self.input_std, input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
net_outs = self.session.run(self.output_names, {self.input_names[0]: blob})
input_height, input_width = blob.shape[2], blob.shape[3]
for idx, stride in enumerate(self._feat_stride_fpn):
if self.batched:
scores = net_outs[idx][0]
bbox_preds = net_outs[idx + self.fmc][0] * stride
if self.use_kps:
kps_preds = net_outs[idx + self.fmc * 2][0] * stride
else:
scores = net_outs[idx]
bbox_preds = net_outs[idx + self.fmc] * stride
if self.use_kps:
kps_preds = net_outs[idx + self.fmc * 2] * stride
height, width = input_height // stride, input_width // stride
key = (height, width, stride)
if key in self.center_cache:
anchor_centers = self.center_cache[key]
else:
anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
anchor_centers = (anchor_centers * stride).reshape((-1, 2))
if self._num_anchors > 1:
anchor_centers = np.stack([anchor_centers] * self._num_anchors, axis=1).reshape((-1, 2))
if len(self.center_cache) < 100:
self.center_cache[key] = anchor_centers
pos_inds = np.where(scores >= threshold)[0]
bboxes = distance2bbox(anchor_centers, bbox_preds)
scores_list.append(scores[pos_inds])
bboxes_list.append(bboxes[pos_inds])
if self.use_kps:
kpss = distance2kps(anchor_centers, kps_preds)
kpss = kpss.reshape((kpss.shape[0], -1, 2))
kpss_list.append(kpss[pos_inds])
return scores_list, bboxes_list, kpss_list
def detect(self, img, det_thresh=0.5, input_size=(640, 640), max_num=0):
im_ratio = float(img.shape[0]) / img.shape[1]
model_ratio = float(input_size[1]) / input_size[0]
if im_ratio > model_ratio:
new_height = input_size[1]
new_width = int(new_height / im_ratio)
else:
new_width = input_size[0]
new_height = int(new_width * im_ratio)
det_scale = float(new_height) / img.shape[0]
resized_img = cv2.resize(img, (new_width, new_height))
det_img = np.zeros((input_size[1], input_size[0], 3), dtype=np.uint8)
det_img[:new_height, :new_width, :] = resized_img
scores_list, bboxes_list, kpss_list = self.forward(det_img, det_thresh)
scores = np.vstack(scores_list).ravel()
order = scores.argsort()[::-1]
bboxes = np.vstack(bboxes_list) / det_scale
if self.use_kps:
kpss = np.vstack(kpss_list) / det_scale
pre_det = np.hstack((bboxes, scores[:, None])).astype(np.float32, copy=False)
pre_det = pre_det[order, :]
keep = self.nms(pre_det)
det = pre_det[keep, :]
kpss = kpss[order, :, :][keep, :, :] if self.use_kps else None
if max_num > 0 and det.shape[0] > max_num:
area = (det[:, 2] - det[:, 0]) * (det[:, 3] - det[:, 1])
img_center = img.shape[0] // 2, img.shape[1] // 2
offsets = np.vstack([
(det[:, 0] + det[:, 2]) / 2 - img_center[1],
(det[:, 1] + det[:, 3]) / 2 - img_center[0]
])
offset_dist_squared = np.sum(np.power(offsets, 2.0), 0)
values = area - offset_dist_squared * 2.0
bindex = np.argsort(values)[::-1][:max_num]
det = det[bindex, :]
if kpss is not None:
kpss = kpss[bindex, :]
return det, kpss
def nms(self, dets, nms_thresh=0.4):
x1, y1, x2, y2, scores = dets[:, 0], dets[:, 1], dets[:, 2], dets[:, 3], dets[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= nms_thresh)[0]
order = order[inds + 1]
return keep
class ArcFaceONNX(BaseONNXModel):
"""Распознаватель лиц (выдает вектор/эмбеддинг)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 127.5
self.input_std = 127.5
self.input_size = tuple(self.input_shape[2:4][::-1])
def get(self, img, face):
aimg = norm_crop(img, landmark=face.kps, image_size=self.input_size[0])
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
net_out = self.session.run(self.output_names, {self.input_names[0]: blob})[0]
face.embedding = net_out.flatten()
return face.embedding
class Attribute(BaseONNXModel):
"""Анализатор атрибутов (выдает пол и возраст)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 0.0
self.input_std = 1.0
self.input_size = tuple(self.input_shape[2:4][::-1])
def get(self, img, face):
bbox = face.bbox
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
_scale = self.input_size[0] / (max(w, h) * 1.5)
# Простая трансформация для Attribute (не требует 5 точек, только центр и масштаб)
M = np.array([
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
], dtype=np.float32)
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
# Получаем гендер и возраст
gender = int(np.argmax(pred[:2]))
age = int(np.round(pred[2] * 100))
face.gender = gender
face.age = age
return gender, age
class INSwapper(BaseONNXModel):
"""Свопер лиц (модели inswapper_128, reswapper)"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 0.0
self.input_std = 255.0
self.input_size = tuple(self.input_shape[2:4][::-1])
# Хак для экономии памяти: импортируем onnx только здесь,
# читаем нужную матрицу emap и сразу выгружаем тяжелую модель из RAM.
import onnx
from onnx import numpy_helper
model = onnx.load(self.model_file, load_external_data=False)
self.emap = numpy_helper.to_array(model.graph.initializer[-1])
del model
def get(self, img, target_face, source_face, paste_back=True):
# 1. Идеальное позиционирование (1 в 1 как в оригинальном C++ Insightface)
# ВАЖНО: Insightface центрирует лицо для INSwapper ТОЛЬКО по оси X!
# По оси Y оно остается прижатым выше, сохраняя оригинальные пропорции.
ratio = float(self.input_size[0]) / 128.0
diff_x = 8.0 * ratio
src_pts = ARCFACE_STD_POINTS.copy() * ratio
src_pts[:, 0] += diff_x # Смещаем ТОЛЬКО координаты X!
# 2. Вычисляем аффинную матрицу родным методом OpenCV
M, _ = cv2.estimateAffinePartial2D(target_face.kps, src_pts)
# 3. Кропаем и выравниваем лицо
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
# 4. Подготавливаем эмбеддинг донора
latent = source_face.normed_embedding.reshape((1, -1))
latent = np.dot(latent, self.emap)
latent /= np.linalg.norm(latent)
# 5. Инференс
pred = self.session.run(self.output_names, {
self.input_names[0]: blob,
self.input_names[1]: latent.astype(np.float32)
})[0]
img_fake = pred.transpose((0, 2, 3, 1))[0]
bgr_fake = np.clip(255 * img_fake, 0, 255).astype(np.uint8)[:, :, ::-1]
if not paste_back:
return bgr_fake, M
# 6. Обратная вклейка (Paste Back)
target_img = img
fake_diff = bgr_fake.astype(np.float32) - aimg.astype(np.float32)
fake_diff = np.abs(fake_diff).mean(axis=2)
# Обрезаем края
fake_diff[:2, :] = 0
fake_diff[-2:, :] = 0
fake_diff[:, :2] = 0
fake_diff[:, -2:] = 0
IM = cv2.invertAffineTransform(M)
img_white = np.full((aimg.shape[0], aimg.shape[1]), 255, dtype=np.float32)
# Возвращаем в исходную перспективу
bgr_fake_warped = cv2.warpAffine(bgr_fake, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white_warped = cv2.warpAffine(img_white, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
fake_diff_warped = cv2.warpAffine(fake_diff, IM, (target_img.shape[1], target_img.shape[0]), borderValue=0.0)
img_white_warped[img_white_warped > 20] = 255
fthresh = 10
fake_diff_warped[fake_diff_warped < fthresh] = 0
fake_diff_warped[fake_diff_warped >= fthresh] = 255
img_mask = img_white_warped
mask_h_inds, mask_w_inds = np.where(img_mask == 255)
# Защита от пустой маски
if len(mask_h_inds) > 0 and len(mask_w_inds) > 0:
mask_h = np.max(mask_h_inds) - np.min(mask_h_inds)
mask_w = np.max(mask_w_inds) - np.min(mask_w_inds)
mask_size = int(np.sqrt(mask_h * mask_w))
k = max(mask_size // 10, 10)
kernel = np.ones((k, k), np.uint8)
img_mask = cv2.erode(img_mask, kernel, iterations=1)
kernel = np.ones((2, 2), np.uint8)
fake_diff_warped = cv2.dilate(fake_diff_warped, kernel, iterations=1)
k = max(mask_size // 20, 5)
blur_size = (k * 2 + 1, k * 2 + 1)
img_mask = cv2.GaussianBlur(img_mask, blur_size, 0)
k = 5
blur_size = (k * 2 + 1, k * 2 + 1)
fake_diff_warped = cv2.GaussianBlur(fake_diff_warped, blur_size, 0)
img_mask /= 255.0
img_mask = np.reshape(img_mask, [img_mask.shape[0], img_mask.shape[1], 1])
fake_merged = img_mask * bgr_fake_warped + (1.0 - img_mask) * target_img.astype(np.float32)
return fake_merged.astype(np.uint8)
class Landmark(BaseONNXModel):
"""Извлекает 106 (2D) или 68 (3D) точек лица"""
def __init__(self, model_file, providers=None):
super().__init__(model_file, providers)
self.input_mean = 127.5
self.input_std = 128.0
self.input_size = tuple(self.input_shape[2:4][::-1])
output_shape = self.outputs[0].shape
# Определяем, какая это модель (3D или 2D) по размеру выхода
if output_shape[1] == 3309:
self.lmk_dim = 3
self.lmk_num = 68
self.taskname = 'landmark_3d_68'
else:
self.lmk_dim = 2
self.lmk_num = output_shape[1] // self.lmk_dim
self.taskname = f'landmark_2d_{self.lmk_num}'
def get(self, img, face):
bbox = face.bbox
w, h = (bbox[2] - bbox[0]), (bbox[3] - bbox[1])
center = ((bbox[2] + bbox[0]) / 2, (bbox[3] + bbox[1]) / 2)
_scale = self.input_size[0] / (max(w, h) * 1.5)
# Матрица трансформации (выравнивание по центру bbox)
M = np.array([
[_scale, 0, self.input_size[0] * 0.5 - center[0] * _scale],
[0, _scale, self.input_size[1] * 0.5 - center[1] * _scale]
], dtype=np.float32)
aimg = cv2.warpAffine(img, M, self.input_size, borderValue=0.0)
blob = cv2.dnn.blobFromImage(aimg, 1.0 / self.input_std, self.input_size,
(self.input_mean, self.input_mean, self.input_mean), swapRB=True)
pred = self.session.run(self.output_names, {self.input_names[0]: blob})[0][0]
if pred.shape[0] >= 3000:
pred = pred.reshape((-1, 3))
else:
pred = pred.reshape((-1, 2))
if self.lmk_num < pred.shape[0]:
pred = pred[-self.lmk_num:, :]
# Денормализация точек в размер модели
pred[:, 0:2] += 1
pred[:, 0:2] *= (self.input_size[0] // 2)
if pred.shape[1] == 3:
pred[:, 2] *= (self.input_size[0] // 2)
# Обратная трансформация точек на оригинальное изображение
IM = cv2.invertAffineTransform(M)
pred_xy = pred[:, 0:2]
pred_xy = np.hstack((pred_xy, np.ones((pred_xy.shape[0], 1)))) # Добавляем гомогенную координату
pred_xy = np.dot(IM, pred_xy.T).T
if pred.shape[1] == 3:
pred = np.hstack((pred_xy, pred[:, 2:3])) # Возвращаем Z
else:
pred = pred_xy
# Сохраняем в объект Face под правильным именем
setattr(face, self.taskname, pred)
# Честный расчет 3D позы
if self.taskname == 'landmark_3d_68':
P = estimate_affine_matrix_3d23d(MEANSHAPE_68, pred)
_, R, _ = P2sRt(P)
rx, ry, rz = matrix2angle(R)
face.pose = np.array([rx, ry, rz], dtype=np.float32)
return pred
+348
View File
@@ -0,0 +1,348 @@
import numpy as np
# эталонная 3D-модель "усредненного" человеческого лица (68 точек в 3D-пространстве)
matrix = [
[
-0.6266950368881226,
-0.2926996946334839,
-0.3140018582344055
],
[
-0.5996649265289307,
-0.12250272184610367,
-0.2924409508705139
],
[
-0.571026086807251,
0.05118739977478981,
-0.2549465000629425
],
[
-0.5338566899299622,
0.21284838020801544,
-0.1866333782672882
],
[
-0.4797332286834717,
0.3506459593772888,
-0.047699932008981705
],
[
-0.3957556486129761,
0.4465116560459137,
0.07328663021326065
],
[
-0.29880842566490173,
0.5106605887413025,
0.17977793514728546
],
[
-0.18838681280612946,
0.554440438747406,
0.3166600465774536
],
[
0.0014708322705700994,
0.584439218044281,
0.38841578364372253
],
[
0.19099053740501404,
0.5517070889472961,
0.31433114409446716
],
[
0.3269285559654236,
0.48957568407058716,
0.16839952766895294
],
[
0.4400261342525482,
0.4023579955101013,
0.03596242889761925
],
[
0.5068787336349487,
0.3116249442100525,
-0.09476063400506973
],
[
0.540894627571106,
0.20452618598937988,
-0.20267142355442047
],
[
0.574118435382843,
0.04570329561829567,
-0.2841764986515045
],
[
0.5991416573524475,
-0.14585931599140167,
-0.296495646238327
],
[
0.6275436282157898,
-0.30774804949760437,
-0.30199483036994934
],
[
-0.47466811537742615,
-0.4376046359539032,
0.23648710548877716
],
[
-0.4166599214076996,
-0.4717560112476349,
0.3159925639629364
],
[
-0.3475387990474701,
-0.4840780198574066,
0.366113543510437
],
[
-0.26063987612724304,
-0.4763909876346588,
0.3992317318916321
],
[
-0.16712301969528198,
-0.45777544379234314,
0.4166091978549957
],
[
0.1231740415096283,
-0.45874011516571045,
0.4250619411468506
],
[
0.2063615769147873,
-0.4804193377494812,
0.41578391194343567
],
[
0.28667303919792175,
-0.4901494085788727,
0.3919537663459778
],
[
0.3623969256877899,
-0.4768601655960083,
0.3527863025665283
],
[
0.425568163394928,
-0.4500580430030823,
0.2953187823295593
],
[
-0.007627937477082014,
-0.3230886459350586,
0.46194377541542053
],
[
-0.007876846939325333,
-0.255738765001297,
0.5104694366455078
],
[
-0.007687545381486416,
-0.19917990267276764,
0.5525456666946411
],
[
-0.007345120422542095,
-0.14261691272258759,
0.5986667275428772
],
[
-0.144961878657341,
0.033126939088106155,
0.4196909964084625
],
[
-0.08434253931045532,
0.03127360716462135,
0.4731740951538086
],
[
-0.005499685648828745,
0.03975145146250725,
0.5146695971488953
],
[
0.06346292793750763,
0.04613516479730606,
0.4792240560054779
],
[
0.13398391008377075,
0.02204025723040104,
0.41907867789268494
],
[
-0.38675081729888916,
-0.3133975565433502,
0.259631872177124
],
[
-0.3166607916355133,
-0.35007160902023315,
0.3285270035266876
],
[
-0.2341379076242447,
-0.35491427779197693,
0.3334933817386627
],
[
-0.15516234934329987,
-0.31524932384490967,
0.3143281042575836
],
[
-0.23092176020145416,
-0.28427040576934814,
0.3255828619003296
],
[
-0.3175090253353119,
-0.28516536951065063,
0.3098825216293335
],
[
0.13895708322525024,
-0.30982404947280884,
0.3182835876941681
],
[
0.21945592761039734,
-0.35319215059280396,
0.33802759647369385
],
[
0.30174651741981506,
-0.349665105342865,
0.333102285861969
],
[
0.37665316462516785,
-0.31351813673973083,
0.26322856545448303
],
[
0.2966947853565216,
-0.2871439754962921,
0.3220130503177643
],
[
0.21462441980838776,
-0.2905276417732239,
0.33124178647994995
],
[
-0.20143844187259674,
0.23736143112182617,
0.37953662872314453
],
[
-0.13732077181339264,
0.18578563630580902,
0.46525245904922485
],
[
-0.07648587226867676,
0.15119342505931854,
0.5035716891288757
],
[
-0.002535885199904442,
0.16872699558734894,
0.5164376497268677
],
[
0.06442102789878845,
0.15088020265102386,
0.5045241117477417
],
[
0.126465305685997,
0.17947596311569214,
0.4685916602611542
],
[
0.21824784576892853,
0.23899227380752563,
0.37567368149757385
],
[
0.13288259506225586,
0.28392839431762695,
0.4400508999824524
],
[
0.06802233308553696,
0.2973543107509613,
0.4774041771888733
],
[
-0.0004690653004217893,
0.30004069209098816,
0.4871094226837158
],
[
-0.06934267282485962,
0.29696860909461975,
0.4805404841899872
],
[
-0.14252015948295593,
0.2743033170700073,
0.43808019161224365
],
[
-0.1781347393989563,
0.23059049248695374,
0.3963589668273926
],
[
-0.07403063774108887,
0.2147187888622284,
0.4653262794017792
],
[
-0.002636224264279008,
0.21414154767990112,
0.4832296073436737
],
[
0.05981616675853729,
0.21076396107673645,
0.47224411368370056
],
[
0.16690002381801605,
0.23127099871635437,
0.396789014339447
],
[
0.059809282422065735,
0.22376428544521332,
0.46641337871551514
],
[
-0.0014343614457175136,
0.2257590889930725,
0.4752439558506012
],
[
-0.07522077113389969,
0.23065608739852905,
0.4671475291252136
]
]
MEANSHAPE_68 = np.array(matrix, dtype=np.float32)
-161
View File
@@ -1,161 +0,0 @@
import os.path as osp
import glob
import logging
import insightface
from insightface.model_zoo.model_zoo import ModelRouter, PickableInferenceSession
from insightface.model_zoo.retinaface import RetinaFace
from insightface.model_zoo.landmark import Landmark
from insightface.model_zoo.attribute import Attribute
from insightface.model_zoo.inswapper import INSwapper
from insightface.model_zoo.arcface_onnx import ArcFaceONNX
from insightface.app import FaceAnalysis
from insightface.utils import DEFAULT_MP_NAME, ensure_available
from insightface.model_zoo import model_zoo
import onnxruntime
import onnx
from onnx import numpy_helper
from scripts.reactor_logger import logger
def patched_get_model_log(self, **kwargs):
session = PickableInferenceSession(self.onnx_file, **kwargs)
print(f'Applied providers: {session._providers}, with options: {session._provider_options}')
inputs = session.get_inputs()
input_cfg = inputs[0]
input_shape = input_cfg.shape
outputs = session.get_outputs()
if len(outputs) >= 5:
return RetinaFace(model_file=self.onnx_file, session=session)
elif input_shape[2] == 192 and input_shape[3] == 192:
return Landmark(model_file=self.onnx_file, session=session)
elif input_shape[2] == 96 and input_shape[3] == 96:
return Attribute(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
return INSwapper(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
return INSwapper(model_file=self.onnx_file, session=session)
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
return ArcFaceONNX(model_file=self.onnx_file, session=session)
else:
return None
def patched_get_model(self, **kwargs):
session = PickableInferenceSession(self.onnx_file, **kwargs)
inputs = session.get_inputs()
input_cfg = inputs[0]
input_shape = input_cfg.shape
outputs = session.get_outputs()
if len(outputs) >= 5:
return RetinaFace(model_file=self.onnx_file, session=session)
elif input_shape[2] == 192 and input_shape[3] == 192:
return Landmark(model_file=self.onnx_file, session=session)
elif input_shape[2] == 96 and input_shape[3] == 96:
return Attribute(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 128 and input_shape[3] == 128:
return INSwapper(model_file=self.onnx_file, session=session)
elif len(inputs) == 2 and input_shape[2] == 256 and input_shape[3] == 256:
return INSwapper(model_file=self.onnx_file, session=session)
elif input_shape[2] == input_shape[3] and input_shape[2] >= 112 and input_shape[2] % 16 == 0:
return ArcFaceONNX(model_file=self.onnx_file, session=session)
else:
return None
def patched_faceanalysis_init(self, name=DEFAULT_MP_NAME, root='~/.insightface', allowed_modules=None, **kwargs):
onnxruntime.set_default_logger_severity(3)
self.models = {}
self.model_dir = ensure_available('models', name, root=root)
onnx_files = glob.glob(osp.join(self.model_dir, '*.onnx'))
onnx_files = sorted(onnx_files)
for onnx_file in onnx_files:
model = model_zoo.get_model(onnx_file, **kwargs)
if model is None:
print('model not recognized:', onnx_file)
elif allowed_modules is not None and model.taskname not in allowed_modules:
print('model ignore:', onnx_file, model.taskname)
del model
elif model.taskname not in self.models and (allowed_modules is None or model.taskname in allowed_modules):
self.models[model.taskname] = model
else:
print('duplicated model task type, ignore:', onnx_file, model.taskname)
del model
assert 'detection' in self.models
self.det_model = self.models['detection']
def patched_faceanalysis_prepare(self, ctx_id, det_thresh=0.5, det_size=(640, 640)):
self.det_thresh = det_thresh
assert det_size is not None
self.det_size = det_size
for taskname, model in self.models.items():
if taskname == 'detection':
model.prepare(ctx_id, input_size=det_size, det_thresh=det_thresh)
else:
model.prepare(ctx_id)
def patched_inswapper_init(self, model_file=None, session=None):
self.model_file = model_file
self.session = session
model = onnx.load(self.model_file)
graph = model.graph
self.emap = numpy_helper.to_array(graph.initializer[-1])
self.input_mean = 0.0
self.input_std = 255.0
if self.session is None:
self.session = onnxruntime.InferenceSession(self.model_file, None)
inputs = self.session.get_inputs()
self.input_names = []
for inp in inputs:
self.input_names.append(inp.name)
outputs = self.session.get_outputs()
output_names = []
for out in outputs:
output_names.append(out.name)
self.output_names = output_names
assert len(self.output_names) == 1
input_cfg = inputs[0]
input_shape = input_cfg.shape
self.input_shape = input_shape
self.input_size = tuple(input_shape[2:4][::-1])
def pathced_retinaface_prepare(self, ctx_id, **kwargs):
if ctx_id<0:
self.session.set_providers(['CPUExecutionProvider'])
nms_thresh = kwargs.get('nms_thresh', None)
if nms_thresh is not None:
self.nms_thresh = nms_thresh
det_thresh = kwargs.get('det_thresh', None)
if det_thresh is not None:
self.det_thresh = det_thresh
input_size = kwargs.get('input_size', None)
if input_size is not None and self.input_size is None:
self.input_size = input_size
def patch_insightface(get_model, faceanalysis_init, faceanalysis_prepare, inswapper_init, retinaface_prepare):
insightface.model_zoo.model_zoo.ModelRouter.get_model = get_model
insightface.app.FaceAnalysis.__init__ = faceanalysis_init
insightface.app.FaceAnalysis.prepare = faceanalysis_prepare
insightface.model_zoo.inswapper.INSwapper.__init__ = inswapper_init
insightface.model_zoo.retinaface.RetinaFace.prepare = retinaface_prepare
# original_functions = [ModelRouter.get_model, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
original_functions = [patched_get_model_log, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__, RetinaFace.prepare]
patched_functions = [patched_get_model, patched_faceanalysis_init, patched_faceanalysis_prepare, patched_inswapper_init, pathced_retinaface_prepare]
def apply_patch(console_log_level):
if console_log_level == 0:
patch_insightface(*patched_functions)
logger.setLevel(logging.WARNING)
elif console_log_level == 1:
patch_insightface(*patched_functions)
logger.setLevel(logging.STATUS)
elif console_log_level == 2:
patch_insightface(*original_functions)
logger.setLevel(logging.INFO)
+1 -1
View File
@@ -7,7 +7,7 @@ import cv2
import math
import logging
import hashlib
from insightface.app.common import Face
from reactor_core.face_objects import Face
from safetensors.torch import save_file, safe_open
from tqdm import tqdm
import urllib.request
+1 -2
View File
@@ -1,7 +1,6 @@
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
+4 -4
View File
@@ -2,14 +2,14 @@ import os, glob
from PIL import Image
import modules.scripts as scripts
import r_modules.scripts as scripts
# from modules.upscaler import Upscaler, UpscalerData
from modules import scripts, scripts_postprocessing
from modules.processing import (
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,
+1 -1
View File
@@ -2,7 +2,7 @@ import logging
import copy
import sys
from modules import shared
from r_modules import shared
from reactor_utils import addLoggingLevel
+1 -1
View File
@@ -30,7 +30,7 @@ def ensure_nsfw_model(nsfwdet_model_path):
MODEL_EXISTS = True if downloaded == 3 else False
return MODEL_EXISTS
SCORE = 0.969
SCORE = 0.979
logging.getLogger("transformers").setLevel(logging.ERROR)
+127 -313
View File
@@ -6,15 +6,15 @@ import cv2
import numpy as np
from PIL import Image
import onnxruntime as ort
import insightface
from insightface.app.common import Face
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 (
@@ -80,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
@@ -102,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)
@@ -116,163 +111,16 @@ def getFaceSwapModel(model_path: str):
FS_MODEL = unload_model(FS_MODEL)
model_filename = os.path.basename(model_path)
if "hyperswap" in model_filename.lower():
if "hyperswap" in model_filename.lower(): # Если это Hyperswap
model_path = os.path.join(folder_paths.models_dir, "hyperswap", model_filename)
FS_MODEL = ort.InferenceSession(model_path, providers=providers)
elif "reswapper" in model_filename.lower():
model_path = os.path.join(folder_paths.models_dir, "reswapper", model_filename)
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
else:
FS_MODEL = insightface.model_zoo.get_model(model_path, providers=providers)
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
# Функция для получения 5 ключевых точек из объекта Face
def get_landmarks_5(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(src_pts, dst_pts):
M, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
return M
# Создаём градиентную маску овальной формы без обрезки
def create_gradient_mask(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(target_img, swapped_face, M, crop_size=256):
# 1. Создание мягкой маски (Эрозия + Размытие)
mask = create_gradient_mask(crop_size)
# Преобразуем в трехканальную маску
mask_3c = np.stack([mask] * 3, axis=2)
# 2. Получаем размеры целевого изображения
h, w = target_img.shape[:2]
# 3. Обратное преобразование (WARP_INVERSE_MAP) для лица И маски
# Для лица (INTER_LANCZOS4 — высококачественная интерполяция)
inv_face = cv2.warpAffine(
swapped_face.astype(np.float32),
M,
(w, h),
flags=cv2.INTER_LANCZOS4 | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_TRANSPARENT
)
# Для маски (INTER_CUBIC — плавные границы)
inv_mask = cv2.warpAffine(
mask_3c,
M,
(w, h),
flags=cv2.INTER_CUBIC | cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_TRANSPARENT
)
# 4. Ограничение значений маски [0, 1]
inv_mask = np.clip(inv_mask, 0, 1)
# 5. Дополнительное размытие для устранения артефактов
inv_mask = cv2.GaussianBlur(inv_mask, (3, 3), 0)
# 6. Плавное наложение
target_img_float = target_img.astype(np.float32)
inv_face_float = inv_face.astype(np.float32)
result = target_img_float * (1.0 - inv_mask) + inv_face_float * inv_mask
# 7. Ограничение результата [0, 255]
result = np.clip(result, 0, 255).astype(np.uint8)
return result
def visualize_points(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 с аффинным преобразованием
def run_hyperswap(session, source_face, target_face, target_img):
# 1. Подготовка эмбеддинга
source_embedding = source_face.normed_embedding.reshape(1, -1).astype(np.float32)
# 2. Получаем 5 точек target
target_landmarks_5 = get_landmarks_5(target_face)
visualize_points(target_img, target_landmarks_5, (0, 255, 0)) # Зеленые точки
if target_landmarks_5 is None:
return 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 = get_affine_transform(target_landmarks_5.astype(np.float32), std_landmarks_256)
# Применяем аффинное преобразование с новой матрицей M
crop = cv2.warpAffine(target_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 = session.run(None, {'source': source_embedding, 'target': crop_input})[0][0]
except:
return target_img
# 6. Обратная нормализация
output = (output * 0.5 + 0.5) * 255.0 # [-1..1] -> [0..255]
output = np.clip(output, 0, 255).astype(np.uint8)
output = output.transpose(1, 2, 0) # CHW -> HWC
output = output[:, :, ::-1] # BGR -> RGB
return output, M # Возвращаем лицо (256x256) и матрицу M
def sort_by_order(face, order: str):
if order == "left-right":
return sorted(face, key=lambda x: x.bbox[0])
@@ -294,35 +142,36 @@ 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, None # 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))
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, face_index # <-- есть, но не тот пол
# 4. Проверяем пол выбранного лица
# face.gender: 0 = female, 1 = male
# gender_condition: 1 = female, 2 = male
expected_gender = 0 if gender_condition == 1 else 1
actual_gender = getattr(face_selected, 'gender', -1)
sel_gender_str = "Male" if actual_gender == 1 else "Female" if actual_gender == 0 else "Unknown"
logger.info("%s Face %s: Detected Gender -%s-", operated, face_index, sel_gender_str)
# Если пол не совпадает с тем, что заказал юзер
if actual_gender != expected_gender:
logger.info(f"{operated} Face {face_index}: WRONG gender ({sel_gender_str})")
return face_selected, 1, face_index # 1 означает флаг wrong_gender = True (цикл его пропустит)
# Если всё идеально
return face_selected, 0, face_index
def half_det_size(det_size):
@@ -335,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
@@ -370,7 +222,6 @@ 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, face_index
# return sorted(face, key=lambda x: x.bbox[0])[face_index], 0
except IndexError:
return None, 0, None
@@ -471,22 +322,27 @@ 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, 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, source_face_index = 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)
@@ -499,51 +355,40 @@ def swap_face(
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, source_face_index = 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, 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...")
if "hyperswap" in model:
swapped_face_256, M = run_hyperswap(face_swapper, source_face, target_face, result)
if swapped_face_256 is not None:
result = paste_back(result, swapped_face_256, M, crop_size=256)
elif face_boost_enabled:
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
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(result, bgr_fake, M)
else:
result = face_swapper.get(result, target_face, source_face)
bbox = [tuple(map(float, target_face.bbox))]
swapped_indexes = [target_face_index]
elif wrong_gender == 1:
wrong_gender = 0
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
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, bbox, swapped_indexes
@@ -570,26 +415,19 @@ def swap_face_many(
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:
@@ -612,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))
@@ -652,47 +487,43 @@ 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, 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, source_face_index = 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
if "inswapper" in model:
model_path = os.path.join(insightface_path, model)
@@ -704,64 +535,47 @@ def swap_face_many(
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, source_face_index = 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, target_face_index = 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 "hyperswap" in model:
swapped_face_256, M = run_hyperswap(face_swapper, source_face, target_face_single, result)
if swapped_face_256 is not None:
result = paste_back(result, swapped_face_256, M, crop_size=256)
elif face_boost_enabled:
logger.status(f"Face Boost is enabled (inswapper/reswapper only)")
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:
result = face_swapper.get(target_img, target_face_single, source_face)
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:
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, bbox, swapped_indexes
+1 -1
View File
@@ -1,5 +1,5 @@
app_title = "ReActor Node for ComfyUI"
version_flag = "v0.6.2"
version_flag = "v0.7.1-b3"
COLORS = {
"CYAN": "\033[0;36m", # CYAN