Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a5500bdf | |||
| 86134b6e1d | |||
| 9e6f30c0a4 | |||
| 97321a740d | |||
| f5f7ac7764 | |||
| 77d3492eef | |||
| 8e3d6e7c65 | |||
| ee9699ee70 | |||
| 3c8b259a3f | |||
| 30b27c2b71 | |||
| 0d8f3b1f82 | |||
| 6e9e7addf2 | |||
| 0c7e871bfc | |||
| e340b0da8a | |||
| d0f81ed755 | |||
| de01b28802 | |||
| b645d5e60b | |||
| 31b3a97003 | |||
| e3b46e83b7 | |||
| e93fb95903 | |||
| aabf41050a | |||
| e57116de68 | |||
| d5338a3eae | |||
| 7ec3a4be29 | |||
| ca6cba9311 | |||
| d89385457e | |||
| b015f0099f | |||
| e56a79222e | |||
| 5b0bf735b5 | |||
| c02bd519d8 | |||
| 36bb1a29b0 | |||
| 2bbc150bfb | |||
| a1722c7b2e | |||
| 07b4d66965 | |||
| ff7cc3ac2f | |||
| f0ec0744f7 | |||
| 36b6ea0019 | |||
| 523ee53c34 | |||
| e544889805 | |||
| c6524facfb | |||
| 91baa6c0a5 | |||
| a4c617af3e | |||
| 9a33f5e184 | |||
| 2b36300b8c | |||
| 21c029f51e | |||
| 06bc8f2152 | |||
| 63b90c428e | |||
| df8e8b427e | |||
| dfd145b996 | |||
| b3c4ed9250 | |||
| 2411f1e9b1 | |||
| 96224efe07 | |||
| 8e05142cda | |||
| a007db2ffa | |||
| 475740b22b | |||
| 600ce34c8d | |||
| 865ab3ca02 | |||
| 178578b034 | |||
| b53132f3a4 | |||
| 00da11b491 | |||
| b82fdc3f31 | |||
| 3ffa9f38b0 | |||
| 3f98d4c826 | |||
| 9b6ca286b9 | |||
| 28c60b69d1 | |||
| fcf547d7d2 | |||
| ae2d21456d | |||
| 0999c0447e | |||
| f9270c5d1c | |||
| fdbc29c1a9 | |||
| 87d982e6f8 | |||
| cf47dabf0e | |||
| d0d90ecc03 | |||
| 2b70131e6a | |||
| fc86365a90 | |||
| 1dd0e8e509 | |||
| 4e0ff540f0 | |||
| f0fae811d8 | |||
| 42687f5bd9 | |||
| 9086072b8e | |||
| 12fda0a3ed | |||
| d963430854 | |||
| 5855d15c09 | |||
| fcc73d0add | |||
| 8d4a386a27 | |||
| b98c5234d8 | |||
| 8bdc14a789 | |||
| f121083bc8 | |||
| 745d449ca6 | |||
| ec6d7d2995 | |||
| e791f2f18a | |||
| 3795e41fd7 | |||
| ab8a1c82c1 | |||
| e1842ae0ba | |||
| 989106e914 | |||
| de27fb8a81 | |||
| 28109e93bb | |||
| fc312516e3 | |||
| 72049f3e91 | |||
| 6cb5de01f8 | |||
| 0bcf340217 | |||
| 994a63c546 | |||
| d5a3fb0c47 | |||
| 9690070399 | |||
| f3e83b985c | |||
| e3e3638b79 | |||
| 4a7874a968 | |||
| 75122da389 | |||
| 7063bba4b3 | |||
| bdbd7dcfbc | |||
| a64940def7 | |||
| fe4a87e8f2 | |||
| 9ecd2dab83 | |||
| c9f36eb350 | |||
| b1f610d432 | |||
| d86c36dc47 | |||
| 647c5f250f | |||
| ae88412aae | |||
| b7e011f5e7 | |||
| 532e7c05ee | |||
| 267a273cb2 | |||
| 938aa9eaf1 | |||
| 37bac27302 | |||
| 84836932e6 | |||
| e879d2ca64 | |||
| 181144ce33 | |||
| 890beb0eae | |||
| 75b5b096d6 | |||
| 40e47a469c | |||
| 874abb4e59 | |||
| 18b259da70 | |||
| 01900dcfb5 | |||
| 07e30fe781 | |||
| 3dda4f2179 | |||
| 71735e4f60 | |||
| 90d5c28542 | |||
| 104d8cf4d6 | |||
| ac3696b69d | |||
| 76fb209e6c | |||
| 2dcd552c4b |
@@ -25,3 +25,5 @@ models/DMDNet.pth
|
|||||||
faceswap/
|
faceswap/
|
||||||
.vscode/
|
.vscode/
|
||||||
switch_states.json
|
switch_states.json
|
||||||
|
/models
|
||||||
|
install.bat
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h1 align="center">Deep-Live-Cam</h1>
|
<h1 align="center">Deep-Live-Cam 2.1</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Real-time face swap and video deepfake with a single click and only a single image.
|
Real-time face swap and video deepfake with a single click and only a single image.
|
||||||
@@ -30,14 +30,13 @@ By using this software, you agree to these terms and commit to using it in a man
|
|||||||
|
|
||||||
Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions.
|
Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions.
|
||||||
|
|
||||||
|
## Exclusive v2.7 beta Quick Start - Pre-built (Windows/Mac Silicon/CPU)
|
||||||
|
|
||||||
## Quick Start - Pre-built (Windows / Nvidia)
|
<a href="https://deeplivecam.net/index.php/quickstart"> <img src="media/Download.png" width="285" height="77" />
|
||||||
|
|
||||||
<a href="https://hacksider.gumroad.com/l/vccdmm"> <img src="https://github.com/user-attachments/assets/7d993b32-e3e8-4cd3-bbfb-a549152ebdd5" width="285" height="77" />
|
##### This is the fastest build you can get if you have a discrete NVIDIA or AMD GPU, CPU or Mac Silicon, And you'll receive special priority support. 2.7 beta is the best you can have with 30+ extra features than the open source version.
|
||||||
|
|
||||||
##### This is the fastest build you can get if you have a discrete NVIDIA GPU.
|
|
||||||
|
|
||||||
###### These Pre-builts are perfect for non-technical users or those who don't have time to, or can't manually install all the requirements. Just a heads-up: this is an open-source project, so you can also install it manually.
|
###### These Pre-builts are perfect for non-technical users or those who don't have time to, or can't manually install all the requirements. Just a heads-up: this is an open-source project, so you can also install it manually.
|
||||||
|
|
||||||
## TLDR; Live Deepfake in just 3 Clicks
|
## TLDR; Live Deepfake in just 3 Clicks
|
||||||

|

|
||||||
@@ -99,7 +98,7 @@ Users are expected to use this software responsibly and legally. If using a real
|
|||||||
|
|
||||||
## Installation (Manual)
|
## Installation (Manual)
|
||||||
|
|
||||||
**Please be aware that the installation requires technical skills and is not for beginners. Consider downloading the prebuilt version.**
|
**Please be aware that the installation requires technical skills and is not for beginners. Consider downloading the quickstart version.**
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click to see the process</summary>
|
<summary>Click to see the process</summary>
|
||||||
@@ -110,7 +109,7 @@ This is more likely to work on your computer but will be slower as it utilizes t
|
|||||||
|
|
||||||
**1. Set up Your Platform**
|
**1. Set up Your Platform**
|
||||||
|
|
||||||
- Python (3.10 recommended)
|
- Python (3.11 recommended)
|
||||||
- pip
|
- pip
|
||||||
- git
|
- git
|
||||||
- [ffmpeg](https://www.youtube.com/watch?v=OlNWCpFdVMA) - ```iex (irm ffmpeg.tc.ht)```
|
- [ffmpeg](https://www.youtube.com/watch?v=OlNWCpFdVMA) - ```iex (irm ffmpeg.tc.ht)```
|
||||||
@@ -125,7 +124,7 @@ cd Deep-Live-Cam
|
|||||||
|
|
||||||
**3. Download the Models**
|
**3. Download the Models**
|
||||||
|
|
||||||
1. [GFPGANv1.4](https://huggingface.co/hacksider/deep-live-cam/resolve/main/GFPGANv1.4.pth)
|
1. [GFPGANv1.4](https://huggingface.co/hacksider/deep-live-cam/resolve/main/GFPGANv1.4.onnx)
|
||||||
2. [inswapper\_128\_fp16.onnx](https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx)
|
2. [inswapper\_128\_fp16.onnx](https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx)
|
||||||
|
|
||||||
Place these files in the "**models**" folder.
|
Place these files in the "**models**" folder.
|
||||||
@@ -134,26 +133,34 @@ Place these files in the "**models**" folder.
|
|||||||
|
|
||||||
We highly recommend using a `venv` to avoid issues.
|
We highly recommend using a `venv` to avoid issues.
|
||||||
|
|
||||||
|
|
||||||
For Windows:
|
For Windows:
|
||||||
```bash
|
```bash
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
venv\Scripts\activate
|
venv\Scripts\activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
For Linux:
|
||||||
|
```bash
|
||||||
|
# Ensure you use the installed Python 3.10
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
**For macOS:**
|
**For macOS:**
|
||||||
|
|
||||||
Apple Silicon (M1/M2/M3) requires specific setup:
|
Apple Silicon (M1/M2/M3) requires specific setup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Python 3.10 (specific version is important)
|
# Install Python 3.11 (specific version is important)
|
||||||
brew install python@3.10
|
brew install python@3.11
|
||||||
|
|
||||||
# Install tkinter package (required for the GUI)
|
# Install tkinter package (required for the GUI)
|
||||||
brew install python-tk@3.10
|
brew install python-tk@3.10
|
||||||
|
|
||||||
# Create and activate virtual environment with Python 3.10
|
# Create and activate virtual environment with Python 3.11
|
||||||
python3.10 -m venv venv
|
python3.11 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
@@ -172,6 +179,11 @@ source venv/bin/activate
|
|||||||
|
|
||||||
# install the dependencies again
|
# install the dependencies again
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# gfpgan and basicsrs issue fix
|
||||||
|
pip install git+https://github.com/xinntao/BasicSR.git@master
|
||||||
|
pip uninstall gfpgan -y
|
||||||
|
pip install git+https://github.com/TencentARC/GFPGAN.git@master
|
||||||
```
|
```
|
||||||
|
|
||||||
**Run:** If you don't have a GPU, you can run Deep-Live-Cam using `python run.py`. Note that initial execution will download models (~300MB).
|
**Run:** If you don't have a GPU, you can run Deep-Live-Cam using `python run.py`. Note that initial execution will download models (~300MB).
|
||||||
@@ -180,12 +192,16 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
**CUDA Execution Provider (Nvidia)**
|
**CUDA Execution Provider (Nvidia)**
|
||||||
|
|
||||||
1. Install [CUDA Toolkit 11.8.0](https://developer.nvidia.com/cuda-11-8-0-download-archive)
|
1. Install [CUDA Toolkit 12.8.0](https://developer.nvidia.com/cuda-12-8-0-download-archive)
|
||||||
2. Install dependencies:
|
2. Install [cuDNN v8.9.7 for CUDA 12.x](https://developer.nvidia.com/rdp/cudnn-archive) (required for onnxruntime-gpu):
|
||||||
|
- Download cuDNN v8.9.7 for CUDA 12.x
|
||||||
|
- Make sure the cuDNN bin directory is in your system PATH
|
||||||
|
3. Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
|
||||||
pip uninstall onnxruntime onnxruntime-gpu
|
pip uninstall onnxruntime onnxruntime-gpu
|
||||||
pip install onnxruntime-gpu==1.16.3
|
pip install onnxruntime-gpu==1.21.0
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Usage:
|
3. Usage:
|
||||||
@@ -225,7 +241,7 @@ python3.10 run.py --execution-provider coreml
|
|||||||
# Uninstall conflicting versions if needed
|
# Uninstall conflicting versions if needed
|
||||||
brew uninstall --ignore-dependencies python@3.11 python@3.13
|
brew uninstall --ignore-dependencies python@3.11 python@3.13
|
||||||
|
|
||||||
# Keep only Python 3.10
|
# Keep only Python 3.11
|
||||||
brew cleanup
|
brew cleanup
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -235,7 +251,7 @@ python3.10 run.py --execution-provider coreml
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip uninstall onnxruntime onnxruntime-coreml
|
pip uninstall onnxruntime onnxruntime-coreml
|
||||||
pip install onnxruntime-coreml==1.13.1
|
pip install onnxruntime-coreml==1.21.0
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Usage:
|
2. Usage:
|
||||||
@@ -250,7 +266,7 @@ python run.py --execution-provider coreml
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip uninstall onnxruntime onnxruntime-directml
|
pip uninstall onnxruntime onnxruntime-directml
|
||||||
pip install onnxruntime-directml==1.15.1
|
pip install onnxruntime-directml==1.21.0
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Usage:
|
2. Usage:
|
||||||
@@ -265,7 +281,7 @@ python run.py --execution-provider directml
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip uninstall onnxruntime onnxruntime-openvino
|
pip uninstall onnxruntime onnxruntime-openvino
|
||||||
pip install onnxruntime-openvino==1.15.0
|
pip install onnxruntime-openvino==1.21.0
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Usage:
|
2. Usage:
|
||||||
@@ -293,18 +309,8 @@ python run.py --execution-provider openvino
|
|||||||
- Use a screen capture tool like OBS to stream.
|
- Use a screen capture tool like OBS to stream.
|
||||||
- To change the face, select a new source image.
|
- To change the face, select a new source image.
|
||||||
|
|
||||||
## Tips and Tricks
|
## Download all models in this huggingface link
|
||||||
|
- [**Download models here**](https://huggingface.co/hacksider/deep-live-cam/tree/main)
|
||||||
Check out these helpful guides to get the most out of Deep-Live-Cam:
|
|
||||||
|
|
||||||
- [Unlocking the Secrets to the Perfect Deepfake Image](https://deeplivecam.net/index.php/blog/tips-and-tricks/unlocking-the-secrets-to-the-perfect-deepfake-image) - Learn how to create the best deepfake with full head coverage
|
|
||||||
- [Video Call with DeepLiveCam](https://deeplivecam.net/index.php/blog/tips-and-tricks/video-call-with-deeplivecam) - Make your meetings livelier by using DeepLiveCam with OBS and meeting software
|
|
||||||
- [Have a Special Guest!](https://deeplivecam.net/index.php/blog/tips-and-tricks/have-a-special-guest) - Tutorial on how to use face mapping to add special guests to your stream
|
|
||||||
- [Watch Deepfake Movies in Realtime](https://deeplivecam.net/index.php/blog/tips-and-tricks/watch-deepfake-movies-in-realtime) - See yourself star in any video without processing the video
|
|
||||||
- [Better Quality without Sacrificing Speed](https://deeplivecam.net/index.php/blog/tips-and-tricks/better-quality-without-sacrificing-speed) - Tips for achieving better results without impacting performance
|
|
||||||
- [Instant Vtuber!](https://deeplivecam.net/index.php/blog/tips-and-tricks/instant-vtuber) - Create a new persona/vtuber easily using Metahuman Creator
|
|
||||||
|
|
||||||
Visit our [official blog](https://deeplivecam.net/index.php/blog/tips-and-tricks) for more tips and tutorials.
|
|
||||||
|
|
||||||
## Command Line Arguments (Unmaintained)
|
## Command Line Arguments (Unmaintained)
|
||||||
|
|
||||||
@@ -335,24 +341,22 @@ Looking for a CLI mode? Using the -s/--source argument will make the run program
|
|||||||
|
|
||||||
## Press
|
## Press
|
||||||
|
|
||||||
**We are always open to criticism and are ready to improve, that's why we didn't cherry-pick anything.**
|
- [**Ars Technica**](https://arstechnica.com/information-technology/2024/08/new-ai-tool-enables-real-time-face-swapping-on-webcams-raising-fraud-concerns/) - *"Deep-Live-Cam goes viral, allowing anyone to become a digital doppelganger"*
|
||||||
|
- [**Yahoo!**](https://www.yahoo.com/tech/ok-viral-ai-live-stream-080041056.html) - *"OK, this viral AI live stream software is truly terrifying"*
|
||||||
|
- [**CNN Brasil**](https://www.cnnbrasil.com.br/tecnologia/ia-consegue-clonar-rostos-na-webcam-entenda-funcionamento/) - *"AI can clone faces on webcam; understand how it works"*
|
||||||
|
- [**Bloomberg Technoz**](https://www.bloombergtechnoz.com/detail-news/71032/kenalan-dengan-teknologi-deep-live-cam-bisa-jadi-alat-menipu) - *"Get to know Deep Live Cam technology, it can be used as a tool for deception."*
|
||||||
|
- [**TrendMicro**](https://www.trendmicro.com/vinfo/gb/security/news/cyber-attacks/ai-vs-ai-deepfakes-and-ekyc) - *"AI vs AI: DeepFakes and eKYC"*
|
||||||
|
- [**PetaPixel**](https://petapixel.com/2024/08/14/deep-live-cam-deepfake-ai-tool-lets-you-become-anyone-in-a-video-call-with-single-photo-mark-zuckerberg-jd-vance-elon-musk/) - *"Deepfake AI Tool Lets You Become Anyone in a Video Call With Single Photo"*
|
||||||
|
- [**SomeOrdinaryGamers**](https://www.youtube.com/watch?time_continue=1074&v=py4Tc-Y8BcY) - *"That's Crazy, Oh God. That's Fucking Freaky Dude... That's So Wild Dude"*
|
||||||
|
- [**IShowSpeed**](https://www.youtube.com/live/mFsCe7AIxq8?feature=shared&t=2686) - *"Alright look look look, now look chat, we can do any face we want to look like chat"*
|
||||||
|
- [**TechLinked (Linus Tech Tips)**](https://www.youtube.com/watch?v=wnCghLjqv3s&t=551s) - *"They do a pretty good job matching poses, expression and even the lighting"*
|
||||||
|
- [**IShowSpeed**](https://youtu.be/JbUPRmXRUtE?t=3964) - *"What the F***! Why do I look like Vinny Jr? I look exactly like Vinny Jr!? No, this shit is crazy! Bro This is F*** Crazy!"*
|
||||||
|
|
||||||
- [*"Deep-Live-Cam goes viral, allowing anyone to become a digital doppelganger"*](https://arstechnica.com/information-technology/2024/08/new-ai-tool-enables-real-time-face-swapping-on-webcams-raising-fraud-concerns/) - Ars Technica
|
|
||||||
- [*"Thanks Deep Live Cam, shapeshifters are among us now"*](https://dataconomy.com/2024/08/15/what-is-deep-live-cam-github-deepfake/) - Dataconomy
|
|
||||||
- [*"This free AI tool lets you become anyone during video-calls"*](https://www.newsbytesapp.com/news/science/deep-live-cam-ai-impersonation-tool-goes-viral/story) - NewsBytes
|
|
||||||
- [*"OK, this viral AI live stream software is truly terrifying"*](https://www.creativebloq.com/ai/ok-this-viral-ai-live-stream-software-is-truly-terrifying) - Creative Bloq
|
|
||||||
- [*"Deepfake AI Tool Lets You Become Anyone in a Video Call With Single Photo"*](https://petapixel.com/2024/08/14/deep-live-cam-deepfake-ai-tool-lets-you-become-anyone-in-a-video-call-with-single-photo-mark-zuckerberg-jd-vance-elon-musk/) - PetaPixel
|
|
||||||
- [*"Deep-Live-Cam Uses AI to Transform Your Face in Real-Time, Celebrities Included"*](https://www.techeblog.com/deep-live-cam-ai-transform-face/) - TechEBlog
|
|
||||||
- [*"An AI tool that "makes you look like anyone" during a video call is going viral online"*](https://telegrafi.com/en/a-tool-that-makes-you-look-like-anyone-during-a-video-call-is-going-viral-on-the-Internet/) - Telegrafi
|
|
||||||
- [*"This Deepfake Tool Turning Images Into Livestreams is Topping the GitHub Charts"*](https://decrypt.co/244565/this-deepfake-tool-turning-images-into-livestreams-is-topping-the-github-charts) - Emerge
|
|
||||||
- [*"New Real-Time Face-Swapping AI Allows Anyone to Mimic Famous Faces"*](https://www.digitalmusicnews.com/2024/08/15/face-swapping-ai-real-time-mimic/) - Digital Music News
|
|
||||||
- [*"This real-time webcam deepfake tool raises alarms about the future of identity theft"*](https://www.diyphotography.net/this-real-time-webcam-deepfake-tool-raises-alarms-about-the-future-of-identity-theft/) - DIYPhotography
|
|
||||||
- [*"That's Crazy, Oh God. That's Fucking Freaky Dude... That's So Wild Dude"*](https://www.youtube.com/watch?time_continue=1074&v=py4Tc-Y8BcY) - SomeOrdinaryGamers
|
|
||||||
- [*"Alright look look look, now look chat, we can do any face we want to look like chat"*](https://www.youtube.com/live/mFsCe7AIxq8?feature=shared&t=2686) - IShowSpeed
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [ffmpeg](https://ffmpeg.org/): for making video-related operations easy
|
- [ffmpeg](https://ffmpeg.org/): for making video-related operations easy
|
||||||
|
- [Henry](https://github.com/henryruhs): One of the major contributor in this repo
|
||||||
- [deepinsight](https://github.com/deepinsight): for their [insightface](https://github.com/deepinsight/insightface) project which provided a well-made library and models. Please be reminded that the [use of the model is for non-commercial research purposes only](https://github.com/deepinsight/insightface?tab=readme-ov-file#license).
|
- [deepinsight](https://github.com/deepinsight): for their [insightface](https://github.com/deepinsight/insightface) project which provided a well-made library and models. Please be reminded that the [use of the model is for non-commercial research purposes only](https://github.com/deepinsight/insightface?tab=readme-ov-file#license).
|
||||||
- [havok2-htwo](https://github.com/havok2-htwo): for sharing the code for webcam
|
- [havok2-htwo](https://github.com/havok2-htwo): for sharing the code for webcam
|
||||||
- [GosuDRM](https://github.com/GosuDRM): for the open version of roop
|
- [GosuDRM](https://github.com/GosuDRM): for the open version of roop
|
||||||
@@ -360,6 +364,7 @@ Looking for a CLI mode? Using the -s/--source argument will make the run program
|
|||||||
- [vic4key](https://github.com/vic4key): For supporting/contributing to this project
|
- [vic4key](https://github.com/vic4key): For supporting/contributing to this project
|
||||||
- [kier007](https://github.com/kier007): for improving the user experience
|
- [kier007](https://github.com/kier007): for improving the user experience
|
||||||
- [qitianai](https://github.com/qitianai): for multi-lingual support
|
- [qitianai](https://github.com/qitianai): for multi-lingual support
|
||||||
|
- [laurigates](https://github.com/laurigates): Decoupling stuffs to make everything faster!
|
||||||
- and [all developers](https://github.com/hacksider/Deep-Live-Cam/graphs/contributors) behind libraries used in this project.
|
- and [all developers](https://github.com/hacksider/Deep-Live-Cam/graphs/contributors) behind libraries used in this project.
|
||||||
- Footnote: Please be informed that the base author of the code is [s0md3v](https://github.com/s0md3v/roop)
|
- Footnote: Please be informed that the base author of the code is [s0md3v](https://github.com/s0md3v/roop)
|
||||||
- All the wonderful users who helped make this project go viral by starring the repo ❤️
|
- All the wonderful users who helped make this project go viral by starring the repo ❤️
|
||||||
@@ -378,4 +383,4 @@ Looking for a CLI mode? Using the -s/--source argument will make the run program
|
|||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" />
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Quelle x Ziel Zuordnung",
|
||||||
|
"select a source image": "Wähle ein Quellbild",
|
||||||
|
"Preview": "Vorschau",
|
||||||
|
"select a target image or video": "Wähle ein Zielbild oder Video",
|
||||||
|
"save image output file": "Bildausgabedatei speichern",
|
||||||
|
"save video output file": "Videoausgabedatei speichern",
|
||||||
|
"select a target image": "Wähle ein Zielbild",
|
||||||
|
"source": "Quelle",
|
||||||
|
"Select a target": "Wähle ein Ziel",
|
||||||
|
"Select a face": "Wähle ein Gesicht",
|
||||||
|
"Keep audio": "Audio beibehalten",
|
||||||
|
"Face Enhancer": "Gesichtsverbesserung",
|
||||||
|
"Many faces": "Mehrere Gesichter",
|
||||||
|
"Show FPS": "FPS anzeigen",
|
||||||
|
"Keep fps": "FPS beibehalten",
|
||||||
|
"Keep frames": "Frames beibehalten",
|
||||||
|
"Fix Blueish Cam": "Bläuliche Kamera korrigieren",
|
||||||
|
"Mouth Mask": "Mundmaske",
|
||||||
|
"Show Mouth Mask Box": "Mundmaskenrahmen anzeigen",
|
||||||
|
"Start": "Starten",
|
||||||
|
"Live": "Live",
|
||||||
|
"Destroy": "Beenden",
|
||||||
|
"Map faces": "Gesichter zuordnen",
|
||||||
|
"Processing...": "Verarbeitung läuft...",
|
||||||
|
"Processing succeed!": "Verarbeitung erfolgreich!",
|
||||||
|
"Processing ignored!": "Verarbeitung ignoriert!",
|
||||||
|
"Failed to start camera": "Kamera konnte nicht gestartet werden",
|
||||||
|
"Please complete pop-up or close it.": "Bitte das Pop-up komplettieren oder schließen.",
|
||||||
|
"Getting unique faces": "Einzigartige Gesichter erfassen",
|
||||||
|
"Please select a source image first": "Bitte zuerst ein Quellbild auswählen",
|
||||||
|
"No faces found in target": "Keine Gesichter im Zielbild gefunden",
|
||||||
|
"Add": "Hinzufügen",
|
||||||
|
"Clear": "Löschen",
|
||||||
|
"Submit": "Absenden",
|
||||||
|
"Select source image": "Quellbild auswählen",
|
||||||
|
"Select target image": "Zielbild auswählen",
|
||||||
|
"Please provide mapping!": "Bitte eine Zuordnung angeben!",
|
||||||
|
"At least 1 source with target is required!": "Mindestens eine Quelle mit einem Ziel ist erforderlich!",
|
||||||
|
"At least 1 source with target is required!": "Mindestens eine Quelle mit einem Ziel ist erforderlich!",
|
||||||
|
"Face could not be detected in last upload!": "Im letzten Upload konnte kein Gesicht erkannt werden!",
|
||||||
|
"Select Camera:": "Kamera auswählen:",
|
||||||
|
"All mappings cleared!": "Alle Zuordnungen gelöscht!",
|
||||||
|
"Mappings successfully submitted!": "Zuordnungen erfolgreich übermittelt!",
|
||||||
|
"Source x Target Mapper is already open.": "Quell-zu-Ziel-Zuordnung ist bereits geöffnet."
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Mapeador de fuente x destino",
|
||||||
|
"select a source image": "Seleccionar imagen fuente",
|
||||||
|
"Preview": "Vista previa",
|
||||||
|
"select a target image or video": "elegir un video o una imagen fuente",
|
||||||
|
"save image output file": "guardar imagen final",
|
||||||
|
"save video output file": "guardar video final",
|
||||||
|
"select a target image": "elegir una imagen objetiva",
|
||||||
|
"source": "fuente",
|
||||||
|
"Select a target": "Elegir un destino",
|
||||||
|
"Select a face": "Elegir una cara",
|
||||||
|
"Keep audio": "Mantener audio original",
|
||||||
|
"Face Enhancer": "Potenciador de caras",
|
||||||
|
"Many faces": "Varias caras",
|
||||||
|
"Show FPS": "Mostrar fps",
|
||||||
|
"Keep fps": "Mantener fps",
|
||||||
|
"Keep frames": "Mantener frames",
|
||||||
|
"Fix Blueish Cam": "Corregir tono azul de video",
|
||||||
|
"Mouth Mask": "Máscara de boca",
|
||||||
|
"Show Mouth Mask Box": "Mostrar área de la máscara de boca",
|
||||||
|
"Start": "Iniciar",
|
||||||
|
"Live": "En vivo",
|
||||||
|
"Destroy": "Borrar",
|
||||||
|
"Map faces": "Mapear caras",
|
||||||
|
"Processing...": "Procesando...",
|
||||||
|
"Processing succeed!": "¡Proceso terminado con éxito!",
|
||||||
|
"Processing ignored!": "¡Procesamiento omitido!",
|
||||||
|
"Failed to start camera": "No se pudo iniciar la cámara",
|
||||||
|
"Please complete pop-up or close it.": "Complete o cierre el pop-up",
|
||||||
|
"Getting unique faces": "Buscando caras únicas",
|
||||||
|
"Please select a source image first": "Primero, seleccione una imagen fuente",
|
||||||
|
"No faces found in target": "No se encontró una cara en el destino",
|
||||||
|
"Add": "Agregar",
|
||||||
|
"Clear": "Limpiar",
|
||||||
|
"Submit": "Enviar",
|
||||||
|
"Select source image": "Seleccionar imagen fuente",
|
||||||
|
"Select target image": "Seleccionar imagen destino",
|
||||||
|
"Please provide mapping!": "Por favor, proporcione un mapeo",
|
||||||
|
"At least 1 source with target is required!": "Se requiere al menos una fuente con un destino.",
|
||||||
|
"At least 1 source with target is required!": "Se requiere al menos una fuente con un destino.",
|
||||||
|
"Face could not be detected in last upload!": "¡No se pudo encontrar una cara en el último video o imagen!",
|
||||||
|
"Select Camera:": "Elegir cámara:",
|
||||||
|
"All mappings cleared!": "¡Todos los mapeos fueron borrados!",
|
||||||
|
"Mappings successfully submitted!": "Mapeos enviados con éxito!",
|
||||||
|
"Source x Target Mapper is already open.": "El mapeador de fuente x destino ya está abierto."
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Source x Target Kartoitin",
|
||||||
|
"select an source image": "Valitse lähde kuva",
|
||||||
|
"Preview": "Esikatsele",
|
||||||
|
"select an target image or video": "Valitse kohde kuva tai video",
|
||||||
|
"save image output file": "tallenna kuva",
|
||||||
|
"save video output file": "tallenna video",
|
||||||
|
"select an target image": "Valitse kohde kuva",
|
||||||
|
"source": "lähde",
|
||||||
|
"Select a target": "Valitse kohde",
|
||||||
|
"Select a face": "Valitse kasvot",
|
||||||
|
"Keep audio": "Säilytä ääni",
|
||||||
|
"Face Enhancer": "Kasvojen Parantaja",
|
||||||
|
"Many faces": "Useampia kasvoja",
|
||||||
|
"Show FPS": "Näytä FPS",
|
||||||
|
"Keep fps": "Säilytä FPS",
|
||||||
|
"Keep frames": "Säilytä ruudut",
|
||||||
|
"Fix Blueish Cam": "Korjaa Sinertävä Kamera",
|
||||||
|
"Mouth Mask": "Suu Maski",
|
||||||
|
"Show Mouth Mask Box": "Näytä Suu Maski Laatiko",
|
||||||
|
"Start": "Aloita",
|
||||||
|
"Live": "Live",
|
||||||
|
"Destroy": "Tuhoa",
|
||||||
|
"Map faces": "Kartoita kasvot",
|
||||||
|
"Processing...": "Prosessoi...",
|
||||||
|
"Processing succeed!": "Prosessointi onnistui!",
|
||||||
|
"Processing ignored!": "Prosessointi lopetettu!",
|
||||||
|
"Failed to start camera": "Kameran käynnistäminen epäonnistui",
|
||||||
|
"Please complete pop-up or close it.": "Viimeistele tai sulje ponnahdusikkuna",
|
||||||
|
"Getting unique faces": "Hankitaan uniikkeja kasvoja",
|
||||||
|
"Please select a source image first": "Valitse ensin lähde kuva",
|
||||||
|
"No faces found in target": "Kasvoja ei löydetty kohteessa",
|
||||||
|
"Add": "Lisää",
|
||||||
|
"Clear": "Tyhjennä",
|
||||||
|
"Submit": "Lähetä",
|
||||||
|
"Select source image": "Valitse lähde kuva",
|
||||||
|
"Select target image": "Valitse kohde kuva",
|
||||||
|
"Please provide mapping!": "Tarjoa kartoitus!",
|
||||||
|
"Atleast 1 source with target is required!": "Vähintään 1 lähde kohteen kanssa on vaadittu!",
|
||||||
|
"At least 1 source with target is required!": "Vähintään 1 lähde kohteen kanssa on vaadittu!",
|
||||||
|
"Face could not be detected in last upload!": "Kasvoja ei voitu tunnistaa edellisessä latauksessa!",
|
||||||
|
"Select Camera:": "Valitse Kamera:",
|
||||||
|
"All mappings cleared!": "Kaikki kartoitukset tyhjennetty!",
|
||||||
|
"Mappings successfully submitted!": "Kartoitukset lähetety onnistuneesti!",
|
||||||
|
"Source x Target Mapper is already open.": "Lähde x Kohde Kartoittaja on jo auki."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Pemetaan Sumber x Target",
|
||||||
|
"select a source image": "Pilih gambar sumber",
|
||||||
|
"Preview": "Pratinjau",
|
||||||
|
"select a target image or video": "Pilih gambar atau video target",
|
||||||
|
"save image output file": "Simpan file keluaran gambar",
|
||||||
|
"save video output file": "Simpan file keluaran video",
|
||||||
|
"select a target image": "Pilih gambar target",
|
||||||
|
"source": "Sumber",
|
||||||
|
"Select a target": "Pilih target",
|
||||||
|
"Select a face": "Pilih wajah",
|
||||||
|
"Keep audio": "Pertahankan audio",
|
||||||
|
"Face Enhancer": "Peningkat wajah",
|
||||||
|
"Many faces": "Banyak wajah",
|
||||||
|
"Show FPS": "Tampilkan FPS",
|
||||||
|
"Keep fps": "Pertahankan FPS",
|
||||||
|
"Keep frames": "Pertahankan frame",
|
||||||
|
"Fix Blueish Cam": "Perbaiki kamera kebiruan",
|
||||||
|
"Mouth Mask": "Masker mulut",
|
||||||
|
"Show Mouth Mask Box": "Tampilkan kotak masker mulut",
|
||||||
|
"Start": "Mulai",
|
||||||
|
"Live": "Langsung",
|
||||||
|
"Destroy": "Hentikan",
|
||||||
|
"Map faces": "Petakan wajah",
|
||||||
|
"Processing...": "Sedang memproses...",
|
||||||
|
"Processing succeed!": "Pemrosesan berhasil!",
|
||||||
|
"Processing ignored!": "Pemrosesan diabaikan!",
|
||||||
|
"Failed to start camera": "Gagal memulai kamera",
|
||||||
|
"Please complete pop-up or close it.": "Harap selesaikan atau tutup pop-up.",
|
||||||
|
"Getting unique faces": "Mengambil wajah unik",
|
||||||
|
"Please select a source image first": "Silakan pilih gambar sumber terlebih dahulu",
|
||||||
|
"No faces found in target": "Tidak ada wajah ditemukan pada target",
|
||||||
|
"Add": "Tambah",
|
||||||
|
"Clear": "Bersihkan",
|
||||||
|
"Submit": "Kirim",
|
||||||
|
"Select source image": "Pilih gambar sumber",
|
||||||
|
"Select target image": "Pilih gambar target",
|
||||||
|
"Please provide mapping!": "Harap tentukan pemetaan!",
|
||||||
|
"At least 1 source with target is required!": "Minimal 1 sumber dengan target diperlukan!",
|
||||||
|
"Face could not be detected in last upload!": "Wajah tidak dapat terdeteksi pada unggahan terakhir!",
|
||||||
|
"Select Camera:": "Pilih Kamera:",
|
||||||
|
"All mappings cleared!": "Semua pemetaan telah dibersihkan!",
|
||||||
|
"Mappings successfully submitted!": "Pemetaan berhasil dikirim!",
|
||||||
|
"Source x Target Mapper is already open.": "Pemetaan Sumber x Target sudah terbuka."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "ប្រភប x បន្ថែម Mapper",
|
||||||
|
"select a source image": "ជ្រើសរើសប្រភពរូបភាព",
|
||||||
|
"Preview": "បង្ហាញ",
|
||||||
|
"select a target image or video": "ជ្រើសរើសគោលដៅរូបភាពឬវីដេអូ",
|
||||||
|
"save image output file": "រក្សាទុកលទ្ធផលឯកសាររូបភាព",
|
||||||
|
"save video output file": "រក្សាទុកលទ្ធផលឯកសារវីដេអូ",
|
||||||
|
"select a target image": "ជ្រើសរើសគោលដៅរូបភាព",
|
||||||
|
"source": "ប្រភព",
|
||||||
|
"Select a target": "ជ្រើសរើសគោលដៅ",
|
||||||
|
"Select a face": "ជ្រើសរើសមុខ",
|
||||||
|
"Keep audio": "រម្លងសម្លេង",
|
||||||
|
"Face Enhancer": "ឧបករណ៍ពង្រឹងមុខ",
|
||||||
|
"Many faces": "ទម្រង់មុខច្រើន",
|
||||||
|
"Show FPS": "បង្ហាញ FPS",
|
||||||
|
"Keep fps": "រម្លង fps",
|
||||||
|
"Keep frames": "រម្លងទម្រង់",
|
||||||
|
"Fix Blueish Cam": "ជួសជុល Cam Blueish",
|
||||||
|
"Mouth Mask": "របាំងមាត់",
|
||||||
|
"Show Mouth Mask Box": "បង្ហាញប្រអប់របាំងមាត់",
|
||||||
|
"Start": "ចាប់ផ្ដើម",
|
||||||
|
"Live": "ផ្សាយផ្ទាល់",
|
||||||
|
"Destroy": "លុប",
|
||||||
|
"Map faces": "ផែនទីមុខ",
|
||||||
|
"Processing...": "កំពុងដំណើរការ...",
|
||||||
|
"Processing succeed!": "ការដំណើរការទទួលបានជោគជ័យ!",
|
||||||
|
"Processing ignored!": "ការដំណើរការមិនទទួលបានជោគជ័យ!",
|
||||||
|
"Failed to start camera": "បរាជ័យដើម្បីចាប់ផ្ដើមបើកកាមេរ៉ា",
|
||||||
|
"Please complete pop-up or close it.": "សូមបញ្ចប់ផ្ទាំងផុស ឬបិទវា.",
|
||||||
|
"Getting unique faces": "ការចាប់ផ្ដើមទម្រង់មុខប្លែក",
|
||||||
|
"Please select a source image first": "សូមជ្រើសរើសប្រភពរូបភាពដំបូង",
|
||||||
|
"No faces found in target": "រកអត់ឃើញមុខនៅក្នុងគោលដៅ",
|
||||||
|
"Add": "បន្ថែម",
|
||||||
|
"Clear": "សម្អាត",
|
||||||
|
"Submit": "បញ្ចូន",
|
||||||
|
"Select source image": "ជ្រើសរើសប្រភពរូបភាព",
|
||||||
|
"Select target image": "ជ្រើសរើសគោលដៅរូបភាព",
|
||||||
|
"Please provide mapping!": "សូមផ្ដល់នៅផែនទី",
|
||||||
|
"At least 1 source with target is required!": "ត្រូវការប្រភពយ៉ាងហោចណាស់ ១ ដែលមានគោលដៅ!",
|
||||||
|
"Face could not be detected in last upload!": "មុខមិនអាចភ្ជាប់នៅក្នុងការបង្ហេាះចុងក្រោយ!",
|
||||||
|
"Select Camera:": "ជ្រើសរើសកាមេរ៉ា",
|
||||||
|
"All mappings cleared!": "ផែនទីទាំងអស់ត្រូវបានសម្អាត!",
|
||||||
|
"Mappings successfully submitted!": "ផែនទីត្រូវបានបញ្ជូនជោគជ័យ!",
|
||||||
|
"Source x Target Mapper is already open.": "ប្រភព x Target Mapper បានបើករួចហើយ។"
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "소스 x 타겟 매퍼",
|
||||||
|
"select a source image": "소스 이미지 선택",
|
||||||
|
"Preview": "미리보기",
|
||||||
|
"select a target image or video": "타겟 이미지 또는 영상 선택",
|
||||||
|
"save image output file": "이미지 출력 파일 저장",
|
||||||
|
"save video output file": "영상 출력 파일 저장",
|
||||||
|
"select a target image": "타겟 이미지 선택",
|
||||||
|
"source": "소스",
|
||||||
|
"Select a target": "타겟 선택",
|
||||||
|
"Select a face": "얼굴 선택",
|
||||||
|
"Keep audio": "오디오 유지",
|
||||||
|
"Face Enhancer": "얼굴 향상",
|
||||||
|
"Many faces": "여러 얼굴",
|
||||||
|
"Show FPS": "FPS 표시",
|
||||||
|
"Keep fps": "FPS 유지",
|
||||||
|
"Keep frames": "프레임 유지",
|
||||||
|
"Fix Blueish Cam": "푸른빛 카메라 보정",
|
||||||
|
"Mouth Mask": "입 마스크",
|
||||||
|
"Show Mouth Mask Box": "입 마스크 박스 표시",
|
||||||
|
"Start": "시작",
|
||||||
|
"Live": "라이브",
|
||||||
|
"Destroy": "종료",
|
||||||
|
"Map faces": "얼굴 매핑",
|
||||||
|
"Processing...": "처리 중...",
|
||||||
|
"Processing succeed!": "처리 성공!",
|
||||||
|
"Processing ignored!": "처리 무시됨!",
|
||||||
|
"Failed to start camera": "카메라 시작 실패",
|
||||||
|
"Please complete pop-up or close it.": "팝업을 완료하거나 닫아주세요.",
|
||||||
|
"Getting unique faces": "고유 얼굴 가져오는 중",
|
||||||
|
"Please select a source image first": "먼저 소스 이미지를 선택해주세요",
|
||||||
|
"No faces found in target": "타겟에서 얼굴을 찾을 수 없음",
|
||||||
|
"Add": "추가",
|
||||||
|
"Clear": "지우기",
|
||||||
|
"Submit": "제출",
|
||||||
|
"Select source image": "소스 이미지 선택",
|
||||||
|
"Select target image": "타겟 이미지 선택",
|
||||||
|
"Please provide mapping!": "매핑을 입력해주세요!",
|
||||||
|
"At least 1 source with target is required!": "최소 하나의 소스와 타겟이 필요합니다!",
|
||||||
|
"Face could not be detected in last upload!": "최근 업로드에서 얼굴을 감지할 수 없습니다!",
|
||||||
|
"Select Camera:": "카메라 선택:",
|
||||||
|
"All mappings cleared!": "모든 매핑이 삭제되었습니다!",
|
||||||
|
"Mappings successfully submitted!": "매핑이 성공적으로 제출되었습니다!",
|
||||||
|
"Source x Target Mapper is already open.": "소스 x 타겟 매퍼가 이미 열려 있습니다."
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Mapeador de Origem x Destino",
|
||||||
|
"select an source image": "Escolha uma imagem de origem",
|
||||||
|
"Preview": "Prévia",
|
||||||
|
"select an target image or video": "Escolha uma imagem ou vídeo de destino",
|
||||||
|
"save image output file": "Salvar imagem final",
|
||||||
|
"save video output file": "Salvar vídeo final",
|
||||||
|
"select an target image": "Escolha uma imagem de destino",
|
||||||
|
"source": "Origem",
|
||||||
|
"Select a target": "Escolha o destino",
|
||||||
|
"Select a face": "Escolha um rosto",
|
||||||
|
"Keep audio": "Manter o áudio original",
|
||||||
|
"Face Enhancer": "Melhorar rosto",
|
||||||
|
"Many faces": "Vários rostos",
|
||||||
|
"Show FPS": "Mostrar FPS",
|
||||||
|
"Keep fps": "Manter FPS",
|
||||||
|
"Keep frames": "Manter frames",
|
||||||
|
"Fix Blueish Cam": "Corrigir tom azulado da câmera",
|
||||||
|
"Mouth Mask": "Máscara da boca",
|
||||||
|
"Show Mouth Mask Box": "Mostrar área da máscara da boca",
|
||||||
|
"Start": "Começar",
|
||||||
|
"Live": "Ao vivo",
|
||||||
|
"Destroy": "Destruir",
|
||||||
|
"Map faces": "Mapear rostos",
|
||||||
|
"Processing...": "Processando...",
|
||||||
|
"Processing succeed!": "Tudo certo!",
|
||||||
|
"Processing ignored!": "Processamento ignorado!",
|
||||||
|
"Failed to start camera": "Não foi possível iniciar a câmera",
|
||||||
|
"Please complete pop-up or close it.": "Finalize ou feche o pop-up",
|
||||||
|
"Getting unique faces": "Buscando rostos diferentes",
|
||||||
|
"Please select a source image first": "Selecione primeiro uma imagem de origem",
|
||||||
|
"No faces found in target": "Nenhum rosto encontrado na imagem de destino",
|
||||||
|
"Add": "Adicionar",
|
||||||
|
"Clear": "Limpar",
|
||||||
|
"Submit": "Enviar",
|
||||||
|
"Select source image": "Escolha a imagem de origem",
|
||||||
|
"Select target image": "Escolha a imagem de destino",
|
||||||
|
"Please provide mapping!": "Você precisa realizar o mapeamento!",
|
||||||
|
"Atleast 1 source with target is required!": "É necessária pelo menos uma origem com um destino!",
|
||||||
|
"At least 1 source with target is required!": "É necessária pelo menos uma origem com um destino!",
|
||||||
|
"Face could not be detected in last upload!": "Não conseguimos detectar o rosto na última imagem!",
|
||||||
|
"Select Camera:": "Escolher câmera:",
|
||||||
|
"All mappings cleared!": "Todos os mapeamentos foram removidos!",
|
||||||
|
"Mappings successfully submitted!": "Mapeamentos enviados com sucesso!",
|
||||||
|
"Source x Target Mapper is already open.": "O Mapeador de Origem x Destino já está aberto."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "Сопоставитель Источник x Цель",
|
||||||
|
"select a source image": "выберите исходное изображение",
|
||||||
|
"Preview": "Предпросмотр",
|
||||||
|
"select a target image or video": "выберите целевое изображение или видео",
|
||||||
|
"save image output file": "сохранить выходной файл изображения",
|
||||||
|
"save video output file": "сохранить выходной файл видео",
|
||||||
|
"select a target image": "выберите целевое изображение",
|
||||||
|
"source": "источник",
|
||||||
|
"Select a target": "Выберите целевое изображение",
|
||||||
|
"Select a face": "Выберите лицо",
|
||||||
|
"Keep audio": "Сохранить аудио",
|
||||||
|
"Face Enhancer": "Улучшение лица",
|
||||||
|
"Many faces": "Несколько лиц",
|
||||||
|
"Show FPS": "Показать FPS",
|
||||||
|
"Keep fps": "Сохранить FPS",
|
||||||
|
"Keep frames": "Сохранить кадры",
|
||||||
|
"Fix Blueish Cam": "Исправить синеву камеры",
|
||||||
|
"Mouth Mask": "Маска рта",
|
||||||
|
"Show Mouth Mask Box": "Показать рамку маски рта",
|
||||||
|
"Start": "Старт",
|
||||||
|
"Live": "В реальном времени",
|
||||||
|
"Destroy": "Остановить",
|
||||||
|
"Map faces": "Сопоставить лица",
|
||||||
|
"Processing...": "Обработка...",
|
||||||
|
"Processing succeed!": "Обработка успешна!",
|
||||||
|
"Processing ignored!": "Обработка проигнорирована!",
|
||||||
|
"Failed to start camera": "Не удалось запустить камеру",
|
||||||
|
"Please complete pop-up or close it.": "Пожалуйста, заполните всплывающее окно или закройте его.",
|
||||||
|
"Getting unique faces": "Получение уникальных лиц",
|
||||||
|
"Please select a source image first": "Сначала выберите исходное изображение, пожалуйста",
|
||||||
|
"No faces found in target": "В целевом изображении не найдено лиц",
|
||||||
|
"Add": "Добавить",
|
||||||
|
"Clear": "Очистить",
|
||||||
|
"Submit": "Отправить",
|
||||||
|
"Select source image": "Выбрать исходное изображение",
|
||||||
|
"Select target image": "Выбрать целевое изображение",
|
||||||
|
"Please provide mapping!": "Пожалуйста, укажите сопоставление!",
|
||||||
|
"At least 1 source with target is required!": "Требуется хотя бы 1 источник с целью!",
|
||||||
|
"Face could not be detected in last upload!": "Лицо не обнаружено в последнем загруженном изображении!",
|
||||||
|
"Select Camera:": "Выберите камеру:",
|
||||||
|
"All mappings cleared!": "Все сопоставления очищены!",
|
||||||
|
"Mappings successfully submitted!": "Сопоставления успешно отправлены!",
|
||||||
|
"Source x Target Mapper is already open.": "Сопоставитель Источник-Цель уже открыт."
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"Source x Target Mapper": "ตัวจับคู่ต้นทาง x ปลายทาง",
|
||||||
|
"select a source image": "เลือกรูปภาพต้นฉบับ",
|
||||||
|
"Preview": "ตัวอย่าง",
|
||||||
|
"select a target image or video": "เลือกรูปภาพหรือวิดีโอเป้าหมาย",
|
||||||
|
"save image output file": "บันทึกไฟล์รูปภาพ",
|
||||||
|
"save video output file": "บันทึกไฟล์วิดีโอ",
|
||||||
|
"select a target image": "เลือกรูปภาพเป้าหมาย",
|
||||||
|
"source": "ต้นฉบับ",
|
||||||
|
"Select a target": "เลือกเป้าหมาย",
|
||||||
|
"Select a face": "เลือกใบหน้า",
|
||||||
|
"Keep audio": "เก็บเสียง",
|
||||||
|
"Face Enhancer": "ปรับปรุงใบหน้า",
|
||||||
|
"Many faces": "หลายใบหน้า",
|
||||||
|
"Show FPS": "แสดง FPS",
|
||||||
|
"Keep fps": "คงค่า FPS",
|
||||||
|
"Keep frames": "คงค่าเฟรม",
|
||||||
|
"Fix Blueish Cam": "แก้ไขภาพอมฟ้าจากกล้อง",
|
||||||
|
"Mouth Mask": "มาสก์ปาก",
|
||||||
|
"Show Mouth Mask Box": "แสดงกรอบมาสก์ปาก",
|
||||||
|
"Start": "เริ่ม",
|
||||||
|
"Live": "สด",
|
||||||
|
"Destroy": "หยุด",
|
||||||
|
"Map faces": "จับคู่ใบหน้า",
|
||||||
|
"Processing...": "กำลังประมวลผล...",
|
||||||
|
"Processing succeed!": "ประมวลผลสำเร็จแล้ว!",
|
||||||
|
"Processing ignored!": "การประมวลผลถูกละเว้น",
|
||||||
|
"Failed to start camera": "ไม่สามารถเริ่มกล้องได้",
|
||||||
|
"Please complete pop-up or close it.": "โปรดดำเนินการในป๊อปอัปให้เสร็จสิ้น หรือปิด",
|
||||||
|
"Getting unique faces": "กำลังค้นหาใบหน้าที่ไม่ซ้ำกัน",
|
||||||
|
"Please select a source image first": "โปรดเลือกภาพต้นฉบับก่อน",
|
||||||
|
"No faces found in target": "ไม่พบใบหน้าในภาพเป้าหมาย",
|
||||||
|
"Add": "เพิ่ม",
|
||||||
|
"Clear": "ล้าง",
|
||||||
|
"Submit": "ส่ง",
|
||||||
|
"Select source image": "เลือกภาพต้นฉบับ",
|
||||||
|
"Select target image": "เลือกภาพเป้าหมาย",
|
||||||
|
"Please provide mapping!": "โปรดระบุการจับคู่!",
|
||||||
|
"At least 1 source with target is required!": "ต้องมีการจับคู่ต้นฉบับกับเป้าหมายอย่างน้อย 1 คู่!",
|
||||||
|
"Face could not be detected in last upload!": "ไม่สามารถตรวจพบใบหน้าในไฟล์อัปโหลดล่าสุด!",
|
||||||
|
"Select Camera:": "เลือกกล้อง:",
|
||||||
|
"All mappings cleared!": "ล้างการจับคู่ทั้งหมดแล้ว!",
|
||||||
|
"Mappings successfully submitted!": "ส่งการจับคู่สำเร็จแล้ว!",
|
||||||
|
"Source x Target Mapper is already open.": "ตัวจับคู่ต้นทาง x ปลายทาง เปิดอยู่แล้ว"
|
||||||
|
}
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"Source x Target Mapper": "Source x Target Mapper",
|
"Source x Target Mapper": "Source x Target Mapper",
|
||||||
"select an source image": "选择一个源图像",
|
"select a source image": "选择一个源图像",
|
||||||
"Preview": "预览",
|
"Preview": "预览",
|
||||||
"select an target image or video": "选择一个目标图像或视频",
|
"select a target image or video": "选择一个目标图像或视频",
|
||||||
"save image output file": "保存图像输出文件",
|
"save image output file": "保存图像输出文件",
|
||||||
"save video output file": "保存视频输出文件",
|
"save video output file": "保存视频输出文件",
|
||||||
"select an target image": "选择一个目标图像",
|
"select a target image": "选择一个目标图像",
|
||||||
"source": "源",
|
"source": "源",
|
||||||
"Select a target": "选择一个目标",
|
"Select a target": "选择一个目标",
|
||||||
"Select a face": "选择一张脸",
|
"Select a face": "选择一张脸",
|
||||||
@@ -36,11 +36,11 @@
|
|||||||
"Select source image": "请选取源图像",
|
"Select source image": "请选取源图像",
|
||||||
"Select target image": "请选取目标图像",
|
"Select target image": "请选取目标图像",
|
||||||
"Please provide mapping!": "请提供映射",
|
"Please provide mapping!": "请提供映射",
|
||||||
"Atleast 1 source with target is required!": "至少需要一个来源图像与目标图像相关!",
|
"At least 1 source with target is required!": "至少需要一个来源图像与目标图像相关!",
|
||||||
"At least 1 source with target is required!": "至少需要一个来源图像与目标图像相关!",
|
"At least 1 source with target is required!": "至少需要一个来源图像与目标图像相关!",
|
||||||
"Face could not be detected in last upload!": "最近上传的图像中没有检测到人脸!",
|
"Face could not be detected in last upload!": "最近上传的图像中没有检测到人脸!",
|
||||||
"Select Camera:": "选择摄像头",
|
"Select Camera:": "选择摄像头",
|
||||||
"All mappings cleared!": "所有映射均已清除!",
|
"All mappings cleared!": "所有映射均已清除!",
|
||||||
"Mappings successfully submitted!": "成功提交映射!",
|
"Mappings successfully submitted!": "成功提交映射!",
|
||||||
"Source x Target Mapper is already open.": "源 x 目标映射器已打开。"
|
"Source x Target Mapper is already open.": "源 x 目标映射器已打开。"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Utility function to support unicode characters in file paths for reading
|
||||||
|
def imread_unicode(path, flags=cv2.IMREAD_COLOR):
|
||||||
|
return cv2.imdecode(np.fromfile(path, dtype=np.uint8), flags)
|
||||||
|
|
||||||
|
# Utility function to support unicode characters in file paths for writing
|
||||||
|
def imwrite_unicode(path, img, params=None):
|
||||||
|
root, ext = os.path.splitext(path)
|
||||||
|
if not ext:
|
||||||
|
ext = ".png"
|
||||||
|
result, encoded_img = cv2.imencode(ext, img, params if params else [])
|
||||||
|
result, encoded_img = cv2.imencode(f".{ext}", img, params if params is not None else [])
|
||||||
|
encoded_img.tofile(path)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
import cv2
|
import cv2
|
||||||
import modules.globals # Import the globals to check the color correction toggle
|
import modules.globals # Import the globals to check the color correction toggle
|
||||||
|
from modules.gpu_processing import gpu_cvt_color
|
||||||
|
|
||||||
|
|
||||||
def get_video_frame(video_path: str, frame_number: int = 0) -> Any:
|
def get_video_frame(video_path: str, frame_number: int = 0) -> Any:
|
||||||
@@ -19,7 +20,7 @@ def get_video_frame(video_path: str, frame_number: int = 0) -> Any:
|
|||||||
|
|
||||||
if has_frame and modules.globals.color_correction:
|
if has_frame and modules.globals.color_correction:
|
||||||
# Convert the frame color if necessary
|
# Convert the frame color if necessary
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
frame = gpu_cvt_color(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
capture.release()
|
capture.release()
|
||||||
return frame if has_frame else None
|
return frame if has_frame else None
|
||||||
|
|||||||
+53
-13
@@ -11,7 +11,11 @@ import platform
|
|||||||
import signal
|
import signal
|
||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import argparse
|
||||||
import torch
|
try:
|
||||||
|
import torch
|
||||||
|
HAS_TORCH = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_TORCH = False
|
||||||
import onnxruntime
|
import onnxruntime
|
||||||
import tensorflow
|
import tensorflow
|
||||||
|
|
||||||
@@ -21,11 +25,12 @@ import modules.ui as ui
|
|||||||
from modules.processors.frame.core import get_frame_processors_modules
|
from modules.processors.frame.core import get_frame_processors_modules
|
||||||
from modules.utilities import has_image_extension, is_image, is_video, detect_fps, create_video, extract_frames, get_temp_frame_paths, restore_audio, create_temp, move_temp, clean_temp, normalize_output_path
|
from modules.utilities import has_image_extension, is_image, is_video, detect_fps, create_video, extract_frames, get_temp_frame_paths, restore_audio, create_temp, move_temp, clean_temp, normalize_output_path
|
||||||
|
|
||||||
if 'ROCMExecutionProvider' in modules.globals.execution_providers:
|
if HAS_TORCH and 'ROCMExecutionProvider' in modules.globals.execution_providers:
|
||||||
del torch
|
del torch
|
||||||
|
|
||||||
warnings.filterwarnings('ignore', category=FutureWarning, module='insightface')
|
warnings.filterwarnings('ignore', category=FutureWarning, module='insightface')
|
||||||
warnings.filterwarnings('ignore', category=UserWarning, module='torchvision')
|
if HAS_TORCH:
|
||||||
|
warnings.filterwarnings('ignore', category=UserWarning, module='torchvision')
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> None:
|
def parse_args() -> None:
|
||||||
@@ -34,7 +39,7 @@ def parse_args() -> None:
|
|||||||
program.add_argument('-s', '--source', help='select an source image', dest='source_path')
|
program.add_argument('-s', '--source', help='select an source image', dest='source_path')
|
||||||
program.add_argument('-t', '--target', help='select an target image or video', dest='target_path')
|
program.add_argument('-t', '--target', help='select an target image or video', dest='target_path')
|
||||||
program.add_argument('-o', '--output', help='select output file or directory', dest='output_path')
|
program.add_argument('-o', '--output', help='select output file or directory', dest='output_path')
|
||||||
program.add_argument('--frame-processor', help='pipeline of frame processors', dest='frame_processor', default=['face_swapper'], choices=['face_swapper', 'face_enhancer'], nargs='+')
|
program.add_argument('--frame-processor', help='pipeline of frame processors', dest='frame_processor', default=['face_swapper'], choices=['face_swapper', 'face_enhancer', 'face_enhancer_gpen256', 'face_enhancer_gpen512'], nargs='+')
|
||||||
program.add_argument('--keep-fps', help='keep original fps', dest='keep_fps', action='store_true', default=False)
|
program.add_argument('--keep-fps', help='keep original fps', dest='keep_fps', action='store_true', default=False)
|
||||||
program.add_argument('--keep-audio', help='keep original audio', dest='keep_audio', action='store_true', default=True)
|
program.add_argument('--keep-audio', help='keep original audio', dest='keep_audio', action='store_true', default=True)
|
||||||
program.add_argument('--keep-frames', help='keep temporary frames', dest='keep_frames', action='store_true', default=False)
|
program.add_argument('--keep-frames', help='keep temporary frames', dest='keep_frames', action='store_true', default=False)
|
||||||
@@ -81,11 +86,9 @@ def parse_args() -> None:
|
|||||||
modules.globals.execution_threads = args.execution_threads
|
modules.globals.execution_threads = args.execution_threads
|
||||||
modules.globals.lang = args.lang
|
modules.globals.lang = args.lang
|
||||||
|
|
||||||
#for ENHANCER tumbler:
|
#for ENHANCER tumblers:
|
||||||
if 'face_enhancer' in args.frame_processor:
|
for enhancer_key in ('face_enhancer', 'face_enhancer_gpen256', 'face_enhancer_gpen512'):
|
||||||
modules.globals.fp_ui['face_enhancer'] = True
|
modules.globals.fp_ui[enhancer_key] = enhancer_key in args.frame_processor
|
||||||
else:
|
|
||||||
modules.globals.fp_ui['face_enhancer'] = False
|
|
||||||
|
|
||||||
# translate deprecated args
|
# translate deprecated args
|
||||||
if args.source_path_deprecated:
|
if args.source_path_deprecated:
|
||||||
@@ -129,11 +132,22 @@ def suggest_execution_providers() -> List[str]:
|
|||||||
|
|
||||||
|
|
||||||
def suggest_execution_threads() -> int:
|
def suggest_execution_threads() -> int:
|
||||||
|
"""Suggest optimal thread count based on hardware and execution provider."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Get CPU count
|
||||||
|
cpu_count = os.cpu_count() or 4
|
||||||
|
|
||||||
if 'DmlExecutionProvider' in modules.globals.execution_providers:
|
if 'DmlExecutionProvider' in modules.globals.execution_providers:
|
||||||
return 1
|
return 1
|
||||||
if 'ROCMExecutionProvider' in modules.globals.execution_providers:
|
if 'ROCMExecutionProvider' in modules.globals.execution_providers:
|
||||||
return 1
|
return 1
|
||||||
return 8
|
if 'CUDAExecutionProvider' in modules.globals.execution_providers:
|
||||||
|
# For CUDA, use more threads for parallel frame processing
|
||||||
|
return min(cpu_count, 16)
|
||||||
|
|
||||||
|
# For CPU execution, use most cores but leave some for system
|
||||||
|
return max(4, min(cpu_count - 2, 16))
|
||||||
|
|
||||||
|
|
||||||
def limit_resources() -> None:
|
def limit_resources() -> None:
|
||||||
@@ -156,7 +170,7 @@ def limit_resources() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def release_resources() -> None:
|
def release_resources() -> None:
|
||||||
if 'CUDAExecutionProvider' in modules.globals.execution_providers:
|
if 'CUDAExecutionProvider' in modules.globals.execution_providers and HAS_TORCH:
|
||||||
torch.cuda.empty_cache()
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
|
||||||
@@ -176,10 +190,16 @@ def update_status(message: str, scope: str = 'DLC.CORE') -> None:
|
|||||||
ui.update_status(message)
|
ui.update_status(message)
|
||||||
|
|
||||||
def start() -> None:
|
def start() -> None:
|
||||||
|
"""Start processing with performance monitoring."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
for frame_processor in get_frame_processors_modules(modules.globals.frame_processors):
|
for frame_processor in get_frame_processors_modules(modules.globals.frame_processors):
|
||||||
if not frame_processor.pre_start():
|
if not frame_processor.pre_start():
|
||||||
return
|
return
|
||||||
update_status('Processing...')
|
update_status('Processing...')
|
||||||
|
|
||||||
# process image to image
|
# process image to image
|
||||||
if has_image_extension(modules.globals.target_path):
|
if has_image_extension(modules.globals.target_path):
|
||||||
if modules.globals.nsfw_filter and ui.check_and_ignore_nsfw(modules.globals.target_path, destroy):
|
if modules.globals.nsfw_filter and ui.check_and_ignore_nsfw(modules.globals.target_path, destroy):
|
||||||
@@ -193,26 +213,40 @@ def start() -> None:
|
|||||||
frame_processor.process_image(modules.globals.source_path, modules.globals.output_path, modules.globals.output_path)
|
frame_processor.process_image(modules.globals.source_path, modules.globals.output_path, modules.globals.output_path)
|
||||||
release_resources()
|
release_resources()
|
||||||
if is_image(modules.globals.target_path):
|
if is_image(modules.globals.target_path):
|
||||||
update_status('Processing to image succeed!')
|
elapsed = time.time() - start_time
|
||||||
|
update_status(f'Processing to image succeed! (Time: {elapsed:.2f}s)')
|
||||||
else:
|
else:
|
||||||
update_status('Processing to image failed!')
|
update_status('Processing to image failed!')
|
||||||
return
|
return
|
||||||
|
|
||||||
# process image to videos
|
# process image to videos
|
||||||
if modules.globals.nsfw_filter and ui.check_and_ignore_nsfw(modules.globals.target_path, destroy):
|
if modules.globals.nsfw_filter and ui.check_and_ignore_nsfw(modules.globals.target_path, destroy):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
extraction_start = time.time()
|
||||||
if not modules.globals.map_faces:
|
if not modules.globals.map_faces:
|
||||||
update_status('Creating temp resources...')
|
update_status('Creating temp resources...')
|
||||||
create_temp(modules.globals.target_path)
|
create_temp(modules.globals.target_path)
|
||||||
update_status('Extracting frames...')
|
update_status('Extracting frames...')
|
||||||
extract_frames(modules.globals.target_path)
|
extract_frames(modules.globals.target_path)
|
||||||
|
extraction_time = time.time() - extraction_start
|
||||||
|
update_status(f'Frame extraction completed in {extraction_time:.2f}s')
|
||||||
|
|
||||||
temp_frame_paths = get_temp_frame_paths(modules.globals.target_path)
|
temp_frame_paths = get_temp_frame_paths(modules.globals.target_path)
|
||||||
|
total_frames = len(temp_frame_paths)
|
||||||
|
update_status(f'Processing {total_frames} frames with {modules.globals.execution_threads} threads...')
|
||||||
|
|
||||||
|
processing_start = time.time()
|
||||||
for frame_processor in get_frame_processors_modules(modules.globals.frame_processors):
|
for frame_processor in get_frame_processors_modules(modules.globals.frame_processors):
|
||||||
update_status('Progressing...', frame_processor.NAME)
|
update_status('Progressing...', frame_processor.NAME)
|
||||||
frame_processor.process_video(modules.globals.source_path, temp_frame_paths)
|
frame_processor.process_video(modules.globals.source_path, temp_frame_paths)
|
||||||
release_resources()
|
release_resources()
|
||||||
|
processing_time = time.time() - processing_start
|
||||||
|
fps_processing = total_frames / processing_time if processing_time > 0 else 0
|
||||||
|
update_status(f'Frame processing completed in {processing_time:.2f}s ({fps_processing:.2f} fps)')
|
||||||
|
|
||||||
# handles fps
|
# handles fps
|
||||||
|
encoding_start = time.time()
|
||||||
if modules.globals.keep_fps:
|
if modules.globals.keep_fps:
|
||||||
update_status('Detecting fps...')
|
update_status('Detecting fps...')
|
||||||
fps = detect_fps(modules.globals.target_path)
|
fps = detect_fps(modules.globals.target_path)
|
||||||
@@ -221,6 +255,9 @@ def start() -> None:
|
|||||||
else:
|
else:
|
||||||
update_status('Creating video with 30.0 fps...')
|
update_status('Creating video with 30.0 fps...')
|
||||||
create_video(modules.globals.target_path)
|
create_video(modules.globals.target_path)
|
||||||
|
encoding_time = time.time() - encoding_start
|
||||||
|
update_status(f'Video encoding completed in {encoding_time:.2f}s')
|
||||||
|
|
||||||
# handle audio
|
# handle audio
|
||||||
if modules.globals.keep_audio:
|
if modules.globals.keep_audio:
|
||||||
if modules.globals.keep_fps:
|
if modules.globals.keep_fps:
|
||||||
@@ -230,10 +267,13 @@ def start() -> None:
|
|||||||
restore_audio(modules.globals.target_path, modules.globals.output_path)
|
restore_audio(modules.globals.target_path, modules.globals.output_path)
|
||||||
else:
|
else:
|
||||||
move_temp(modules.globals.target_path, modules.globals.output_path)
|
move_temp(modules.globals.target_path, modules.globals.output_path)
|
||||||
|
|
||||||
# clean and validate
|
# clean and validate
|
||||||
clean_temp(modules.globals.target_path)
|
clean_temp(modules.globals.target_path)
|
||||||
|
|
||||||
|
total_time = time.time() - start_time
|
||||||
if is_video(modules.globals.target_path):
|
if is_video(modules.globals.target_path):
|
||||||
update_status('Processing to video succeed!')
|
update_status(f'Processing to video succeed! Total time: {total_time:.2f}s')
|
||||||
else:
|
else:
|
||||||
update_status('Processing to video failed!')
|
update_status('Processing to video failed!')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from insightface.app.common import Face
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
Face = Face
|
||||||
|
Frame = numpy.ndarray[Any, Any]
|
||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import insightface
|
import insightface
|
||||||
|
import threading
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -13,14 +14,23 @@ from modules.utilities import get_temp_directory_path, create_temp, extract_fram
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
FACE_ANALYSER = None
|
FACE_ANALYSER = None
|
||||||
|
FACE_ANALYSER_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def get_face_analyser() -> Any:
|
def get_face_analyser() -> Any:
|
||||||
|
"""Get face analyser with thread-safe initialization."""
|
||||||
global FACE_ANALYSER
|
global FACE_ANALYSER
|
||||||
|
|
||||||
if FACE_ANALYSER is None:
|
if FACE_ANALYSER is None:
|
||||||
FACE_ANALYSER = insightface.app.FaceAnalysis(name='buffalo_l', providers=modules.globals.execution_providers)
|
with FACE_ANALYSER_LOCK:
|
||||||
FACE_ANALYSER.prepare(ctx_id=0, det_size=(640, 640))
|
# Double-check after acquiring lock
|
||||||
|
if FACE_ANALYSER is None:
|
||||||
|
FACE_ANALYSER = insightface.app.FaceAnalysis(
|
||||||
|
name='buffalo_l',
|
||||||
|
providers=modules.globals.execution_providers,
|
||||||
|
allowed_modules=['detection', 'recognition', 'landmark_2d_106']
|
||||||
|
)
|
||||||
|
FACE_ANALYSER.prepare(ctx_id=0, det_size=(640, 640))
|
||||||
return FACE_ANALYSER
|
return FACE_ANALYSER
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+60
-30
@@ -1,3 +1,5 @@
|
|||||||
|
# --- START OF FILE globals.py ---
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
@@ -9,35 +11,63 @@ file_types = [
|
|||||||
("Video", ("*.mp4", "*.mkv")),
|
("Video", ("*.mp4", "*.mkv")),
|
||||||
]
|
]
|
||||||
|
|
||||||
source_target_map = []
|
# Face Mapping Data
|
||||||
simple_map = {}
|
source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing
|
||||||
|
simple_map: Dict[str, Any] = {} # Stores simplified map (embeddings/faces) for live/simple mode
|
||||||
|
|
||||||
source_path = None
|
# Paths
|
||||||
target_path = None
|
source_path: str | None = None
|
||||||
output_path = None
|
target_path: str | None = None
|
||||||
|
output_path: str | None = None
|
||||||
|
|
||||||
|
# Processing Options
|
||||||
frame_processors: List[str] = []
|
frame_processors: List[str] = []
|
||||||
keep_fps = True
|
keep_fps: bool = True
|
||||||
keep_audio = True
|
keep_audio: bool = True
|
||||||
keep_frames = False
|
keep_frames: bool = False
|
||||||
many_faces = False
|
many_faces: bool = False # Process all detected faces with default source
|
||||||
map_faces = False
|
map_faces: bool = False # Use source_target_map or simple_map for specific swaps
|
||||||
color_correction = False # New global variable for color correction toggle
|
poisson_blend: bool = False # Enable Poisson Blending for smoother face swaps
|
||||||
nsfw_filter = False
|
color_correction: bool = False # Enable color correction (implementation specific)
|
||||||
video_encoder = None
|
nsfw_filter: bool = False
|
||||||
video_quality = None
|
|
||||||
live_mirror = False
|
# Video Output Options
|
||||||
live_resizable = True
|
video_encoder: str | None = None
|
||||||
max_memory = None
|
video_quality: int | None = None # Typically a CRF value or bitrate
|
||||||
execution_providers: List[str] = []
|
|
||||||
execution_threads = None
|
# Live Mode Options
|
||||||
headless = None
|
live_mirror: bool = False
|
||||||
log_level = "error"
|
live_resizable: bool = True
|
||||||
fp_ui: Dict[str, bool] = {"face_enhancer": False}
|
camera_input_combobox: Any | None = None # Placeholder for UI element if needed
|
||||||
camera_input_combobox = None
|
webcam_preview_running: bool = False
|
||||||
webcam_preview_running = False
|
show_fps: bool = False
|
||||||
show_fps = False
|
|
||||||
mouth_mask = False
|
# System Configuration
|
||||||
show_mouth_mask_box = False
|
max_memory: int | None = None # Memory limit in GB? (Needs clarification)
|
||||||
mask_feather_ratio = 8
|
execution_providers: List[str] = [] # e.g., ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
||||||
mask_down_size = 0.50
|
execution_threads: int | None = None # Number of threads for CPU execution
|
||||||
mask_size = 1
|
headless: bool | None = None # Run without UI?
|
||||||
|
log_level: str = "error" # Logging level (e.g., 'debug', 'info', 'warning', 'error')
|
||||||
|
|
||||||
|
# Face Processor UI Toggles (Example)
|
||||||
|
fp_ui: Dict[str, bool] = {"face_enhancer": False, "face_enhancer_gpen256": False, "face_enhancer_gpen512": False}
|
||||||
|
|
||||||
|
# Face Swapper Specific Options
|
||||||
|
face_swapper_enabled: bool = True # General toggle for the swapper processor
|
||||||
|
opacity: float = 1.0 # Blend factor for the swapped face (0.0-1.0)
|
||||||
|
sharpness: float = 0.0 # Sharpness enhancement for swapped face (0.0-1.0+)
|
||||||
|
|
||||||
|
# Mouth Mask Options
|
||||||
|
mouth_mask: bool = False # Enable mouth area masking/pasting
|
||||||
|
show_mouth_mask_box: bool = False # Visualize the mouth mask area (for debugging)
|
||||||
|
mask_feather_ratio: int = 12 # Denominator for feathering calculation (higher = smaller feather)
|
||||||
|
mask_down_size: float = 0.1 # Expansion factor for lower lip mask (relative)
|
||||||
|
mask_size: float = 1.0 # Expansion factor for upper lip mask (relative)
|
||||||
|
mouth_mask_size: float = 0.0 # Mouth mask size (0-100; 0=off, 100=mouth to chin)
|
||||||
|
|
||||||
|
# --- START: Added for Frame Interpolation ---
|
||||||
|
enable_interpolation: bool = True # Toggle temporal smoothing
|
||||||
|
interpolation_weight: float = 0 # Blend weight for current frame (0.0-1.0). Lower=smoother.
|
||||||
|
# --- END: Added for Frame Interpolation ---
|
||||||
|
|
||||||
|
# --- END OF FILE globals.py ---
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
# --- START OF FILE gpu_processing.py ---
|
||||||
|
"""
|
||||||
|
GPU-accelerated image processing using OpenCV CUDA (cv2.cuda.GpuMat).
|
||||||
|
|
||||||
|
Provides drop-in replacements for common cv2 functions. When OpenCV is built
|
||||||
|
with CUDA support the functions transparently upload → process → download via
|
||||||
|
GpuMat; otherwise they fall back to the regular CPU path so the rest of the
|
||||||
|
codebase never has to care whether CUDA is available.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
from modules.gpu_processing import (
|
||||||
|
gpu_gaussian_blur, gpu_sharpen, gpu_add_weighted,
|
||||||
|
gpu_resize, gpu_cvt_color, gpu_flip,
|
||||||
|
is_gpu_accelerated,
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CUDA availability detection (evaluated once at import time)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CUDA_AVAILABLE: bool = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# cv2.cuda.GpuMat is only present when OpenCV is compiled with CUDA
|
||||||
|
_test_mat = cv2.cuda.GpuMat()
|
||||||
|
# Verify we have the required filter / image-processing functions
|
||||||
|
_has_gauss = hasattr(cv2.cuda, "createGaussianFilter")
|
||||||
|
_has_resize = hasattr(cv2.cuda, "resize")
|
||||||
|
_has_cvt = hasattr(cv2.cuda, "cvtColor")
|
||||||
|
if _has_gauss and _has_resize and _has_cvt:
|
||||||
|
CUDA_AVAILABLE = True
|
||||||
|
print("[gpu_processing] OpenCV CUDA support detected – GPU-accelerated processing enabled.")
|
||||||
|
else:
|
||||||
|
missing = []
|
||||||
|
if not _has_gauss:
|
||||||
|
missing.append("createGaussianFilter")
|
||||||
|
if not _has_resize:
|
||||||
|
missing.append("resize")
|
||||||
|
if not _has_cvt:
|
||||||
|
missing.append("cvtColor")
|
||||||
|
print(f"[gpu_processing] cv2.cuda.GpuMat exists but missing: {', '.join(missing)} – falling back to CPU.")
|
||||||
|
except Exception:
|
||||||
|
print("[gpu_processing] OpenCV CUDA not available – using CPU fallback for all operations.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _ensure_uint8(img: np.ndarray) -> np.ndarray:
|
||||||
|
"""Clip and convert to uint8 if necessary."""
|
||||||
|
if img.dtype != np.uint8:
|
||||||
|
return np.clip(img, 0, 255).astype(np.uint8)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _ksize_odd(ksize: Tuple[int, int]) -> Tuple[int, int]:
|
||||||
|
"""Ensure kernel dimensions are positive and odd (required by GaussianBlur)."""
|
||||||
|
kw = max(1, ksize[0] // 2 * 2 + 1) if ksize[0] > 0 else 0
|
||||||
|
kh = max(1, ksize[1] // 2 * 2 + 1) if ksize[1] > 0 else 0
|
||||||
|
return (kw, kh)
|
||||||
|
|
||||||
|
|
||||||
|
def _cv_type_for(img: np.ndarray) -> int:
|
||||||
|
"""Return the OpenCV type constant matching *img* (uint8 only)."""
|
||||||
|
channels = 1 if img.ndim == 2 else img.shape[2]
|
||||||
|
if channels == 1:
|
||||||
|
return cv2.CV_8UC1
|
||||||
|
elif channels == 3:
|
||||||
|
return cv2.CV_8UC3
|
||||||
|
elif channels == 4:
|
||||||
|
return cv2.CV_8UC4
|
||||||
|
return cv2.CV_8UC3 # fallback
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – Gaussian Blur
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gpu_gaussian_blur(
|
||||||
|
src: np.ndarray,
|
||||||
|
ksize: Tuple[int, int],
|
||||||
|
sigma_x: float,
|
||||||
|
sigma_y: float = 0,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Drop-in replacement for ``cv2.GaussianBlur`` with CUDA acceleration.
|
||||||
|
|
||||||
|
Parameters match ``cv2.GaussianBlur(src, ksize, sigmaX, sigmaY)``.
|
||||||
|
When *ksize* is ``(0, 0)`` OpenCV computes the kernel size from *sigma_x*.
|
||||||
|
"""
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
src_u8 = _ensure_uint8(src)
|
||||||
|
cv_type = _cv_type_for(src_u8)
|
||||||
|
ks = _ksize_odd(ksize) if ksize != (0, 0) else ksize
|
||||||
|
|
||||||
|
gauss = cv2.cuda.createGaussianFilter(cv_type, cv_type, ks, sigma_x, sigma_y)
|
||||||
|
gpu_src = cv2.cuda.GpuMat()
|
||||||
|
gpu_src.upload(src_u8)
|
||||||
|
gpu_dst = gauss.apply(gpu_src)
|
||||||
|
return gpu_dst.download()
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cv2.GaussianBlur(src, ksize, sigma_x, sigmaY=sigma_y)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – addWeighted
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gpu_add_weighted(
|
||||||
|
src1: np.ndarray,
|
||||||
|
alpha: float,
|
||||||
|
src2: np.ndarray,
|
||||||
|
beta: float,
|
||||||
|
gamma: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Drop-in replacement for ``cv2.addWeighted`` with CUDA acceleration."""
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
s1 = _ensure_uint8(src1)
|
||||||
|
s2 = _ensure_uint8(src2)
|
||||||
|
g1 = cv2.cuda.GpuMat()
|
||||||
|
g2 = cv2.cuda.GpuMat()
|
||||||
|
g1.upload(s1)
|
||||||
|
g2.upload(s2)
|
||||||
|
gpu_dst = cv2.cuda.addWeighted(g1, alpha, g2, beta, gamma)
|
||||||
|
return gpu_dst.download()
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cv2.addWeighted(src1, alpha, src2, beta, gamma)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – Unsharp-mask sharpening
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gpu_sharpen(
|
||||||
|
src: np.ndarray,
|
||||||
|
strength: float,
|
||||||
|
sigma: float = 3,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Unsharp-mask sharpening, optionally GPU-accelerated.
|
||||||
|
|
||||||
|
Equivalent to::
|
||||||
|
|
||||||
|
blurred = GaussianBlur(src, (0,0), sigma)
|
||||||
|
result = addWeighted(src, 1+strength, blurred, -strength, 0)
|
||||||
|
"""
|
||||||
|
if strength <= 0:
|
||||||
|
return src
|
||||||
|
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
src_u8 = _ensure_uint8(src)
|
||||||
|
cv_type = _cv_type_for(src_u8)
|
||||||
|
|
||||||
|
gauss = cv2.cuda.createGaussianFilter(cv_type, cv_type, (0, 0), sigma)
|
||||||
|
gpu_src = cv2.cuda.GpuMat()
|
||||||
|
gpu_src.upload(src_u8)
|
||||||
|
gpu_blurred = gauss.apply(gpu_src)
|
||||||
|
gpu_sharp = cv2.cuda.addWeighted(gpu_src, 1.0 + strength, gpu_blurred, -strength, 0)
|
||||||
|
result = gpu_sharp.download()
|
||||||
|
return np.clip(result, 0, 255).astype(np.uint8)
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
blurred = cv2.GaussianBlur(src, (0, 0), sigma)
|
||||||
|
sharpened = cv2.addWeighted(src, 1.0 + strength, blurred, -strength, 0)
|
||||||
|
return np.clip(sharpened, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – Resize
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Map common cv2 interpolation flags to their CUDA equivalents
|
||||||
|
_INTERP_MAP = {
|
||||||
|
cv2.INTER_NEAREST: cv2.INTER_NEAREST,
|
||||||
|
cv2.INTER_LINEAR: cv2.INTER_LINEAR,
|
||||||
|
cv2.INTER_CUBIC: cv2.INTER_CUBIC,
|
||||||
|
cv2.INTER_AREA: cv2.INTER_AREA,
|
||||||
|
cv2.INTER_LANCZOS4: cv2.INTER_LANCZOS4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_resize(
|
||||||
|
src: np.ndarray,
|
||||||
|
dsize: Tuple[int, int],
|
||||||
|
fx: float = 0,
|
||||||
|
fy: float = 0,
|
||||||
|
interpolation: int = cv2.INTER_LINEAR,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Drop-in replacement for ``cv2.resize`` with CUDA acceleration.
|
||||||
|
|
||||||
|
Parameters match ``cv2.resize(src, dsize, fx=fx, fy=fy, interpolation=...)``.
|
||||||
|
"""
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
src_u8 = _ensure_uint8(src)
|
||||||
|
gpu_src = cv2.cuda.GpuMat()
|
||||||
|
gpu_src.upload(src_u8)
|
||||||
|
|
||||||
|
interp = _INTERP_MAP.get(interpolation, cv2.INTER_LINEAR)
|
||||||
|
|
||||||
|
if dsize and dsize[0] > 0 and dsize[1] > 0:
|
||||||
|
gpu_dst = cv2.cuda.resize(gpu_src, dsize, interpolation=interp)
|
||||||
|
else:
|
||||||
|
gpu_dst = cv2.cuda.resize(gpu_src, (0, 0), fx=fx, fy=fy, interpolation=interp)
|
||||||
|
|
||||||
|
return gpu_dst.download()
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cv2.resize(src, dsize, fx=fx, fy=fy, interpolation=interpolation)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – Color conversion
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gpu_cvt_color(
|
||||||
|
src: np.ndarray,
|
||||||
|
code: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Drop-in replacement for ``cv2.cvtColor`` with CUDA acceleration.
|
||||||
|
|
||||||
|
Parameters match ``cv2.cvtColor(src, code)``.
|
||||||
|
"""
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
src_u8 = _ensure_uint8(src)
|
||||||
|
gpu_src = cv2.cuda.GpuMat()
|
||||||
|
gpu_src.upload(src_u8)
|
||||||
|
gpu_dst = cv2.cuda.cvtColor(gpu_src, code)
|
||||||
|
return gpu_dst.download()
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cv2.cvtColor(src, code)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API – Flip
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gpu_flip(
|
||||||
|
src: np.ndarray,
|
||||||
|
flip_code: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Drop-in replacement for ``cv2.flip`` with CUDA acceleration.
|
||||||
|
|
||||||
|
Parameters match ``cv2.flip(src, flipCode)``.
|
||||||
|
*flip_code*: 0 = vertical, 1 = horizontal, -1 = both.
|
||||||
|
"""
|
||||||
|
if CUDA_AVAILABLE:
|
||||||
|
try:
|
||||||
|
src_u8 = _ensure_uint8(src)
|
||||||
|
gpu_src = cv2.cuda.GpuMat()
|
||||||
|
gpu_src.upload(src_u8)
|
||||||
|
gpu_dst = cv2.cuda.flip(gpu_src, flip_code)
|
||||||
|
return gpu_dst.download()
|
||||||
|
except cv2.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cv2.flip(src, flip_code)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Convenience: check at runtime whether GPU path is active
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def is_gpu_accelerated() -> bool:
|
||||||
|
"""Return ``True`` when the CUDA path will be used."""
|
||||||
|
return CUDA_AVAILABLE
|
||||||
|
|
||||||
|
# --- END OF FILE gpu_processing.py ---
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
name = 'Deep-Live-Cam'
|
name = 'Deep-Live-Cam'
|
||||||
version = '1.8'
|
version = '2.1'
|
||||||
edition = 'GitHub Edition'
|
edition = 'GitHub Edition'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Shared path constants for the Deep-Live-Cam project."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
MODELS_DIR = os.path.join(ROOT_DIR, "models")
|
||||||
@@ -3,6 +3,7 @@ import opennsfw2
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cv2 # Add OpenCV import
|
import cv2 # Add OpenCV import
|
||||||
import modules.globals # Import globals to access the color correction toggle
|
import modules.globals # Import globals to access the color correction toggle
|
||||||
|
from modules.gpu_processing import gpu_cvt_color
|
||||||
|
|
||||||
from modules.typing import Frame
|
from modules.typing import Frame
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ model = None
|
|||||||
def predict_frame(target_frame: Frame) -> bool:
|
def predict_frame(target_frame: Frame) -> bool:
|
||||||
# Convert the frame to RGB before processing if color correction is enabled
|
# Convert the frame to RGB before processing if color correction is enabled
|
||||||
if modules.globals.color_correction:
|
if modules.globals.color_correction:
|
||||||
target_frame = cv2.cvtColor(target_frame, cv2.COLOR_BGR2RGB)
|
target_frame = gpu_cvt_color(target_frame, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
image = Image.fromarray(target_frame)
|
image = Image.fromarray(target_frame)
|
||||||
image = opennsfw2.preprocess_image(image, opennsfw2.Preprocessing.YAHOO)
|
image = opennsfw2.preprocess_image(image, opennsfw2.Preprocessing.YAHOO)
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""Shared ONNX-based face enhancement utilities for GPEN-BFR models.
|
||||||
|
|
||||||
|
Provides session creation, pre/post processing, and the core
|
||||||
|
enhance-face-via-ONNX pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import onnxruntime
|
||||||
|
|
||||||
|
import modules.globals
|
||||||
|
|
||||||
|
IS_APPLE_SILICON = platform.system() == "Darwin" and platform.machine() == "arm64"
|
||||||
|
|
||||||
|
# Limit concurrent ONNX calls to avoid VRAM exhaustion on multi-face frames
|
||||||
|
THREAD_SEMAPHORE = threading.Semaphore(min(max(1, (os.cpu_count() or 1)), 8))
|
||||||
|
|
||||||
|
|
||||||
|
def create_onnx_session(model_path: str) -> onnxruntime.InferenceSession:
|
||||||
|
"""Create an ONNX Runtime session using the configured execution providers."""
|
||||||
|
providers = modules.globals.execution_providers
|
||||||
|
session = onnxruntime.InferenceSession(model_path, providers=providers)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def warmup_session(session: onnxruntime.InferenceSession) -> None:
|
||||||
|
"""Run a dummy inference pass to trigger JIT / compile caching."""
|
||||||
|
try:
|
||||||
|
input_feed = {
|
||||||
|
inp.name: np.zeros(
|
||||||
|
[d if isinstance(d, int) and d > 0 else 1 for d in inp.shape],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
for inp in session.get_inputs()
|
||||||
|
}
|
||||||
|
session.run(None, input_feed)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ONNX enhancer warmup skipped (non-fatal): {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_face(face_img: np.ndarray, input_size: int) -> np.ndarray:
|
||||||
|
"""Resize, normalize, and convert a BGR face crop to ONNX input blob.
|
||||||
|
|
||||||
|
GPEN-BFR expects [1, 3, H, W] float32 in RGB, normalized to [-1, 1].
|
||||||
|
"""
|
||||||
|
resized = cv2.resize(face_img, (input_size, input_size), interpolation=cv2.INTER_LINEAR)
|
||||||
|
rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
|
||||||
|
blob = rgb.astype(np.float32) / 255.0 * 2.0 - 1.0
|
||||||
|
blob = np.transpose(blob, (2, 0, 1))[np.newaxis, ...]
|
||||||
|
return blob
|
||||||
|
|
||||||
|
|
||||||
|
def postprocess_face(output: np.ndarray) -> np.ndarray:
|
||||||
|
"""Convert ONNX output [1, 3, H, W] float32 back to BGR uint8 image."""
|
||||||
|
img = output[0].transpose(1, 2, 0)
|
||||||
|
img = ((img + 1.0) / 2.0 * 255.0)
|
||||||
|
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _get_face_affine(face: Any, input_size: int):
|
||||||
|
"""Compute affine transform to align a face to GPEN input space.
|
||||||
|
|
||||||
|
Returns (M, inv_M) — forward and inverse affine matrices.
|
||||||
|
"""
|
||||||
|
template = np.array([
|
||||||
|
[0.31556875, 0.4615741],
|
||||||
|
[0.68262291, 0.4615741],
|
||||||
|
[0.50009375, 0.6405054],
|
||||||
|
[0.34947187, 0.8246919],
|
||||||
|
[0.65343645, 0.8246919],
|
||||||
|
], dtype=np.float32) * input_size
|
||||||
|
|
||||||
|
landmarks = None
|
||||||
|
if hasattr(face, "kps") and face.kps is not None:
|
||||||
|
landmarks = face.kps.astype(np.float32)
|
||||||
|
elif hasattr(face, "landmark_2d_106") and face.landmark_2d_106 is not None:
|
||||||
|
lm106 = face.landmark_2d_106
|
||||||
|
landmarks = np.array([
|
||||||
|
lm106[38], # left eye
|
||||||
|
lm106[88], # right eye
|
||||||
|
lm106[86], # nose tip
|
||||||
|
lm106[52], # left mouth
|
||||||
|
lm106[61], # right mouth
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
if landmarks is None or len(landmarks) < 5:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
M = cv2.estimateAffinePartial2D(landmarks, template, method=cv2.LMEDS)[0]
|
||||||
|
if M is None:
|
||||||
|
return None, None
|
||||||
|
inv_M = cv2.invertAffineTransform(M)
|
||||||
|
return M, inv_M
|
||||||
|
|
||||||
|
|
||||||
|
def enhance_face_onnx(
|
||||||
|
frame: np.ndarray,
|
||||||
|
face: Any,
|
||||||
|
session: onnxruntime.InferenceSession,
|
||||||
|
input_size: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Enhance a single face in the frame using an ONNX face restoration model."""
|
||||||
|
M, inv_M = _get_face_affine(face, input_size)
|
||||||
|
if M is None:
|
||||||
|
return frame
|
||||||
|
|
||||||
|
face_crop = cv2.warpAffine(
|
||||||
|
frame, M, (input_size, input_size),
|
||||||
|
flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
blob = preprocess_face(face_crop, input_size)
|
||||||
|
with THREAD_SEMAPHORE:
|
||||||
|
output = session.run(None, {session.get_inputs()[0].name: blob})[0]
|
||||||
|
enhanced = postprocess_face(output)
|
||||||
|
|
||||||
|
# Create mask for blending (feathered edges)
|
||||||
|
mask = np.ones((input_size, input_size), dtype=np.float32)
|
||||||
|
border = max(1, input_size // 16)
|
||||||
|
mask[:border, :] = np.linspace(0, 1, border)[:, np.newaxis]
|
||||||
|
mask[-border:, :] = np.linspace(1, 0, border)[:, np.newaxis]
|
||||||
|
mask[:, :border] = np.minimum(mask[:, :border], np.linspace(0, 1, border)[np.newaxis, :])
|
||||||
|
mask[:, -border:] = np.minimum(mask[:, -border:], np.linspace(1, 0, border)[np.newaxis, :])
|
||||||
|
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
warped_enhanced = cv2.warpAffine(
|
||||||
|
enhanced, inv_M, (w, h),
|
||||||
|
flags=cv2.INTER_LINEAR, borderValue=(0, 0, 0),
|
||||||
|
)
|
||||||
|
warped_mask = cv2.warpAffine(
|
||||||
|
mask, inv_M, (w, h),
|
||||||
|
flags=cv2.INTER_LINEAR, borderValue=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
mask_3ch = warped_mask[:, :, np.newaxis]
|
||||||
|
result = (warped_enhanced.astype(np.float32) * mask_3ch +
|
||||||
|
frame.astype(np.float32) * (1.0 - mask_3ch))
|
||||||
|
return np.clip(result, 0, 255).astype(np.uint8)
|
||||||
@@ -17,8 +17,17 @@ FRAME_PROCESSORS_INTERFACE = [
|
|||||||
'process_video'
|
'process_video'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ALLOWED_PROCESSORS = {
|
||||||
|
'face_swapper',
|
||||||
|
'face_enhancer',
|
||||||
|
'face_enhancer_gpen256',
|
||||||
|
'face_enhancer_gpen512'
|
||||||
|
}
|
||||||
|
|
||||||
def load_frame_processor_module(frame_processor: str) -> Any:
|
def load_frame_processor_module(frame_processor: str) -> Any:
|
||||||
|
if frame_processor not in ALLOWED_PROCESSORS:
|
||||||
|
print(f"Frame processor {frame_processor} is not allowed")
|
||||||
|
sys.exit()
|
||||||
try:
|
try:
|
||||||
frame_processor_module = importlib.import_module(f'modules.processors.frame.{frame_processor}')
|
frame_processor_module = importlib.import_module(f'modules.processors.frame.{frame_processor}')
|
||||||
for method_name in FRAME_PROCESSORS_INTERFACE:
|
for method_name in FRAME_PROCESSORS_INTERFACE:
|
||||||
@@ -42,27 +51,54 @@ def get_frame_processors_modules(frame_processors: List[str]) -> List[ModuleType
|
|||||||
|
|
||||||
def set_frame_processors_modules_from_ui(frame_processors: List[str]) -> None:
|
def set_frame_processors_modules_from_ui(frame_processors: List[str]) -> None:
|
||||||
global FRAME_PROCESSORS_MODULES
|
global FRAME_PROCESSORS_MODULES
|
||||||
|
current_processor_names = [proc.__name__.split('.')[-1] for proc in FRAME_PROCESSORS_MODULES]
|
||||||
|
|
||||||
for frame_processor, state in modules.globals.fp_ui.items():
|
for frame_processor, state in modules.globals.fp_ui.items():
|
||||||
if state == True and frame_processor not in frame_processors:
|
if state == True and frame_processor not in current_processor_names:
|
||||||
frame_processor_module = load_frame_processor_module(frame_processor)
|
|
||||||
FRAME_PROCESSORS_MODULES.append(frame_processor_module)
|
|
||||||
modules.globals.frame_processors.append(frame_processor)
|
|
||||||
if state == False:
|
|
||||||
try:
|
try:
|
||||||
frame_processor_module = load_frame_processor_module(frame_processor)
|
frame_processor_module = load_frame_processor_module(frame_processor)
|
||||||
FRAME_PROCESSORS_MODULES.remove(frame_processor_module)
|
FRAME_PROCESSORS_MODULES.append(frame_processor_module)
|
||||||
modules.globals.frame_processors.remove(frame_processor)
|
if frame_processor not in modules.globals.frame_processors:
|
||||||
except:
|
modules.globals.frame_processors.append(frame_processor)
|
||||||
pass
|
except SystemExit:
|
||||||
|
print(f"Warning: Failed to load frame processor {frame_processor} requested by UI state.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Error loading frame processor {frame_processor} requested by UI state: {e}")
|
||||||
|
|
||||||
|
elif state == False and frame_processor in current_processor_names:
|
||||||
|
try:
|
||||||
|
module_to_remove = next((mod for mod in FRAME_PROCESSORS_MODULES if mod.__name__.endswith(f'.{frame_processor}')), None)
|
||||||
|
if module_to_remove:
|
||||||
|
FRAME_PROCESSORS_MODULES.remove(module_to_remove)
|
||||||
|
if frame_processor in modules.globals.frame_processors:
|
||||||
|
modules.globals.frame_processors.remove(frame_processor)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Error removing frame processor {frame_processor}: {e}")
|
||||||
|
|
||||||
def multi_process_frame(source_path: str, temp_frame_paths: List[str], process_frames: Callable[[str, List[str], Any], None], progress: Any = None) -> None:
|
def multi_process_frame(source_path: str, temp_frame_paths: List[str], process_frames: Callable[[str, List[str], Any], None], progress: Any = None) -> None:
|
||||||
with ThreadPoolExecutor(max_workers=modules.globals.execution_threads) as executor:
|
"""Process frames in parallel with optimized batching and memory management."""
|
||||||
futures = []
|
max_workers = modules.globals.execution_threads
|
||||||
for path in temp_frame_paths:
|
|
||||||
future = executor.submit(process_frames, source_path, [path], progress)
|
# Determine optimal batch size based on available memory and thread count
|
||||||
futures.append(future)
|
# Process frames in batches to avoid memory overflow
|
||||||
for future in futures:
|
batch_size = max(1, min(32, len(temp_frame_paths) // max(1, max_workers)))
|
||||||
future.result()
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
# Process in batches to manage memory better
|
||||||
|
for i in range(0, len(temp_frame_paths), batch_size):
|
||||||
|
batch = temp_frame_paths[i:i + batch_size]
|
||||||
|
futures = []
|
||||||
|
|
||||||
|
for path in batch:
|
||||||
|
future = executor.submit(process_frames, source_path, [path], progress)
|
||||||
|
futures.append(future)
|
||||||
|
|
||||||
|
# Wait for batch to complete before starting next batch
|
||||||
|
for future in futures:
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing frame: {e}")
|
||||||
|
|
||||||
|
|
||||||
def process_video(source_path: str, frame_paths: list[str], process_frames: Callable[[str, List[str], Any], None]) -> None:
|
def process_video(source_path: str, frame_paths: list[str], process_frames: Callable[[str, List[str], Any], None]) -> None:
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
|
# --- START OF FILE face_enhancer.py ---
|
||||||
|
# Uses ONNX Runtime for GFPGAN face enhancement (no torch/gfpgan dependency)
|
||||||
|
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
import cv2
|
import cv2
|
||||||
import threading
|
import threading
|
||||||
import gfpgan
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import onnxruntime
|
||||||
|
|
||||||
import modules.globals
|
import modules.globals
|
||||||
import modules.processors.frame.core
|
import modules.processors.frame.core
|
||||||
from modules.core import update_status
|
from modules.core import update_status
|
||||||
from modules.face_analyser import get_one_face
|
from modules.face_analyser import get_one_face, get_many_faces
|
||||||
from modules.typing import Frame, Face
|
from modules.typing import Frame, Face
|
||||||
import platform
|
|
||||||
import torch
|
|
||||||
from modules.utilities import (
|
from modules.utilities import (
|
||||||
conditional_download,
|
|
||||||
is_image,
|
is_image,
|
||||||
is_video,
|
is_video,
|
||||||
)
|
)
|
||||||
@@ -27,15 +29,29 @@ models_dir = os.path.join(
|
|||||||
os.path.dirname(os.path.dirname(os.path.dirname(abs_dir))), "models"
|
os.path.dirname(os.path.dirname(os.path.dirname(abs_dir))), "models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Standard FFHQ 5-point face template for 512x512 resolution
|
||||||
|
# Points: left_eye, right_eye, nose, left_mouth, right_mouth
|
||||||
|
FFHQ_TEMPLATE_512 = np.array(
|
||||||
|
[
|
||||||
|
[192.98138, 239.94708],
|
||||||
|
[318.90277, 240.19366],
|
||||||
|
[256.63416, 314.01935],
|
||||||
|
[201.26117, 371.41043],
|
||||||
|
[313.08905, 371.15118],
|
||||||
|
],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pre_check() -> bool:
|
def pre_check() -> bool:
|
||||||
download_directory_path = models_dir
|
model_path = os.path.join(models_dir, "gfpgan-1024.onnx")
|
||||||
conditional_download(
|
if not os.path.exists(model_path):
|
||||||
download_directory_path,
|
update_status(
|
||||||
[
|
f"GFPGAN ONNX model not found at {model_path}. "
|
||||||
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth"
|
"Please place gfpgan-1024.onnx in the models folder.",
|
||||||
],
|
NAME,
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -48,62 +64,309 @@ def pre_start() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_face_enhancer() -> Any:
|
def get_face_enhancer() -> onnxruntime.InferenceSession:
|
||||||
|
"""
|
||||||
|
Initializes and returns the GFPGAN ONNX Runtime inference session,
|
||||||
|
using the execution providers configured in modules.globals.
|
||||||
|
"""
|
||||||
global FACE_ENHANCER
|
global FACE_ENHANCER
|
||||||
|
|
||||||
with THREAD_LOCK:
|
with THREAD_LOCK:
|
||||||
if FACE_ENHANCER is None:
|
if FACE_ENHANCER is None:
|
||||||
model_path = os.path.join(models_dir, "GFPGANv1.4.pth")
|
model_path = os.path.join(models_dir, "gfpgan-1024.onnx")
|
||||||
|
|
||||||
match platform.system():
|
if not os.path.exists(model_path):
|
||||||
case "Darwin": # Mac OS
|
raise FileNotFoundError(
|
||||||
if torch.backends.mps.is_available():
|
f"{NAME}: Model not found at {model_path}"
|
||||||
mps_device = torch.device("mps")
|
)
|
||||||
FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1, device=mps_device) # type: ignore[attr-defined]
|
|
||||||
else:
|
try:
|
||||||
FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1) # type: ignore[attr-defined]
|
providers = modules.globals.execution_providers
|
||||||
case _: # Other OS
|
|
||||||
FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1) # type: ignore[attr-defined]
|
session_options = onnxruntime.SessionOptions()
|
||||||
|
session_options.graph_optimization_level = (
|
||||||
|
onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
FACE_ENHANCER = onnxruntime.InferenceSession(
|
||||||
|
model_path,
|
||||||
|
sess_options=session_options,
|
||||||
|
providers=providers,
|
||||||
|
)
|
||||||
|
|
||||||
|
input_info = FACE_ENHANCER.get_inputs()[0]
|
||||||
|
output_info = FACE_ENHANCER.get_outputs()[0]
|
||||||
|
active_providers = FACE_ENHANCER.get_providers()
|
||||||
|
print(
|
||||||
|
f"{NAME}: GFPGAN ONNX model loaded successfully."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{NAME}: Input: {input_info.name}, "
|
||||||
|
f"shape: {input_info.shape}, type: {input_info.type}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{NAME}: Output: {output_info.name}, "
|
||||||
|
f"shape: {output_info.shape}, type: {output_info.type}"
|
||||||
|
)
|
||||||
|
print(f"{NAME}: Active providers: {active_providers}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: Error loading GFPGAN ONNX model: {e}")
|
||||||
|
FACE_ENHANCER = None
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{NAME}: Failed to load GFPGAN ONNX model: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if FACE_ENHANCER is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{NAME}: Failed to initialize GFPGAN ONNX session. Check logs."
|
||||||
|
)
|
||||||
|
|
||||||
return FACE_ENHANCER
|
return FACE_ENHANCER
|
||||||
|
|
||||||
|
|
||||||
|
def _align_face(
|
||||||
|
frame: Frame, landmarks_5: np.ndarray, output_size: int
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Align and crop a face from the frame using 5-point landmarks and the
|
||||||
|
standard FFHQ template.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(aligned_face, affine_matrix) or (None, None) on failure.
|
||||||
|
"""
|
||||||
|
# Scale the 512-base template to the desired output size
|
||||||
|
scale = output_size / 512.0
|
||||||
|
template = FFHQ_TEMPLATE_512 * scale
|
||||||
|
|
||||||
|
# Estimate a similarity transform (4 DOF: rotation, scale, tx, ty)
|
||||||
|
affine_matrix, _ = cv2.estimateAffinePartial2D(
|
||||||
|
landmarks_5, template, method=cv2.LMEDS
|
||||||
|
)
|
||||||
|
if affine_matrix is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Warp the face to the aligned position
|
||||||
|
aligned_face = cv2.warpAffine(
|
||||||
|
frame,
|
||||||
|
affine_matrix,
|
||||||
|
(output_size, output_size),
|
||||||
|
borderMode=cv2.BORDER_CONSTANT,
|
||||||
|
borderValue=(135, 133, 132),
|
||||||
|
)
|
||||||
|
|
||||||
|
return aligned_face, affine_matrix
|
||||||
|
|
||||||
|
|
||||||
|
def _paste_back(
|
||||||
|
frame: Frame,
|
||||||
|
enhanced_face: np.ndarray,
|
||||||
|
affine_matrix: np.ndarray,
|
||||||
|
output_size: int,
|
||||||
|
) -> Frame:
|
||||||
|
"""
|
||||||
|
Paste an enhanced (aligned) face back onto the original frame using the
|
||||||
|
inverse affine transform with feathered-edge blending.
|
||||||
|
"""
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
|
||||||
|
# Inverse the affine warp
|
||||||
|
inv_matrix = cv2.invertAffineTransform(affine_matrix)
|
||||||
|
inv_restored = cv2.warpAffine(
|
||||||
|
enhanced_face,
|
||||||
|
inv_matrix,
|
||||||
|
(w, h),
|
||||||
|
borderMode=cv2.BORDER_CONSTANT,
|
||||||
|
borderValue=(0, 0, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build a soft feathered mask in aligned space for edge blending
|
||||||
|
face_mask = np.ones((output_size, output_size), dtype=np.float32)
|
||||||
|
|
||||||
|
# Feather the border (5 % of the size on each edge)
|
||||||
|
border = max(1, int(output_size * 0.05))
|
||||||
|
ramp_up = np.linspace(0.0, 1.0, border, dtype=np.float32)
|
||||||
|
ramp_down = np.linspace(1.0, 0.0, border, dtype=np.float32)
|
||||||
|
|
||||||
|
# Top / bottom rows
|
||||||
|
face_mask[:border, :] *= ramp_up[:, None]
|
||||||
|
face_mask[-border:, :] *= ramp_down[:, None]
|
||||||
|
# Left / right columns
|
||||||
|
face_mask[:, :border] *= ramp_up[None, :]
|
||||||
|
face_mask[:, -border:] *= ramp_down[None, :]
|
||||||
|
|
||||||
|
# Expand to 3-channel
|
||||||
|
face_mask_3c = np.stack([face_mask] * 3, axis=-1)
|
||||||
|
|
||||||
|
# Warp mask back to original frame space
|
||||||
|
inv_mask = cv2.warpAffine(
|
||||||
|
face_mask_3c,
|
||||||
|
inv_matrix,
|
||||||
|
(w, h),
|
||||||
|
borderMode=cv2.BORDER_CONSTANT,
|
||||||
|
borderValue=(0, 0, 0),
|
||||||
|
)
|
||||||
|
inv_mask = np.clip(inv_mask, 0.0, 1.0)
|
||||||
|
|
||||||
|
# Alpha-blend
|
||||||
|
result = (
|
||||||
|
frame.astype(np.float32) * (1.0 - inv_mask)
|
||||||
|
+ inv_restored.astype(np.float32) * inv_mask
|
||||||
|
)
|
||||||
|
return np.clip(result, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
def _preprocess_face(aligned_face: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Convert an aligned BGR uint8 face image to the ONNX model input tensor.
|
||||||
|
Format: NCHW float32, normalised to [-1, 1].
|
||||||
|
"""
|
||||||
|
# BGR -> RGB
|
||||||
|
rgb = cv2.cvtColor(aligned_face, cv2.COLOR_BGR2RGB).astype(np.float32)
|
||||||
|
# [0, 255] -> [0, 1] -> [-1, 1]
|
||||||
|
rgb = rgb / 255.0
|
||||||
|
rgb = (rgb - 0.5) / 0.5
|
||||||
|
# HWC -> CHW, add batch dim
|
||||||
|
chw = np.transpose(rgb, (2, 0, 1))
|
||||||
|
return np.expand_dims(chw, axis=0) # shape: (1, 3, H, W)
|
||||||
|
|
||||||
|
|
||||||
|
def _postprocess_face(output: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Convert the ONNX model output tensor back to a BGR uint8 image.
|
||||||
|
Expects input in NCHW format with values in [-1, 1].
|
||||||
|
"""
|
||||||
|
face = np.squeeze(output) # remove batch dim -> (3, H, W)
|
||||||
|
face = np.transpose(face, (1, 2, 0)) # CHW -> HWC
|
||||||
|
# [-1, 1] -> [0, 1] -> [0, 255]
|
||||||
|
face = (face + 1.0) / 2.0
|
||||||
|
face = np.clip(face * 255.0, 0, 255).astype(np.uint8)
|
||||||
|
# RGB -> BGR
|
||||||
|
return cv2.cvtColor(face, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
|
||||||
def enhance_face(temp_frame: Frame) -> Frame:
|
def enhance_face(temp_frame: Frame) -> Frame:
|
||||||
with THREAD_SEMAPHORE:
|
"""Enhances all faces in a frame using the GFPGAN ONNX model."""
|
||||||
_, _, temp_frame = get_face_enhancer().enhance(temp_frame, paste_back=True)
|
session = get_face_enhancer()
|
||||||
return temp_frame
|
|
||||||
|
# Determine model input resolution from the session metadata
|
||||||
|
input_info = session.get_inputs()[0]
|
||||||
|
input_name = input_info.name
|
||||||
|
input_shape = input_info.shape # e.g. [1, 3, 512, 512]
|
||||||
|
# Safely extract input size (handle dynamic / symbolic dimensions)
|
||||||
|
try:
|
||||||
|
align_size = int(input_shape[2])
|
||||||
|
if align_size <= 0:
|
||||||
|
align_size = 512
|
||||||
|
except (ValueError, TypeError, IndexError):
|
||||||
|
align_size = 512
|
||||||
|
|
||||||
|
# Detect faces using InsightFace (already a project dependency)
|
||||||
|
faces = get_many_faces(temp_frame)
|
||||||
|
if not faces:
|
||||||
|
return temp_frame
|
||||||
|
|
||||||
|
result_frame = temp_frame.copy()
|
||||||
|
|
||||||
|
for face in faces:
|
||||||
|
# Need the 5-point key-points for alignment
|
||||||
|
if not hasattr(face, "kps") or face.kps is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
landmarks_5 = face.kps.astype(np.float32)
|
||||||
|
if landmarks_5.shape[0] < 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Align / crop the face at the model's INPUT resolution
|
||||||
|
aligned_face, affine_matrix = _align_face(
|
||||||
|
temp_frame, landmarks_5, output_size=align_size
|
||||||
|
)
|
||||||
|
if aligned_face is None or affine_matrix is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with THREAD_SEMAPHORE:
|
||||||
|
input_tensor = _preprocess_face(aligned_face)
|
||||||
|
output_tensor = session.run(None, {input_name: input_tensor})[0]
|
||||||
|
enhanced_bgr = _postprocess_face(output_tensor)
|
||||||
|
|
||||||
|
# The model may output at a different resolution than its input
|
||||||
|
# (e.g. input 512x512 → output 1024x1024). Resize the enhanced
|
||||||
|
# face back to the alignment size so the inverse affine maps
|
||||||
|
# correctly.
|
||||||
|
eh, ew = enhanced_bgr.shape[:2]
|
||||||
|
if eh != align_size or ew != align_size:
|
||||||
|
enhanced_bgr = cv2.resize(
|
||||||
|
enhanced_bgr,
|
||||||
|
(align_size, align_size),
|
||||||
|
interpolation=cv2.INTER_LANCZOS4,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Paste enhanced face back onto the frame
|
||||||
|
result_frame = _paste_back(
|
||||||
|
result_frame, enhanced_bgr, affine_matrix, output_size=align_size
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: Error enhancing a face: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return result_frame
|
||||||
|
|
||||||
|
|
||||||
def process_frame(source_face: Face, temp_frame: Frame) -> Frame:
|
def process_frame(source_face: Face | None, temp_frame: Frame) -> Frame:
|
||||||
target_face = get_one_face(temp_frame)
|
"""Processes a frame: enhances face if detected."""
|
||||||
if target_face:
|
temp_frame = enhance_face(temp_frame)
|
||||||
temp_frame = enhance_face(temp_frame)
|
|
||||||
return temp_frame
|
return temp_frame
|
||||||
|
|
||||||
|
|
||||||
def process_frames(
|
def process_frames(
|
||||||
source_path: str, temp_frame_paths: List[str], progress: Any = None
|
source_path: str | None, temp_frame_paths: List[str], progress: Any = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Processes multiple frames from file paths."""
|
||||||
for temp_frame_path in temp_frame_paths:
|
for temp_frame_path in temp_frame_paths:
|
||||||
|
if not os.path.exists(temp_frame_path):
|
||||||
|
print(
|
||||||
|
f"{NAME}: Warning: Frame path not found {temp_frame_path}, skipping."
|
||||||
|
)
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
continue
|
||||||
|
|
||||||
temp_frame = cv2.imread(temp_frame_path)
|
temp_frame = cv2.imread(temp_frame_path)
|
||||||
result = process_frame(None, temp_frame)
|
if temp_frame is None:
|
||||||
cv2.imwrite(temp_frame_path, result)
|
print(
|
||||||
|
f"{NAME}: Warning: Failed to read frame {temp_frame_path}, skipping."
|
||||||
|
)
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_frame = process_frame(None, temp_frame)
|
||||||
|
cv2.imwrite(temp_frame_path, result_frame)
|
||||||
if progress:
|
if progress:
|
||||||
progress.update(1)
|
progress.update(1)
|
||||||
|
|
||||||
|
|
||||||
def process_image(source_path: str, target_path: str, output_path: str) -> None:
|
def process_image(
|
||||||
|
source_path: str | None, target_path: str, output_path: str
|
||||||
|
) -> None:
|
||||||
|
"""Processes a single image file."""
|
||||||
target_frame = cv2.imread(target_path)
|
target_frame = cv2.imread(target_path)
|
||||||
result = process_frame(None, target_frame)
|
if target_frame is None:
|
||||||
cv2.imwrite(output_path, result)
|
print(f"{NAME}: Error: Failed to read target image {target_path}")
|
||||||
|
return
|
||||||
|
result_frame = process_frame(None, target_frame)
|
||||||
|
cv2.imwrite(output_path, result_frame)
|
||||||
|
print(f"{NAME}: Enhanced image saved to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
def process_video(source_path: str, temp_frame_paths: List[str]) -> None:
|
def process_video(
|
||||||
modules.processors.frame.core.process_video(None, temp_frame_paths, process_frames)
|
source_path: str | None, temp_frame_paths: List[str]
|
||||||
|
) -> None:
|
||||||
|
"""Processes video frames using the frame processor core."""
|
||||||
|
modules.processors.frame.core.process_video(
|
||||||
|
source_path, temp_frame_paths, process_frames
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_frame_v2(temp_frame: Frame) -> Frame:
|
# --- END OF FILE face_enhancer.py ---
|
||||||
target_face = get_one_face(temp_frame)
|
|
||||||
if target_face:
|
|
||||||
temp_frame = enhance_face(temp_frame)
|
|
||||||
return temp_frame
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""GPEN-BFR-256 face enhancer — ONNX-based face restoration at 256x256."""
|
||||||
|
|
||||||
|
from typing import Any, List
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import modules.globals
|
||||||
|
import modules.processors.frame.core
|
||||||
|
from modules.core import update_status
|
||||||
|
from modules.face_analyser import get_one_face
|
||||||
|
from modules.typing import Frame, Face
|
||||||
|
from modules.utilities import (
|
||||||
|
is_image,
|
||||||
|
is_video,
|
||||||
|
)
|
||||||
|
from modules.processors.frame._onnx_enhancer import (
|
||||||
|
create_onnx_session,
|
||||||
|
warmup_session,
|
||||||
|
enhance_face_onnx,
|
||||||
|
)
|
||||||
|
|
||||||
|
NAME = "DLC.FACE-ENHANCER-GPEN256"
|
||||||
|
INPUT_SIZE = 256
|
||||||
|
MODEL_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-256.onnx"
|
||||||
|
MODEL_FILE = "GPEN-BFR-256.onnx"
|
||||||
|
|
||||||
|
ENHANCER = None
|
||||||
|
THREAD_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
abs_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
models_dir = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(abs_dir))), "models"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_check() -> bool:
|
||||||
|
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
update_status(f"Downloading {MODEL_FILE}...", NAME)
|
||||||
|
from modules.utilities import conditional_download
|
||||||
|
conditional_download(models_dir, [MODEL_URL])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def pre_start() -> bool:
|
||||||
|
if not is_image(modules.globals.target_path) and not is_video(modules.globals.target_path):
|
||||||
|
update_status("Select an image or video for target path.", NAME)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_enhancer() -> Any:
|
||||||
|
global ENHANCER
|
||||||
|
with THREAD_LOCK:
|
||||||
|
if ENHANCER is None:
|
||||||
|
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
from modules.utilities import conditional_download
|
||||||
|
conditional_download(models_dir, [MODEL_URL])
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
raise FileNotFoundError(f"Model file not found: {model_path}")
|
||||||
|
print(f"{NAME}: Loading ONNX model from {model_path}")
|
||||||
|
ENHANCER = create_onnx_session(model_path)
|
||||||
|
warmup_session(ENHANCER)
|
||||||
|
print(f"{NAME}: Model loaded successfully.")
|
||||||
|
return ENHANCER
|
||||||
|
|
||||||
|
|
||||||
|
def enhance_face(temp_frame: Frame, face: Face) -> Frame:
|
||||||
|
try:
|
||||||
|
session = get_enhancer()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: {e}")
|
||||||
|
return temp_frame
|
||||||
|
try:
|
||||||
|
return enhance_face_onnx(temp_frame, face, session, INPUT_SIZE)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: Error during face enhancement: {e}")
|
||||||
|
return temp_frame
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame(source_face: Face | None, temp_frame: Frame) -> Frame:
|
||||||
|
target_face = get_one_face(temp_frame)
|
||||||
|
if target_face is None:
|
||||||
|
return temp_frame
|
||||||
|
return enhance_face(temp_frame, target_face)
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame_v2(temp_frame: Frame) -> Frame:
|
||||||
|
target_face = get_one_face(temp_frame)
|
||||||
|
if target_face:
|
||||||
|
temp_frame = enhance_face(temp_frame, target_face)
|
||||||
|
return temp_frame
|
||||||
|
|
||||||
|
|
||||||
|
def process_frames(
|
||||||
|
source_path: str | None, temp_frame_paths: List[str], progress: Any = None
|
||||||
|
) -> None:
|
||||||
|
for temp_frame_path in temp_frame_paths:
|
||||||
|
temp_frame = cv2.imread(temp_frame_path)
|
||||||
|
if temp_frame is None:
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
continue
|
||||||
|
result = process_frame(None, temp_frame)
|
||||||
|
cv2.imwrite(temp_frame_path, result)
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
|
||||||
|
|
||||||
|
def process_image(source_path: str | None, target_path: str, output_path: str) -> None:
|
||||||
|
target_frame = cv2.imread(target_path)
|
||||||
|
if target_frame is None:
|
||||||
|
print(f"{NAME}: Error: Failed to read target image {target_path}")
|
||||||
|
return
|
||||||
|
result_frame = process_frame(None, target_frame)
|
||||||
|
cv2.imwrite(output_path, result_frame)
|
||||||
|
print(f"{NAME}: Enhanced image saved to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def process_video(source_path: str | None, temp_frame_paths: List[str]) -> None:
|
||||||
|
modules.processors.frame.core.process_video(source_path, temp_frame_paths, process_frames)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""GPEN-BFR-512 face enhancer — ONNX-based face restoration at 512x512."""
|
||||||
|
|
||||||
|
from typing import Any, List
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import modules.globals
|
||||||
|
import modules.processors.frame.core
|
||||||
|
from modules.core import update_status
|
||||||
|
from modules.face_analyser import get_one_face
|
||||||
|
from modules.typing import Frame, Face
|
||||||
|
from modules.utilities import (
|
||||||
|
is_image,
|
||||||
|
is_video,
|
||||||
|
)
|
||||||
|
from modules.processors.frame._onnx_enhancer import (
|
||||||
|
create_onnx_session,
|
||||||
|
warmup_session,
|
||||||
|
enhance_face_onnx,
|
||||||
|
)
|
||||||
|
|
||||||
|
NAME = "DLC.FACE-ENHANCER-GPEN512"
|
||||||
|
INPUT_SIZE = 512
|
||||||
|
MODEL_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-512.onnx"
|
||||||
|
MODEL_FILE = "GPEN-BFR-512.onnx"
|
||||||
|
|
||||||
|
ENHANCER = None
|
||||||
|
THREAD_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
abs_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
models_dir = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(abs_dir))), "models"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_check() -> bool:
|
||||||
|
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
update_status(f"Downloading {MODEL_FILE}...", NAME)
|
||||||
|
from modules.utilities import conditional_download
|
||||||
|
conditional_download(models_dir, [MODEL_URL])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def pre_start() -> bool:
|
||||||
|
if not is_image(modules.globals.target_path) and not is_video(modules.globals.target_path):
|
||||||
|
update_status("Select an image or video for target path.", NAME)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_enhancer() -> Any:
|
||||||
|
global ENHANCER
|
||||||
|
with THREAD_LOCK:
|
||||||
|
if ENHANCER is None:
|
||||||
|
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
from modules.utilities import conditional_download
|
||||||
|
conditional_download(models_dir, [MODEL_URL])
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
raise FileNotFoundError(f"Model file not found: {model_path}")
|
||||||
|
print(f"{NAME}: Loading ONNX model from {model_path}")
|
||||||
|
ENHANCER = create_onnx_session(model_path)
|
||||||
|
warmup_session(ENHANCER)
|
||||||
|
print(f"{NAME}: Model loaded successfully.")
|
||||||
|
return ENHANCER
|
||||||
|
|
||||||
|
|
||||||
|
def enhance_face(temp_frame: Frame, face: Face) -> Frame:
|
||||||
|
try:
|
||||||
|
session = get_enhancer()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: {e}")
|
||||||
|
return temp_frame
|
||||||
|
try:
|
||||||
|
return enhance_face_onnx(temp_frame, face, session, INPUT_SIZE)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{NAME}: Error during face enhancement: {e}")
|
||||||
|
return temp_frame
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame(source_face: Face | None, temp_frame: Frame) -> Frame:
|
||||||
|
target_face = get_one_face(temp_frame)
|
||||||
|
if target_face is None:
|
||||||
|
return temp_frame
|
||||||
|
return enhance_face(temp_frame, target_face)
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame_v2(temp_frame: Frame) -> Frame:
|
||||||
|
target_face = get_one_face(temp_frame)
|
||||||
|
if target_face:
|
||||||
|
temp_frame = enhance_face(temp_frame, target_face)
|
||||||
|
return temp_frame
|
||||||
|
|
||||||
|
|
||||||
|
def process_frames(
|
||||||
|
source_path: str | None, temp_frame_paths: List[str], progress: Any = None
|
||||||
|
) -> None:
|
||||||
|
for temp_frame_path in temp_frame_paths:
|
||||||
|
temp_frame = cv2.imread(temp_frame_path)
|
||||||
|
if temp_frame is None:
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
continue
|
||||||
|
result = process_frame(None, temp_frame)
|
||||||
|
cv2.imwrite(temp_frame_path, result)
|
||||||
|
if progress:
|
||||||
|
progress.update(1)
|
||||||
|
|
||||||
|
|
||||||
|
def process_image(source_path: str | None, target_path: str, output_path: str) -> None:
|
||||||
|
target_frame = cv2.imread(target_path)
|
||||||
|
if target_frame is None:
|
||||||
|
print(f"{NAME}: Error: Failed to read target image {target_path}")
|
||||||
|
return
|
||||||
|
result_frame = process_frame(None, target_frame)
|
||||||
|
cv2.imwrite(output_path, result_frame)
|
||||||
|
print(f"{NAME}: Enhanced image saved to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def process_video(source_path: str | None, temp_frame_paths: List[str]) -> None:
|
||||||
|
modules.processors.frame.core.process_video(source_path, temp_frame_paths, process_frames)
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from modules.typing import Face, Frame
|
||||||
|
import modules.globals
|
||||||
|
from modules.gpu_processing import gpu_gaussian_blur, gpu_resize, gpu_cvt_color
|
||||||
|
|
||||||
|
def apply_color_transfer(source, target):
|
||||||
|
"""
|
||||||
|
Apply color transfer from target to source image using LAB color space.
|
||||||
|
Uses float32 throughout for performance (sufficient precision for 8-bit images).
|
||||||
|
"""
|
||||||
|
# Convert to float32 [0,1] range for proper LAB conversion
|
||||||
|
source_f32 = source.astype(np.float32) / 255.0
|
||||||
|
target_f32 = target.astype(np.float32) / 255.0
|
||||||
|
|
||||||
|
source_lab = cv2.cvtColor(source_f32, cv2.COLOR_BGR2LAB)
|
||||||
|
target_lab = cv2.cvtColor(target_f32, cv2.COLOR_BGR2LAB)
|
||||||
|
|
||||||
|
source_mean, source_std = cv2.meanStdDev(source_lab)
|
||||||
|
target_mean, target_std = cv2.meanStdDev(target_lab)
|
||||||
|
|
||||||
|
# Reshape mean and std to be broadcastable (already float64 from meanStdDev, cast to f32)
|
||||||
|
source_mean = source_mean.reshape(1, 1, 3).astype(np.float32)
|
||||||
|
source_std = np.maximum(source_std.reshape(1, 1, 3), 1e-6).astype(np.float32)
|
||||||
|
target_mean = target_mean.reshape(1, 1, 3).astype(np.float32)
|
||||||
|
target_std = target_std.reshape(1, 1, 3).astype(np.float32)
|
||||||
|
|
||||||
|
# Perform the color transfer in LAB space
|
||||||
|
result_lab = (source_lab - source_mean) * (target_std / source_std) + target_mean
|
||||||
|
|
||||||
|
# Convert back to BGR and uint8
|
||||||
|
result_bgr = cv2.cvtColor(result_lab, cv2.COLOR_LAB2BGR)
|
||||||
|
return np.clip(result_bgr * 255.0, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
def create_face_mask(face: Face, frame: Frame) -> np.ndarray:
|
||||||
|
mask = np.zeros(frame.shape[:2], dtype=np.uint8)
|
||||||
|
landmarks = face.landmark_2d_106
|
||||||
|
if landmarks is not None:
|
||||||
|
# Convert landmarks to int32
|
||||||
|
landmarks = landmarks.astype(np.int32)
|
||||||
|
|
||||||
|
# Extract facial features
|
||||||
|
right_side_face = landmarks[0:16]
|
||||||
|
left_side_face = landmarks[17:32]
|
||||||
|
right_eye = landmarks[33:42]
|
||||||
|
right_eye_brow = landmarks[43:51]
|
||||||
|
left_eye = landmarks[87:96]
|
||||||
|
left_eye_brow = landmarks[97:105]
|
||||||
|
|
||||||
|
# Calculate padding
|
||||||
|
padding = int(
|
||||||
|
np.linalg.norm(right_side_face[0] - left_side_face[-1]) * 0.05
|
||||||
|
) # 5% of face width
|
||||||
|
|
||||||
|
# Create a slightly larger convex hull for padding
|
||||||
|
face_outline = landmarks[0:33]
|
||||||
|
hull = cv2.convexHull(face_outline)
|
||||||
|
# Vectorized hull padding — expand each point outward from center
|
||||||
|
center = np.mean(face_outline, axis=0, dtype=np.float32)
|
||||||
|
hull_pts = hull.reshape(-1, 2).astype(np.float32)
|
||||||
|
directions = hull_pts - center
|
||||||
|
norms = np.linalg.norm(directions, axis=1, keepdims=True)
|
||||||
|
norms = np.maximum(norms, 1e-6) # avoid division by zero
|
||||||
|
directions /= norms
|
||||||
|
hull_padded = (hull_pts + directions * padding).astype(np.int32)
|
||||||
|
|
||||||
|
# Fill the padded convex hull
|
||||||
|
cv2.fillConvexPoly(mask, hull_padded, 255)
|
||||||
|
|
||||||
|
# Smooth the mask edges (GPU-accelerated when available)
|
||||||
|
mask = gpu_gaussian_blur(mask, (5, 5), 3)
|
||||||
|
|
||||||
|
return mask
|
||||||
|
|
||||||
|
def create_lower_mouth_mask(
|
||||||
|
face: Face, frame: Frame
|
||||||
|
) -> (np.ndarray, np.ndarray, tuple, np.ndarray):
|
||||||
|
mask = np.zeros(frame.shape[:2], dtype=np.uint8)
|
||||||
|
mouth_cutout = None
|
||||||
|
lower_lip_polygon = None
|
||||||
|
mouth_box = (0,0,0,0)
|
||||||
|
|
||||||
|
landmarks = face.landmark_2d_106
|
||||||
|
if landmarks is not None:
|
||||||
|
# Use outer mouth landmarks (52-71) to capture the full mouth area
|
||||||
|
lower_lip_order = list(range(52, 72))
|
||||||
|
|
||||||
|
if max(lower_lip_order) >= landmarks.shape[0]:
|
||||||
|
return mask, mouth_cutout, mouth_box, lower_lip_polygon
|
||||||
|
|
||||||
|
lower_lip_landmarks = landmarks[lower_lip_order].astype(np.float32)
|
||||||
|
|
||||||
|
# Calculate the center of the landmarks
|
||||||
|
center = np.mean(lower_lip_landmarks, axis=0)
|
||||||
|
|
||||||
|
# Expand the landmarks outward using the mouth_mask_size
|
||||||
|
mouth_mask_size = getattr(modules.globals, "mouth_mask_size", 0.0) # 0-100 slider
|
||||||
|
expansion_factor = 1 + (mouth_mask_size / 100.0) * 2.5
|
||||||
|
|
||||||
|
# Expand with extra downward bias toward chin
|
||||||
|
offsets = lower_lip_landmarks - center
|
||||||
|
chin_bias = 1 + (mouth_mask_size / 100.0) * 1.5
|
||||||
|
scale_y = np.where(offsets[:, 1] > 0, expansion_factor * chin_bias, expansion_factor)
|
||||||
|
expanded_landmarks = lower_lip_landmarks.copy()
|
||||||
|
expanded_landmarks[:, 0] = center[0] + offsets[:, 0] * expansion_factor
|
||||||
|
expanded_landmarks[:, 1] = center[1] + offsets[:, 1] * scale_y
|
||||||
|
|
||||||
|
# Convert back to integer coordinates
|
||||||
|
expanded_landmarks = expanded_landmarks.astype(np.int32)
|
||||||
|
|
||||||
|
# Calculate bounding box for the expanded lower mouth
|
||||||
|
min_x, min_y = np.min(expanded_landmarks, axis=0)
|
||||||
|
max_x, max_y = np.max(expanded_landmarks, axis=0)
|
||||||
|
|
||||||
|
# Add some padding to the bounding box
|
||||||
|
padding = int((max_x - min_x) * 0.1) # 10% padding
|
||||||
|
min_x = max(0, min_x - padding)
|
||||||
|
min_y = max(0, min_y - padding)
|
||||||
|
max_x = min(frame.shape[1], max_x + padding)
|
||||||
|
max_y = min(frame.shape[0], max_y + padding)
|
||||||
|
|
||||||
|
# Ensure the bounding box dimensions are valid
|
||||||
|
if max_x <= min_x or max_y <= min_y:
|
||||||
|
if (max_x - min_x) <= 1:
|
||||||
|
max_x = min_x + 1
|
||||||
|
if (max_y - min_y) <= 1:
|
||||||
|
max_y = min_y + 1
|
||||||
|
|
||||||
|
# Create the mask
|
||||||
|
mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8)
|
||||||
|
# Shift polygon coordinates relative to the ROI's top-left corner
|
||||||
|
polygon_relative_to_roi = expanded_landmarks - [min_x, min_y]
|
||||||
|
cv2.fillPoly(mask_roi, [polygon_relative_to_roi], 255)
|
||||||
|
|
||||||
|
# Apply Gaussian blur to soften the mask edges (GPU-accelerated when available)
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (15, 15), 5)
|
||||||
|
|
||||||
|
# Place the mask ROI in the full-sized mask
|
||||||
|
mask[min_y:max_y, min_x:max_x] = mask_roi
|
||||||
|
|
||||||
|
# Extract the masked area from the frame
|
||||||
|
mouth_cutout = frame[min_y:max_y, min_x:max_x].copy()
|
||||||
|
|
||||||
|
# Return the expanded lower lip polygon in original frame coordinates
|
||||||
|
lower_lip_polygon = expanded_landmarks
|
||||||
|
mouth_box = (min_x, min_y, max_x, max_y)
|
||||||
|
|
||||||
|
return mask, mouth_cutout, mouth_box, lower_lip_polygon
|
||||||
|
|
||||||
|
def create_eyes_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray):
|
||||||
|
mask = np.zeros(frame.shape[:2], dtype=np.uint8)
|
||||||
|
eyes_cutout = None
|
||||||
|
landmarks = face.landmark_2d_106
|
||||||
|
if landmarks is not None:
|
||||||
|
# Left eye landmarks (87-96) and right eye landmarks (33-42)
|
||||||
|
left_eye = landmarks[87:96]
|
||||||
|
right_eye = landmarks[33:42]
|
||||||
|
|
||||||
|
# Calculate centers and dimensions for each eye
|
||||||
|
left_eye_center = np.mean(left_eye, axis=0).astype(np.int32)
|
||||||
|
right_eye_center = np.mean(right_eye, axis=0).astype(np.int32)
|
||||||
|
|
||||||
|
# Calculate eye dimensions with size adjustment
|
||||||
|
def get_eye_dimensions(eye_points):
|
||||||
|
x_coords = eye_points[:, 0]
|
||||||
|
y_coords = eye_points[:, 1]
|
||||||
|
width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size * modules.globals.eyes_mask_size))
|
||||||
|
height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size * modules.globals.eyes_mask_size))
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
left_width, left_height = get_eye_dimensions(left_eye)
|
||||||
|
right_width, right_height = get_eye_dimensions(right_eye)
|
||||||
|
|
||||||
|
# Add extra padding
|
||||||
|
padding = int(max(left_width, right_width) * 0.2)
|
||||||
|
|
||||||
|
# Calculate bounding box for both eyes
|
||||||
|
min_x = min(left_eye_center[0] - left_width//2, right_eye_center[0] - right_width//2) - padding
|
||||||
|
max_x = max(left_eye_center[0] + left_width//2, right_eye_center[0] + right_width//2) + padding
|
||||||
|
min_y = min(left_eye_center[1] - left_height//2, right_eye_center[1] - right_height//2) - padding
|
||||||
|
max_y = max(left_eye_center[1] + left_height//2, right_eye_center[1] + right_height//2) + padding
|
||||||
|
|
||||||
|
# Ensure coordinates are within frame bounds
|
||||||
|
min_x = max(0, min_x)
|
||||||
|
min_y = max(0, min_y)
|
||||||
|
max_x = min(frame.shape[1], max_x)
|
||||||
|
max_y = min(frame.shape[0], max_y)
|
||||||
|
|
||||||
|
# Create mask for the eyes region
|
||||||
|
mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8)
|
||||||
|
|
||||||
|
# Draw ellipses for both eyes
|
||||||
|
left_center = (left_eye_center[0] - min_x, left_eye_center[1] - min_y)
|
||||||
|
right_center = (right_eye_center[0] - min_x, right_eye_center[1] - min_y)
|
||||||
|
|
||||||
|
# Calculate axes lengths (half of width and height)
|
||||||
|
left_axes = (left_width//2, left_height//2)
|
||||||
|
right_axes = (right_width//2, right_height//2)
|
||||||
|
|
||||||
|
# Draw filled ellipses
|
||||||
|
cv2.ellipse(mask_roi, left_center, left_axes, 0, 0, 360, 255, -1)
|
||||||
|
cv2.ellipse(mask_roi, right_center, right_axes, 0, 0, 360, 255, -1)
|
||||||
|
|
||||||
|
# Apply Gaussian blur to soften mask edges (GPU-accelerated when available)
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (15, 15), 5)
|
||||||
|
|
||||||
|
# Place the mask ROI in the full-sized mask
|
||||||
|
mask[min_y:max_y, min_x:max_x] = mask_roi
|
||||||
|
|
||||||
|
# Extract the masked area from the frame
|
||||||
|
eyes_cutout = frame[min_y:max_y, min_x:max_x].copy()
|
||||||
|
|
||||||
|
# Create polygon points for visualization
|
||||||
|
def create_ellipse_points(center, axes):
|
||||||
|
t = np.linspace(0, 2*np.pi, 32)
|
||||||
|
x = center[0] + axes[0] * np.cos(t)
|
||||||
|
y = center[1] + axes[1] * np.sin(t)
|
||||||
|
return np.column_stack((x, y)).astype(np.int32)
|
||||||
|
|
||||||
|
# Generate points for both ellipses
|
||||||
|
left_points = create_ellipse_points((left_eye_center[0], left_eye_center[1]), (left_width//2, left_height//2))
|
||||||
|
right_points = create_ellipse_points((right_eye_center[0], right_eye_center[1]), (right_width//2, right_height//2))
|
||||||
|
|
||||||
|
# Combine points for both eyes
|
||||||
|
eyes_polygon = np.vstack([left_points, right_points])
|
||||||
|
|
||||||
|
return mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon
|
||||||
|
|
||||||
|
def create_curved_eyebrow(points):
|
||||||
|
if len(points) >= 5:
|
||||||
|
# Sort points by x-coordinate
|
||||||
|
sorted_idx = np.argsort(points[:, 0])
|
||||||
|
sorted_points = points[sorted_idx]
|
||||||
|
|
||||||
|
# Calculate dimensions
|
||||||
|
x_min, y_min = np.min(sorted_points, axis=0)
|
||||||
|
x_max, y_max = np.max(sorted_points, axis=0)
|
||||||
|
width = x_max - x_min
|
||||||
|
height = y_max - y_min
|
||||||
|
|
||||||
|
# Create more points for smoother curve
|
||||||
|
num_points = 50
|
||||||
|
x = np.linspace(x_min, x_max, num_points)
|
||||||
|
|
||||||
|
# Fit quadratic curve through points for more natural arch
|
||||||
|
coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2)
|
||||||
|
y = np.polyval(coeffs, x)
|
||||||
|
|
||||||
|
# Increased offsets to create more separation
|
||||||
|
top_offset = height * 0.5 # Increased from 0.3 to shift up more
|
||||||
|
bottom_offset = height * 0.2 # Increased from 0.1 to shift down more
|
||||||
|
|
||||||
|
# Create smooth curves
|
||||||
|
top_curve = y - top_offset
|
||||||
|
bottom_curve = y + bottom_offset
|
||||||
|
|
||||||
|
# Create curved endpoints with more pronounced taper
|
||||||
|
end_points = 5
|
||||||
|
start_x = np.linspace(x[0] - width * 0.15, x[0], end_points) # Increased taper
|
||||||
|
end_x = np.linspace(x[-1], x[-1] + width * 0.15, end_points) # Increased taper
|
||||||
|
|
||||||
|
# Create tapered ends
|
||||||
|
start_curve = np.column_stack((
|
||||||
|
start_x,
|
||||||
|
np.linspace(bottom_curve[0], top_curve[0], end_points)
|
||||||
|
))
|
||||||
|
end_curve = np.column_stack((
|
||||||
|
end_x,
|
||||||
|
np.linspace(bottom_curve[-1], top_curve[-1], end_points)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Combine all points to form a smooth contour
|
||||||
|
contour_points = np.vstack([
|
||||||
|
start_curve,
|
||||||
|
np.column_stack((x, top_curve)),
|
||||||
|
end_curve,
|
||||||
|
np.column_stack((x[::-1], bottom_curve[::-1]))
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add slight padding for better coverage
|
||||||
|
center = np.mean(contour_points, axis=0)
|
||||||
|
vectors = contour_points - center
|
||||||
|
padded_points = center + vectors * 1.2 # Increased padding slightly
|
||||||
|
|
||||||
|
return padded_points
|
||||||
|
return points
|
||||||
|
|
||||||
|
def create_eyebrows_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray):
|
||||||
|
mask = np.zeros(frame.shape[:2], dtype=np.uint8)
|
||||||
|
eyebrows_cutout = None
|
||||||
|
landmarks = face.landmark_2d_106
|
||||||
|
if landmarks is not None:
|
||||||
|
# Left eyebrow landmarks (97-105) and right eyebrow landmarks (43-51)
|
||||||
|
left_eyebrow = landmarks[97:105].astype(np.float32)
|
||||||
|
right_eyebrow = landmarks[43:51].astype(np.float32)
|
||||||
|
|
||||||
|
# Calculate centers and dimensions for each eyebrow
|
||||||
|
left_center = np.mean(left_eyebrow, axis=0)
|
||||||
|
right_center = np.mean(right_eyebrow, axis=0)
|
||||||
|
|
||||||
|
# Calculate bounding box with padding adjusted by size
|
||||||
|
all_points = np.vstack([left_eyebrow, right_eyebrow])
|
||||||
|
padding_factor = modules.globals.eyebrows_mask_size
|
||||||
|
min_x = np.min(all_points[:, 0]) - 25 * padding_factor
|
||||||
|
max_x = np.max(all_points[:, 0]) + 25 * padding_factor
|
||||||
|
min_y = np.min(all_points[:, 1]) - 20 * padding_factor
|
||||||
|
max_y = np.max(all_points[:, 1]) + 15 * padding_factor
|
||||||
|
|
||||||
|
# Ensure coordinates are within frame bounds
|
||||||
|
min_x = max(0, int(min_x))
|
||||||
|
min_y = max(0, int(min_y))
|
||||||
|
max_x = min(frame.shape[1], int(max_x))
|
||||||
|
max_y = min(frame.shape[0], int(max_y))
|
||||||
|
|
||||||
|
# Create mask for the eyebrows region
|
||||||
|
mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert points to local coordinates
|
||||||
|
left_local = left_eyebrow - [min_x, min_y]
|
||||||
|
right_local = right_eyebrow - [min_x, min_y]
|
||||||
|
|
||||||
|
def create_curved_eyebrow(points):
|
||||||
|
if len(points) >= 5:
|
||||||
|
# Sort points by x-coordinate
|
||||||
|
sorted_idx = np.argsort(points[:, 0])
|
||||||
|
sorted_points = points[sorted_idx]
|
||||||
|
|
||||||
|
# Calculate dimensions
|
||||||
|
x_min, y_min = np.min(sorted_points, axis=0)
|
||||||
|
x_max, y_max = np.max(sorted_points, axis=0)
|
||||||
|
width = x_max - x_min
|
||||||
|
height = y_max - y_min
|
||||||
|
|
||||||
|
# Create more points for smoother curve
|
||||||
|
num_points = 50
|
||||||
|
x = np.linspace(x_min, x_max, num_points)
|
||||||
|
|
||||||
|
# Fit quadratic curve through points for more natural arch
|
||||||
|
coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2)
|
||||||
|
y = np.polyval(coeffs, x)
|
||||||
|
|
||||||
|
# Increased offsets to create more separation
|
||||||
|
top_offset = height * 0.5 # Increased from 0.3 to shift up more
|
||||||
|
bottom_offset = height * 0.2 # Increased from 0.1 to shift down more
|
||||||
|
|
||||||
|
# Create smooth curves
|
||||||
|
top_curve = y - top_offset
|
||||||
|
bottom_curve = y + bottom_offset
|
||||||
|
|
||||||
|
# Create curved endpoints with more pronounced taper
|
||||||
|
end_points = 5
|
||||||
|
start_x = np.linspace(x[0] - width * 0.15, x[0], end_points) # Increased taper
|
||||||
|
end_x = np.linspace(x[-1], x[-1] + width * 0.15, end_points) # Increased taper
|
||||||
|
|
||||||
|
# Create tapered ends
|
||||||
|
start_curve = np.column_stack((
|
||||||
|
start_x,
|
||||||
|
np.linspace(bottom_curve[0], top_curve[0], end_points)
|
||||||
|
))
|
||||||
|
end_curve = np.column_stack((
|
||||||
|
end_x,
|
||||||
|
np.linspace(bottom_curve[-1], top_curve[-1], end_points)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Combine all points to form a smooth contour
|
||||||
|
contour_points = np.vstack([
|
||||||
|
start_curve,
|
||||||
|
np.column_stack((x, top_curve)),
|
||||||
|
end_curve,
|
||||||
|
np.column_stack((x[::-1], bottom_curve[::-1]))
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add slight padding for better coverage
|
||||||
|
center = np.mean(contour_points, axis=0)
|
||||||
|
vectors = contour_points - center
|
||||||
|
padded_points = center + vectors * 1.2 # Increased padding slightly
|
||||||
|
|
||||||
|
return padded_points
|
||||||
|
return points
|
||||||
|
|
||||||
|
# Generate and draw eyebrow shapes
|
||||||
|
left_shape = create_curved_eyebrow(left_local)
|
||||||
|
right_shape = create_curved_eyebrow(right_local)
|
||||||
|
|
||||||
|
# Apply multi-stage blurring for natural feathering (GPU-accelerated when available)
|
||||||
|
# First, strong Gaussian blur for initial softening
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (21, 21), 7)
|
||||||
|
|
||||||
|
# Second, medium blur for transition areas
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (11, 11), 3)
|
||||||
|
|
||||||
|
# Finally, light blur for fine details
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (5, 5), 1)
|
||||||
|
|
||||||
|
# Normalize mask values
|
||||||
|
mask_roi = cv2.normalize(mask_roi, None, 0, 255, cv2.NORM_MINMAX)
|
||||||
|
|
||||||
|
# Place the mask ROI in the full-sized mask
|
||||||
|
mask[min_y:max_y, min_x:max_x] = mask_roi
|
||||||
|
|
||||||
|
# Extract the masked area from the frame
|
||||||
|
eyebrows_cutout = frame[min_y:max_y, min_x:max_x].copy()
|
||||||
|
|
||||||
|
# Combine points for visualization
|
||||||
|
eyebrows_polygon = np.vstack([
|
||||||
|
left_shape + [min_x, min_y],
|
||||||
|
right_shape + [min_x, min_y]
|
||||||
|
]).astype(np.int32)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to simple polygons if curve fitting fails
|
||||||
|
left_local = left_eyebrow - [min_x, min_y]
|
||||||
|
right_local = right_eyebrow - [min_x, min_y]
|
||||||
|
cv2.fillPoly(mask_roi, [left_local.astype(np.int32)], 255)
|
||||||
|
cv2.fillPoly(mask_roi, [right_local.astype(np.int32)], 255)
|
||||||
|
mask_roi = gpu_gaussian_blur(mask_roi, (21, 21), 7)
|
||||||
|
mask[min_y:max_y, min_x:max_x] = mask_roi
|
||||||
|
eyebrows_cutout = frame[min_y:max_y, min_x:max_x].copy()
|
||||||
|
eyebrows_polygon = np.vstack([left_eyebrow, right_eyebrow]).astype(np.int32)
|
||||||
|
|
||||||
|
return mask, eyebrows_cutout, (min_x, min_y, max_x, max_y), eyebrows_polygon
|
||||||
|
|
||||||
|
def apply_mask_area(
|
||||||
|
frame: np.ndarray,
|
||||||
|
cutout: np.ndarray,
|
||||||
|
box: tuple,
|
||||||
|
face_mask: np.ndarray,
|
||||||
|
polygon: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
min_x, min_y, max_x, max_y = box
|
||||||
|
box_width = max_x - min_x
|
||||||
|
box_height = max_y - min_y
|
||||||
|
|
||||||
|
if (
|
||||||
|
cutout is None
|
||||||
|
or box_width is None
|
||||||
|
or box_height is None
|
||||||
|
or face_mask is None
|
||||||
|
or polygon is None
|
||||||
|
):
|
||||||
|
return frame
|
||||||
|
|
||||||
|
try:
|
||||||
|
resized_cutout = gpu_resize(cutout, (box_width, box_height))
|
||||||
|
roi = frame[min_y:max_y, min_x:max_x]
|
||||||
|
|
||||||
|
if roi.shape != resized_cutout.shape:
|
||||||
|
resized_cutout = gpu_resize(
|
||||||
|
resized_cutout, (roi.shape[1], roi.shape[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
color_corrected_area = apply_color_transfer(resized_cutout, roi)
|
||||||
|
|
||||||
|
# Create mask for the area
|
||||||
|
polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8)
|
||||||
|
|
||||||
|
# Split points for left and right parts if needed
|
||||||
|
if len(polygon) > 50: # Arbitrary threshold to detect if we have multiple parts
|
||||||
|
mid_point = len(polygon) // 2
|
||||||
|
left_points = polygon[:mid_point] - [min_x, min_y]
|
||||||
|
right_points = polygon[mid_point:] - [min_x, min_y]
|
||||||
|
cv2.fillPoly(polygon_mask, [left_points], 255)
|
||||||
|
cv2.fillPoly(polygon_mask, [right_points], 255)
|
||||||
|
else:
|
||||||
|
adjusted_polygon = polygon - [min_x, min_y]
|
||||||
|
cv2.fillPoly(polygon_mask, [adjusted_polygon], 255)
|
||||||
|
|
||||||
|
# Apply strong initial feathering (GPU-accelerated when available)
|
||||||
|
polygon_mask = gpu_gaussian_blur(polygon_mask, (21, 21), 7)
|
||||||
|
|
||||||
|
# Apply additional feathering
|
||||||
|
feather_amount = min(
|
||||||
|
30,
|
||||||
|
box_width // modules.globals.mask_feather_ratio,
|
||||||
|
box_height // modules.globals.mask_feather_ratio,
|
||||||
|
)
|
||||||
|
feathered_mask = cv2.GaussianBlur(
|
||||||
|
polygon_mask.astype(np.float32), (0, 0), feather_amount
|
||||||
|
)
|
||||||
|
max_val = feathered_mask.max()
|
||||||
|
if max_val > 1e-6:
|
||||||
|
feathered_mask *= np.float32(1.0 / max_val)
|
||||||
|
|
||||||
|
# Apply additional smoothing to the mask edges
|
||||||
|
feathered_mask = cv2.GaussianBlur(feathered_mask, (5, 5), 1)
|
||||||
|
|
||||||
|
face_mask_roi = face_mask[min_y:max_y, min_x:max_x]
|
||||||
|
combined_mask = feathered_mask * (face_mask_roi.astype(np.float32) * np.float32(1.0 / 255.0))
|
||||||
|
|
||||||
|
combined_mask_3ch = combined_mask[:, :, np.newaxis]
|
||||||
|
inv_mask = np.float32(1.0) - combined_mask_3ch
|
||||||
|
blended = (
|
||||||
|
color_corrected_area * combined_mask_3ch + roi * inv_mask
|
||||||
|
).astype(np.uint8)
|
||||||
|
|
||||||
|
# Apply face mask to blended result
|
||||||
|
face_mask_f32 = face_mask_roi[:, :, np.newaxis].astype(np.float32) * np.float32(1.0 / 255.0)
|
||||||
|
face_mask_3channel = np.broadcast_to(face_mask_f32, blended.shape)
|
||||||
|
final_blend = blended * face_mask_3channel + roi * (np.float32(1.0) - face_mask_3channel)
|
||||||
|
|
||||||
|
frame[min_y:max_y, min_x:max_x] = final_blend.astype(np.uint8)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def draw_mask_visualization(
|
||||||
|
frame: Frame,
|
||||||
|
mask_data: tuple,
|
||||||
|
label: str,
|
||||||
|
draw_method: str = "polygon"
|
||||||
|
) -> Frame:
|
||||||
|
mask, cutout, (min_x, min_y, max_x, max_y), polygon = mask_data
|
||||||
|
|
||||||
|
vis_frame = frame.copy()
|
||||||
|
|
||||||
|
# Ensure coordinates are within frame bounds
|
||||||
|
height, width = vis_frame.shape[:2]
|
||||||
|
min_x, min_y = max(0, min_x), max(0, min_y)
|
||||||
|
max_x, max_y = min(width, max_x), min(height, max_y)
|
||||||
|
|
||||||
|
if draw_method == "ellipse" and len(polygon) > 50: # For eyes
|
||||||
|
# Split points for left and right parts
|
||||||
|
mid_point = len(polygon) // 2
|
||||||
|
left_points = polygon[:mid_point]
|
||||||
|
right_points = polygon[mid_point:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fit ellipses to points - need at least 5 points
|
||||||
|
if len(left_points) >= 5 and len(right_points) >= 5:
|
||||||
|
# Convert points to the correct format for ellipse fitting
|
||||||
|
left_points = left_points.astype(np.float32)
|
||||||
|
right_points = right_points.astype(np.float32)
|
||||||
|
|
||||||
|
# Fit ellipses
|
||||||
|
left_ellipse = cv2.fitEllipse(left_points)
|
||||||
|
right_ellipse = cv2.fitEllipse(right_points)
|
||||||
|
|
||||||
|
# Draw the ellipses
|
||||||
|
cv2.ellipse(vis_frame, left_ellipse, (0, 255, 0), 2)
|
||||||
|
cv2.ellipse(vis_frame, right_ellipse, (0, 255, 0), 2)
|
||||||
|
except Exception as e:
|
||||||
|
# If ellipse fitting fails, draw simple rectangles as fallback
|
||||||
|
left_rect = cv2.boundingRect(left_points)
|
||||||
|
right_rect = cv2.boundingRect(right_points)
|
||||||
|
cv2.rectangle(vis_frame,
|
||||||
|
(left_rect[0], left_rect[1]),
|
||||||
|
(left_rect[0] + left_rect[2], left_rect[1] + left_rect[3]),
|
||||||
|
(0, 255, 0), 2)
|
||||||
|
cv2.rectangle(vis_frame,
|
||||||
|
(right_rect[0], right_rect[1]),
|
||||||
|
(right_rect[0] + right_rect[2], right_rect[1] + right_rect[3]),
|
||||||
|
(0, 255, 0), 2)
|
||||||
|
else: # For mouth and eyebrows
|
||||||
|
# Draw the polygon
|
||||||
|
if len(polygon) > 50: # If we have multiple parts
|
||||||
|
mid_point = len(polygon) // 2
|
||||||
|
left_points = polygon[:mid_point]
|
||||||
|
right_points = polygon[mid_point:]
|
||||||
|
cv2.polylines(vis_frame, [left_points], True, (0, 255, 0), 2, cv2.LINE_AA)
|
||||||
|
cv2.polylines(vis_frame, [right_points], True, (0, 255, 0), 2, cv2.LINE_AA)
|
||||||
|
else:
|
||||||
|
cv2.polylines(vis_frame, [polygon], True, (0, 255, 0), 2, cv2.LINE_AA)
|
||||||
|
|
||||||
|
# Add label
|
||||||
|
cv2.putText(
|
||||||
|
vis_frame,
|
||||||
|
label,
|
||||||
|
(min_x, min_y - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.5,
|
||||||
|
(255, 255, 255),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return vis_frame
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Import the tkinter fix to patch the ScreenChanged error
|
||||||
|
import tkinter_fix
|
||||||
|
|
||||||
|
import core
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
core.run()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import tkinter
|
||||||
|
|
||||||
|
# Only needs to be imported once at the beginning of the application
|
||||||
|
def apply_patch():
|
||||||
|
# Create a monkey patch for the internal _tkinter module
|
||||||
|
original_init = tkinter.Tk.__init__
|
||||||
|
|
||||||
|
def patched_init(self, *args, **kwargs):
|
||||||
|
# Call the original init
|
||||||
|
original_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
# Define the missing ::tk::ScreenChanged procedure
|
||||||
|
self.tk.eval("""
|
||||||
|
if {[info commands ::tk::ScreenChanged] == ""} {
|
||||||
|
proc ::tk::ScreenChanged {args} {
|
||||||
|
# Do nothing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Apply the monkey patch
|
||||||
|
tkinter.Tk.__init__ = patched_init
|
||||||
|
|
||||||
|
# Apply the patch automatically when this module is imported
|
||||||
|
apply_patch()
|
||||||
+495
-124
@@ -3,14 +3,20 @@ import webbrowser
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from typing import Callable, Tuple
|
from typing import Callable, Tuple
|
||||||
import cv2
|
import cv2
|
||||||
from cv2_enumerate_cameras import enumerate_cameras # Add this import
|
from modules.gpu_processing import gpu_cvt_color, gpu_resize, gpu_flip
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
import modules.globals
|
import modules.globals
|
||||||
import modules.metadata
|
import modules.metadata
|
||||||
from modules.face_analyser import (
|
from modules.face_analyser import (
|
||||||
get_one_face,
|
get_one_face,
|
||||||
|
get_many_faces,
|
||||||
get_unique_faces_from_target_image,
|
get_unique_faces_from_target_image,
|
||||||
get_unique_faces_from_target_video,
|
get_unique_faces_from_target_video,
|
||||||
add_blank_map,
|
add_blank_map,
|
||||||
@@ -27,15 +33,40 @@ from modules.utilities import (
|
|||||||
)
|
)
|
||||||
from modules.video_capture import VideoCapturer
|
from modules.video_capture import VideoCapturer
|
||||||
from modules.gettext import LanguageManager
|
from modules.gettext import LanguageManager
|
||||||
|
from modules.ui_tooltip import ToolTip
|
||||||
|
from modules import globals
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
from pygrabber.dshow_graph import FilterGraph
|
from pygrabber.dshow_graph import FilterGraph
|
||||||
|
|
||||||
|
# --- Tk 9.0 compatibility patch ---
|
||||||
|
# In Tk 9.0, Menu.index("end") returns "" instead of raising TclError
|
||||||
|
# when the menu is empty. CustomTkinter's CTkOptionMenu doesn't handle
|
||||||
|
# this, causing crashes. This patch adds the missing guard.
|
||||||
|
try:
|
||||||
|
from customtkinter.windows.widgets.core_widget_classes import DropdownMenu as _DropdownMenu
|
||||||
|
|
||||||
|
_original_add_menu_commands = _DropdownMenu._add_menu_commands
|
||||||
|
|
||||||
|
def _patched_add_menu_commands(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
end_index = self._menu.index("end")
|
||||||
|
if end_index == "" or end_index is None:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_original_add_menu_commands(self, *args, **kwargs)
|
||||||
|
|
||||||
|
_DropdownMenu._add_menu_commands = _patched_add_menu_commands
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass # CustomTkinter version doesn't have this class path
|
||||||
|
# --- End Tk 9.0 patch ---
|
||||||
|
|
||||||
ROOT = None
|
ROOT = None
|
||||||
POPUP = None
|
POPUP = None
|
||||||
POPUP_LIVE = None
|
POPUP_LIVE = None
|
||||||
ROOT_HEIGHT = 700
|
ROOT_HEIGHT = 800
|
||||||
ROOT_WIDTH = 600
|
ROOT_WIDTH = 600
|
||||||
|
|
||||||
PREVIEW = None
|
PREVIEW = None
|
||||||
@@ -97,6 +128,7 @@ def save_switch_states():
|
|||||||
"keep_frames": modules.globals.keep_frames,
|
"keep_frames": modules.globals.keep_frames,
|
||||||
"many_faces": modules.globals.many_faces,
|
"many_faces": modules.globals.many_faces,
|
||||||
"map_faces": modules.globals.map_faces,
|
"map_faces": modules.globals.map_faces,
|
||||||
|
"poisson_blend": modules.globals.poisson_blend,
|
||||||
"color_correction": modules.globals.color_correction,
|
"color_correction": modules.globals.color_correction,
|
||||||
"nsfw_filter": modules.globals.nsfw_filter,
|
"nsfw_filter": modules.globals.nsfw_filter,
|
||||||
"live_mirror": modules.globals.live_mirror,
|
"live_mirror": modules.globals.live_mirror,
|
||||||
@@ -105,6 +137,7 @@ def save_switch_states():
|
|||||||
"show_fps": modules.globals.show_fps,
|
"show_fps": modules.globals.show_fps,
|
||||||
"mouth_mask": modules.globals.mouth_mask,
|
"mouth_mask": modules.globals.mouth_mask,
|
||||||
"show_mouth_mask_box": modules.globals.show_mouth_mask_box,
|
"show_mouth_mask_box": modules.globals.show_mouth_mask_box,
|
||||||
|
"mouth_mask_size": modules.globals.mouth_mask_size,
|
||||||
}
|
}
|
||||||
with open("switch_states.json", "w") as f:
|
with open("switch_states.json", "w") as f:
|
||||||
json.dump(switch_states, f)
|
json.dump(switch_states, f)
|
||||||
@@ -119,16 +152,17 @@ def load_switch_states():
|
|||||||
modules.globals.keep_frames = switch_states.get("keep_frames", False)
|
modules.globals.keep_frames = switch_states.get("keep_frames", False)
|
||||||
modules.globals.many_faces = switch_states.get("many_faces", False)
|
modules.globals.many_faces = switch_states.get("many_faces", False)
|
||||||
modules.globals.map_faces = switch_states.get("map_faces", False)
|
modules.globals.map_faces = switch_states.get("map_faces", False)
|
||||||
|
modules.globals.poisson_blend = switch_states.get("poisson_blend", False)
|
||||||
modules.globals.color_correction = switch_states.get("color_correction", False)
|
modules.globals.color_correction = switch_states.get("color_correction", False)
|
||||||
modules.globals.nsfw_filter = switch_states.get("nsfw_filter", False)
|
modules.globals.nsfw_filter = switch_states.get("nsfw_filter", False)
|
||||||
modules.globals.live_mirror = switch_states.get("live_mirror", False)
|
modules.globals.live_mirror = switch_states.get("live_mirror", False)
|
||||||
modules.globals.live_resizable = switch_states.get("live_resizable", False)
|
modules.globals.live_resizable = switch_states.get("live_resizable", False)
|
||||||
modules.globals.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False})
|
modules.globals.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False})
|
||||||
modules.globals.show_fps = switch_states.get("show_fps", False)
|
modules.globals.show_fps = switch_states.get("show_fps", False)
|
||||||
modules.globals.mouth_mask = switch_states.get("mouth_mask", False)
|
modules.globals.mouth_mask_size = switch_states.get("mouth_mask_size", 0.0)
|
||||||
modules.globals.show_mouth_mask_box = switch_states.get(
|
# mouth_mask is driven by the slider: on if size > 0, off if 0
|
||||||
"show_mouth_mask_box", False
|
modules.globals.mouth_mask = modules.globals.mouth_mask_size > 0
|
||||||
)
|
modules.globals.show_mouth_mask_box = False # always start hidden
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# If the file doesn't exist, use default values
|
# If the file doesn't exist, use default values
|
||||||
pass
|
pass
|
||||||
@@ -152,20 +186,28 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
root.protocol("WM_DELETE_WINDOW", lambda: destroy())
|
root.protocol("WM_DELETE_WINDOW", lambda: destroy())
|
||||||
|
|
||||||
source_label = ctk.CTkLabel(root, text=None)
|
source_label = ctk.CTkLabel(root, text=None)
|
||||||
source_label.place(relx=0.1, rely=0.1, relwidth=0.3, relheight=0.25)
|
source_label.place(relx=0.1, rely=0.05, relwidth=0.275, relheight=0.225)
|
||||||
|
|
||||||
target_label = ctk.CTkLabel(root, text=None)
|
target_label = ctk.CTkLabel(root, text=None)
|
||||||
target_label.place(relx=0.6, rely=0.1, relwidth=0.3, relheight=0.25)
|
target_label.place(relx=0.6, rely=0.05, relwidth=0.275, relheight=0.225)
|
||||||
|
|
||||||
select_face_button = ctk.CTkButton(
|
select_face_button = ctk.CTkButton(
|
||||||
root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path()
|
root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path()
|
||||||
)
|
)
|
||||||
select_face_button.place(relx=0.1, rely=0.4, relwidth=0.3, relheight=0.1)
|
select_face_button.place(relx=0.1, rely=0.30, relwidth=0.24, relheight=0.1)
|
||||||
|
ToolTip(select_face_button, _("Choose the source face image to swap onto the target"))
|
||||||
|
|
||||||
|
random_face_button = ctk.CTkButton(
|
||||||
|
root, text="🔄", cursor="hand2", width=30, command=lambda: fetch_random_face()
|
||||||
|
)
|
||||||
|
random_face_button.place(relx=0.35, rely=0.30, relwidth=0.05, relheight=0.1)
|
||||||
|
ToolTip(random_face_button, _("Get a random face from thispersondoesnotexist.com"))
|
||||||
|
|
||||||
swap_faces_button = ctk.CTkButton(
|
swap_faces_button = ctk.CTkButton(
|
||||||
root, text="↔", cursor="hand2", command=lambda: swap_faces_paths()
|
root, text="↔", cursor="hand2", command=lambda: swap_faces_paths()
|
||||||
)
|
)
|
||||||
swap_faces_button.place(relx=0.45, rely=0.4, relwidth=0.1, relheight=0.1)
|
swap_faces_button.place(relx=0.45, rely=0.30, relwidth=0.1, relheight=0.1)
|
||||||
|
ToolTip(swap_faces_button, _("Swap source and target images"))
|
||||||
|
|
||||||
select_target_button = ctk.CTkButton(
|
select_target_button = ctk.CTkButton(
|
||||||
root,
|
root,
|
||||||
@@ -173,7 +215,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
command=lambda: select_target_path(),
|
command=lambda: select_target_path(),
|
||||||
)
|
)
|
||||||
select_target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1)
|
select_target_button.place(relx=0.6, rely=0.30, relwidth=0.3, relheight=0.1)
|
||||||
|
ToolTip(select_target_button, _("Choose the target image or video to apply face swap to"))
|
||||||
|
|
||||||
keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps)
|
keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps)
|
||||||
keep_fps_checkbox = ctk.CTkSwitch(
|
keep_fps_checkbox = ctk.CTkSwitch(
|
||||||
@@ -186,7 +229,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
keep_fps_checkbox.place(relx=0.1, rely=0.6)
|
keep_fps_checkbox.place(relx=0.1, rely=0.42)
|
||||||
|
ToolTip(keep_fps_checkbox, _("Output video keeps the original frame rate"))
|
||||||
|
|
||||||
keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames)
|
keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames)
|
||||||
keep_frames_switch = ctk.CTkSwitch(
|
keep_frames_switch = ctk.CTkSwitch(
|
||||||
@@ -199,20 +243,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
keep_frames_switch.place(relx=0.1, rely=0.65)
|
keep_frames_switch.place(relx=0.1, rely=0.47)
|
||||||
|
ToolTip(keep_frames_switch, _("Keep extracted frames on disk after processing"))
|
||||||
enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"])
|
|
||||||
enhancer_switch = ctk.CTkSwitch(
|
|
||||||
root,
|
|
||||||
text=_("Face Enhancer"),
|
|
||||||
variable=enhancer_value,
|
|
||||||
cursor="hand2",
|
|
||||||
command=lambda: (
|
|
||||||
update_tumbler("face_enhancer", enhancer_value.get()),
|
|
||||||
save_switch_states(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
enhancer_switch.place(relx=0.1, rely=0.7)
|
|
||||||
|
|
||||||
keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio)
|
keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio)
|
||||||
keep_audio_switch = ctk.CTkSwitch(
|
keep_audio_switch = ctk.CTkSwitch(
|
||||||
@@ -225,7 +257,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
keep_audio_switch.place(relx=0.6, rely=0.6)
|
keep_audio_switch.place(relx=0.6, rely=0.42)
|
||||||
|
ToolTip(keep_audio_switch, _("Copy audio track from the source video to output"))
|
||||||
|
|
||||||
many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces)
|
many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces)
|
||||||
many_faces_switch = ctk.CTkSwitch(
|
many_faces_switch = ctk.CTkSwitch(
|
||||||
@@ -238,7 +271,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
many_faces_switch.place(relx=0.6, rely=0.65)
|
many_faces_switch.place(relx=0.6, rely=0.47)
|
||||||
|
ToolTip(many_faces_switch, _("Swap every detected face, not just the primary one"))
|
||||||
|
|
||||||
color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction)
|
color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction)
|
||||||
color_correction_switch = ctk.CTkSwitch(
|
color_correction_switch = ctk.CTkSwitch(
|
||||||
@@ -251,7 +285,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
color_correction_switch.place(relx=0.6, rely=0.70)
|
color_correction_switch.place(relx=0.6, rely=0.57)
|
||||||
|
ToolTip(color_correction_switch, _("Fix blue/green color cast from some webcams"))
|
||||||
|
|
||||||
# nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter)
|
# nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter)
|
||||||
# nsfw_switch = ctk.CTkSwitch(root, text='NSFW filter', variable=nsfw_value, cursor='hand2', command=lambda: setattr(modules.globals, 'nsfw_filter', nsfw_value.get()))
|
# nsfw_switch = ctk.CTkSwitch(root, text='NSFW filter', variable=nsfw_value, cursor='hand2', command=lambda: setattr(modules.globals, 'nsfw_filter', nsfw_value.get()))
|
||||||
@@ -269,7 +304,22 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
close_mapper_window() if not map_faces.get() else None
|
close_mapper_window() if not map_faces.get() else None
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
map_faces_switch.place(relx=0.1, rely=0.75)
|
map_faces_switch.place(relx=0.1, rely=0.52)
|
||||||
|
ToolTip(map_faces_switch, _("Manually assign which source face maps to which target face"))
|
||||||
|
|
||||||
|
poisson_blend_value = ctk.BooleanVar(value=modules.globals.poisson_blend)
|
||||||
|
poisson_blend_switch = ctk.CTkSwitch(
|
||||||
|
root,
|
||||||
|
text=_("Poisson Blend"),
|
||||||
|
variable=poisson_blend_value,
|
||||||
|
cursor="hand2",
|
||||||
|
command=lambda: (
|
||||||
|
setattr(modules.globals, "poisson_blend", poisson_blend_value.get()),
|
||||||
|
save_switch_states(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
poisson_blend_switch.place(relx=0.1, rely=0.57)
|
||||||
|
ToolTip(poisson_blend_switch, _("Blend face edges smoothly using Poisson blending"))
|
||||||
|
|
||||||
show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps)
|
show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps)
|
||||||
show_fps_switch = ctk.CTkSwitch(
|
show_fps_switch = ctk.CTkSwitch(
|
||||||
@@ -282,48 +332,34 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
save_switch_states(),
|
save_switch_states(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
show_fps_switch.place(relx=0.6, rely=0.75)
|
show_fps_switch.place(relx=0.6, rely=0.52)
|
||||||
|
ToolTip(show_fps_switch, _("Display frames-per-second counter on the live preview"))
|
||||||
|
|
||||||
|
# mouth_mask and show_mouth_mask_box are auto-controlled by the Mouth Mask slider
|
||||||
mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask)
|
mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask)
|
||||||
mouth_mask_switch = ctk.CTkSwitch(
|
|
||||||
root,
|
|
||||||
text=_("Mouth Mask"),
|
|
||||||
variable=mouth_mask_var,
|
|
||||||
cursor="hand2",
|
|
||||||
command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()),
|
|
||||||
)
|
|
||||||
mouth_mask_switch.place(relx=0.1, rely=0.55)
|
|
||||||
|
|
||||||
show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box)
|
show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box)
|
||||||
show_mouth_mask_box_switch = ctk.CTkSwitch(
|
|
||||||
root,
|
|
||||||
text=_("Show Mouth Mask Box"),
|
|
||||||
variable=show_mouth_mask_box_var,
|
|
||||||
cursor="hand2",
|
|
||||||
command=lambda: setattr(
|
|
||||||
modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
show_mouth_mask_box_switch.place(relx=0.6, rely=0.55)
|
|
||||||
|
|
||||||
start_button = ctk.CTkButton(
|
start_button = ctk.CTkButton(
|
||||||
root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root)
|
root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root)
|
||||||
)
|
)
|
||||||
start_button.place(relx=0.15, rely=0.80, relwidth=0.2, relheight=0.05)
|
start_button.place(relx=0.15, rely=0.78, relwidth=0.2, relheight=0.04)
|
||||||
|
ToolTip(start_button, _("Begin processing the target image/video with selected face"))
|
||||||
|
|
||||||
stop_button = ctk.CTkButton(
|
stop_button = ctk.CTkButton(
|
||||||
root, text=_("Destroy"), cursor="hand2", command=lambda: destroy()
|
root, text=_("Destroy"), cursor="hand2", command=lambda: destroy()
|
||||||
)
|
)
|
||||||
stop_button.place(relx=0.4, rely=0.80, relwidth=0.2, relheight=0.05)
|
stop_button.place(relx=0.4, rely=0.78, relwidth=0.2, relheight=0.04)
|
||||||
|
ToolTip(stop_button, _("Stop processing and close the application"))
|
||||||
|
|
||||||
preview_button = ctk.CTkButton(
|
preview_button = ctk.CTkButton(
|
||||||
root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview()
|
root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview()
|
||||||
)
|
)
|
||||||
preview_button.place(relx=0.65, rely=0.80, relwidth=0.2, relheight=0.05)
|
preview_button.place(relx=0.65, rely=0.78, relwidth=0.2, relheight=0.04)
|
||||||
|
ToolTip(preview_button, _("Show/hide a preview of the processed output"))
|
||||||
|
|
||||||
# --- Camera Selection ---
|
# --- Camera Selection ---
|
||||||
camera_label = ctk.CTkLabel(root, text=_("Select Camera:"))
|
camera_label = ctk.CTkLabel(root, text=_("Select Camera:"))
|
||||||
camera_label.place(relx=0.1, rely=0.86, relwidth=0.2, relheight=0.05)
|
camera_label.place(relx=0.1, rely=0.83, relwidth=0.2, relheight=0.03)
|
||||||
|
|
||||||
available_cameras = get_available_cameras()
|
available_cameras = get_available_cameras()
|
||||||
camera_indices, camera_names = available_cameras
|
camera_indices, camera_names = available_cameras
|
||||||
@@ -342,7 +378,8 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
root, variable=camera_variable, values=camera_names
|
root, variable=camera_variable, values=camera_names
|
||||||
)
|
)
|
||||||
|
|
||||||
camera_optionmenu.place(relx=0.35, rely=0.86, relwidth=0.25, relheight=0.05)
|
camera_optionmenu.place(relx=0.35, rely=0.83, relwidth=0.25, relheight=0.03)
|
||||||
|
ToolTip(camera_optionmenu, _("Select which camera to use for live mode"))
|
||||||
|
|
||||||
live_button = ctk.CTkButton(
|
live_button = ctk.CTkButton(
|
||||||
root,
|
root,
|
||||||
@@ -362,16 +399,173 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
else "disabled"
|
else "disabled"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
live_button.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05)
|
live_button.place(relx=0.65, rely=0.83, relwidth=0.2, relheight=0.03)
|
||||||
|
ToolTip(live_button, _("Start real-time face swap using webcam"))
|
||||||
# --- End Camera Selection ---
|
# --- End Camera Selection ---
|
||||||
|
|
||||||
|
# --- Face Enhancer Dropdown ---
|
||||||
|
enhancer_options = ["None", "GFPGAN", "GPEN-512", "GPEN-256"]
|
||||||
|
enhancer_key_map = {
|
||||||
|
"None": None,
|
||||||
|
"GFPGAN": "face_enhancer",
|
||||||
|
"GPEN-512": "face_enhancer_gpen512",
|
||||||
|
"GPEN-256": "face_enhancer_gpen256",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine initial value from current fp_ui state
|
||||||
|
initial_enhancer = "None"
|
||||||
|
if modules.globals.fp_ui.get("face_enhancer", False):
|
||||||
|
initial_enhancer = "GFPGAN"
|
||||||
|
elif modules.globals.fp_ui.get("face_enhancer_gpen512", False):
|
||||||
|
initial_enhancer = "GPEN-512"
|
||||||
|
elif modules.globals.fp_ui.get("face_enhancer_gpen256", False):
|
||||||
|
initial_enhancer = "GPEN-256"
|
||||||
|
|
||||||
|
enhancer_variable = ctk.StringVar(value=initial_enhancer)
|
||||||
|
|
||||||
|
def on_enhancer_change(choice: str):
|
||||||
|
# Disable all enhancers first
|
||||||
|
for key in ["face_enhancer", "face_enhancer_gpen256", "face_enhancer_gpen512"]:
|
||||||
|
update_tumbler(key, False)
|
||||||
|
# Enable the selected one
|
||||||
|
selected_key = enhancer_key_map.get(choice)
|
||||||
|
if selected_key:
|
||||||
|
update_tumbler(selected_key, True)
|
||||||
|
save_switch_states()
|
||||||
|
|
||||||
|
enhancer_label = ctk.CTkLabel(root, text="Face Enhancer:")
|
||||||
|
enhancer_label.place(relx=0.1, rely=0.62, relwidth=0.2, relheight=0.03)
|
||||||
|
|
||||||
|
enhancer_dropdown = ctk.CTkOptionMenu(
|
||||||
|
root,
|
||||||
|
variable=enhancer_variable,
|
||||||
|
values=enhancer_options,
|
||||||
|
command=on_enhancer_change,
|
||||||
|
)
|
||||||
|
enhancer_dropdown.place(relx=0.35, rely=0.62, relwidth=0.3, relheight=0.03)
|
||||||
|
ToolTip(enhancer_dropdown, _("Select a face enhancement model (None = no enhancement)"))
|
||||||
|
|
||||||
|
# 1) Define a DoubleVar for transparency (0 = fully transparent, 1 = fully opaque)
|
||||||
|
transparency_var = ctk.DoubleVar(value=1.0)
|
||||||
|
|
||||||
|
def on_transparency_change(value: float):
|
||||||
|
# Convert slider value to float
|
||||||
|
val = float(value)
|
||||||
|
modules.globals.opacity = val # Set global opacity
|
||||||
|
percentage = int(val * 100)
|
||||||
|
|
||||||
|
if percentage == 0:
|
||||||
|
modules.globals.fp_ui["face_enhancer"] = False
|
||||||
|
update_status("Transparency set to 0% - Face swapping disabled.")
|
||||||
|
elif percentage == 100:
|
||||||
|
modules.globals.face_swapper_enabled = True
|
||||||
|
update_status("Transparency set to 100%.")
|
||||||
|
else:
|
||||||
|
modules.globals.face_swapper_enabled = True
|
||||||
|
update_status(f"Transparency set to {percentage}%")
|
||||||
|
|
||||||
|
# 2) Transparency label and slider
|
||||||
|
transparency_label = ctk.CTkLabel(root, text="Transparency:")
|
||||||
|
transparency_label.place(relx=0.15, rely=0.66, relwidth=0.2, relheight=0.03)
|
||||||
|
|
||||||
|
transparency_slider = ctk.CTkSlider(
|
||||||
|
root,
|
||||||
|
from_=0.0,
|
||||||
|
to=1.0,
|
||||||
|
variable=transparency_var,
|
||||||
|
command=on_transparency_change,
|
||||||
|
fg_color="#E0E0E0",
|
||||||
|
progress_color="#007BFF",
|
||||||
|
button_color="#FFFFFF",
|
||||||
|
button_hover_color="#CCCCCC",
|
||||||
|
height=5,
|
||||||
|
border_width=1,
|
||||||
|
corner_radius=3,
|
||||||
|
)
|
||||||
|
transparency_slider.place(relx=0.35, rely=0.67, relwidth=0.5, relheight=0.02)
|
||||||
|
ToolTip(transparency_slider, _("Blend between original and swapped face (0% = original, 100% = fully swapped)"))
|
||||||
|
|
||||||
|
# 3) Sharpness label & slider
|
||||||
|
sharpness_var = ctk.DoubleVar(value=0.0) # start at 0.0
|
||||||
|
def on_sharpness_change(value: float):
|
||||||
|
modules.globals.sharpness = float(value)
|
||||||
|
update_status(f"Sharpness set to {value:.1f}")
|
||||||
|
|
||||||
|
sharpness_label = ctk.CTkLabel(root, text="Sharpness:")
|
||||||
|
sharpness_label.place(relx=0.15, rely=0.69, relwidth=0.2, relheight=0.03)
|
||||||
|
|
||||||
|
sharpness_slider = ctk.CTkSlider(
|
||||||
|
root,
|
||||||
|
from_=0.0,
|
||||||
|
to=5.0,
|
||||||
|
variable=sharpness_var,
|
||||||
|
command=on_sharpness_change,
|
||||||
|
fg_color="#E0E0E0",
|
||||||
|
progress_color="#007BFF",
|
||||||
|
button_color="#FFFFFF",
|
||||||
|
button_hover_color="#CCCCCC",
|
||||||
|
height=5,
|
||||||
|
border_width=1,
|
||||||
|
corner_radius=3,
|
||||||
|
)
|
||||||
|
sharpness_slider.place(relx=0.35, rely=0.70, relwidth=0.5, relheight=0.02)
|
||||||
|
ToolTip(sharpness_slider, _("Sharpen the enhanced face output"))
|
||||||
|
|
||||||
|
# 4) Mouth Mask Size slider
|
||||||
|
mouth_mask_size_var = ctk.DoubleVar(value=modules.globals.mouth_mask_size)
|
||||||
|
|
||||||
|
def on_mouth_mask_size_change(value: float):
|
||||||
|
val = float(value)
|
||||||
|
modules.globals.mouth_mask_size = val
|
||||||
|
# Auto-enable/disable mouth mask based on slider position
|
||||||
|
if val > 0:
|
||||||
|
modules.globals.mouth_mask = True
|
||||||
|
mouth_mask_var.set(True)
|
||||||
|
else:
|
||||||
|
modules.globals.mouth_mask = False
|
||||||
|
mouth_mask_var.set(False)
|
||||||
|
modules.globals.show_mouth_mask_box = False
|
||||||
|
|
||||||
|
def on_mouth_mask_slider_release(event):
|
||||||
|
# Hide bounding box when user releases the slider
|
||||||
|
modules.globals.show_mouth_mask_box = False
|
||||||
|
|
||||||
|
def on_mouth_mask_slider_press(event):
|
||||||
|
# Show bounding box while dragging
|
||||||
|
if modules.globals.mouth_mask_size > 0:
|
||||||
|
modules.globals.show_mouth_mask_box = True
|
||||||
|
|
||||||
|
mouth_mask_size_label = ctk.CTkLabel(root, text="Mouth Mask:")
|
||||||
|
mouth_mask_size_label.place(relx=0.15, rely=0.72, relwidth=0.2, relheight=0.03)
|
||||||
|
|
||||||
|
mouth_mask_size_slider = ctk.CTkSlider(
|
||||||
|
root,
|
||||||
|
from_=0.0,
|
||||||
|
to=100.0,
|
||||||
|
variable=mouth_mask_size_var,
|
||||||
|
command=on_mouth_mask_size_change,
|
||||||
|
fg_color="#E0E0E0",
|
||||||
|
progress_color="#007BFF",
|
||||||
|
button_color="#FFFFFF",
|
||||||
|
button_hover_color="#CCCCCC",
|
||||||
|
height=5,
|
||||||
|
border_width=1,
|
||||||
|
corner_radius=3,
|
||||||
|
)
|
||||||
|
mouth_mask_size_slider.place(relx=0.35, rely=0.73, relwidth=0.5, relheight=0.02)
|
||||||
|
mouth_mask_size_slider.bind("<ButtonPress-1>", on_mouth_mask_slider_press)
|
||||||
|
mouth_mask_size_slider.bind("<ButtonRelease-1>", on_mouth_mask_slider_release)
|
||||||
|
ToolTip(mouth_mask_size_slider, _("0 = use swapped mouth, 100 = expose original mouth to chin area"))
|
||||||
|
|
||||||
|
# Status and link at the bottom
|
||||||
|
global status_label
|
||||||
status_label = ctk.CTkLabel(root, text=None, justify="center")
|
status_label = ctk.CTkLabel(root, text=None, justify="center")
|
||||||
status_label.place(relx=0.1, rely=0.9, relwidth=0.8)
|
status_label.place(relx=0.1, rely=0.75, relwidth=0.8)
|
||||||
|
|
||||||
donate_label = ctk.CTkLabel(
|
donate_label = ctk.CTkLabel(
|
||||||
root, text="Deep Live Cam", justify="center", cursor="hand2"
|
root, text="Deep Live Cam", justify="center", cursor="hand2"
|
||||||
)
|
)
|
||||||
donate_label.place(relx=0.1, rely=0.95, relwidth=0.8)
|
donate_label.place(relx=0.1, rely=0.87, relwidth=0.8)
|
||||||
donate_label.configure(
|
donate_label.configure(
|
||||||
text_color=ctk.ThemeManager.theme.get("URL").get("text_color")
|
text_color=ctk.ThemeManager.theme.get("URL").get("text_color")
|
||||||
)
|
)
|
||||||
@@ -381,6 +575,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
|
|||||||
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
def close_mapper_window():
|
def close_mapper_window():
|
||||||
global POPUP, POPUP_LIVE
|
global POPUP, POPUP_LIVE
|
||||||
if POPUP and POPUP.winfo_exists():
|
if POPUP and POPUP.winfo_exists():
|
||||||
@@ -459,7 +654,7 @@ def create_source_target_popup(
|
|||||||
)
|
)
|
||||||
x_label.grid(row=id, column=2, padx=10, pady=10)
|
x_label.grid(row=id, column=2, padx=10, pady=10)
|
||||||
|
|
||||||
image = Image.fromarray(cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB))
|
image = Image.fromarray(gpu_cvt_color(item["target"]["cv2"], cv2.COLOR_BGR2RGB))
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
)
|
)
|
||||||
@@ -514,7 +709,7 @@ def update_popup_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
image = Image.fromarray(
|
image = Image.fromarray(
|
||||||
cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
gpu_cvt_color(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
||||||
)
|
)
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
@@ -579,6 +774,26 @@ def update_tumbler(var: str, value: bool) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_random_face() -> None:
|
||||||
|
PREVIEW.withdraw()
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
"https://thispersondoesnotexist.com/",
|
||||||
|
headers={"User-Agent": "Mozilla/5.0"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
temp_dir = tempfile.gettempdir()
|
||||||
|
temp_path = os.path.join(temp_dir, "deep_live_cam_random_face.jpg")
|
||||||
|
with open(temp_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
modules.globals.source_path = temp_path
|
||||||
|
image = render_image_preview(temp_path, (200, 200))
|
||||||
|
source_label.configure(image=image)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to fetch random face: {e}")
|
||||||
|
|
||||||
|
|
||||||
def select_source_path() -> None:
|
def select_source_path() -> None:
|
||||||
global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft
|
global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft
|
||||||
|
|
||||||
@@ -696,22 +911,18 @@ def check_and_ignore_nsfw(target, destroy: Callable = None) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def fit_image_to_size(image, width: int, height: int):
|
def fit_image_to_size(image, width: int, height: int):
|
||||||
if width is None or height is None or width <= 0 or height <= 0:
|
if width is None and height is None:
|
||||||
return image
|
return image
|
||||||
h, w, _ = image.shape
|
h, w, _ = image.shape
|
||||||
ratio_h = 0.0
|
ratio_h = 0.0
|
||||||
ratio_w = 0.0
|
ratio_w = 0.0
|
||||||
ratio_w = width / w
|
if width > height:
|
||||||
ratio_h = height / h
|
ratio_h = height / h
|
||||||
# Use the smaller ratio to ensure the image fits within the given dimensions
|
else:
|
||||||
ratio = min(ratio_w, ratio_h)
|
ratio_w = width / w
|
||||||
|
ratio = max(ratio_w, ratio_h)
|
||||||
# Compute new dimensions, ensuring they're at least 1 pixel
|
new_size = (int(ratio * w), int(ratio * h))
|
||||||
new_width = max(1, int(ratio * w))
|
return gpu_resize(image, dsize=new_size)
|
||||||
new_height = max(1, int(ratio * h))
|
|
||||||
new_size = (new_width, new_height)
|
|
||||||
|
|
||||||
return cv2.resize(image, dsize=new_size)
|
|
||||||
|
|
||||||
|
|
||||||
def render_image_preview(image_path: str, size: Tuple[int, int]) -> ctk.CTkImage:
|
def render_image_preview(image_path: str, size: Tuple[int, int]) -> ctk.CTkImage:
|
||||||
@@ -729,7 +940,7 @@ def render_video_preview(
|
|||||||
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
||||||
has_frame, frame = capture.read()
|
has_frame, frame = capture.read()
|
||||||
if has_frame:
|
if has_frame:
|
||||||
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
image = Image.fromarray(gpu_cvt_color(frame, cv2.COLOR_BGR2RGB))
|
||||||
if size:
|
if size:
|
||||||
image = ImageOps.fit(image, size, Image.LANCZOS)
|
image = ImageOps.fit(image, size, Image.LANCZOS)
|
||||||
return ctk.CTkImage(image, size=image.size)
|
return ctk.CTkImage(image, size=image.size)
|
||||||
@@ -767,7 +978,7 @@ def update_preview(frame_number: int = 0) -> None:
|
|||||||
temp_frame = frame_processor.process_frame(
|
temp_frame = frame_processor.process_frame(
|
||||||
get_one_face(cv2.imread(modules.globals.source_path)), temp_frame
|
get_one_face(cv2.imread(modules.globals.source_path)), temp_frame
|
||||||
)
|
)
|
||||||
image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB))
|
image = Image.fromarray(gpu_cvt_color(temp_frame, cv2.COLOR_BGR2RGB))
|
||||||
image = ImageOps.contain(
|
image = ImageOps.contain(
|
||||||
image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
)
|
)
|
||||||
@@ -838,21 +1049,13 @@ def get_available_cameras():
|
|||||||
camera_indices = []
|
camera_indices = []
|
||||||
camera_names = []
|
camera_names = []
|
||||||
|
|
||||||
if platform.system() == "Darwin": # macOS specific handling
|
if platform.system() == "Darwin":
|
||||||
# Try to open the default FaceTime camera first
|
# Do NOT probe cameras with cv2.VideoCapture on macOS — probing
|
||||||
cap = cv2.VideoCapture(0)
|
# invalid indices triggers the OBSENSOR backend and causes SIGSEGV.
|
||||||
if cap.isOpened():
|
# Default to indices 0 and 1 (covers FaceTime + one USB camera).
|
||||||
camera_indices.append(0)
|
# The user can select the correct index from the UI dropdown.
|
||||||
camera_names.append("FaceTime Camera")
|
camera_indices = [0, 1]
|
||||||
cap.release()
|
camera_names = ["Camera 0", "Camera 1"]
|
||||||
|
|
||||||
# On macOS, additional cameras typically use indices 1 and 2
|
|
||||||
for i in [1, 2]:
|
|
||||||
cap = cv2.VideoCapture(i)
|
|
||||||
if cap.isOpened():
|
|
||||||
camera_indices.append(i)
|
|
||||||
camera_names.append(f"Camera {i}")
|
|
||||||
cap.release()
|
|
||||||
else:
|
else:
|
||||||
# Linux camera detection - test first 10 indices
|
# Linux camera detection - test first 10 indices
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
@@ -868,52 +1071,122 @@ def get_available_cameras():
|
|||||||
return camera_indices, camera_names
|
return camera_indices, camera_names
|
||||||
|
|
||||||
|
|
||||||
def create_webcam_preview(camera_index: int):
|
def _capture_thread_func(cap, capture_queue, stop_event):
|
||||||
global preview_label, PREVIEW
|
"""Capture thread: reads frames from camera and puts them into the queue.
|
||||||
|
Drops frames when the queue is full to avoid backpressure on the camera."""
|
||||||
|
while not stop_event.is_set():
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
stop_event.set()
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
capture_queue.put_nowait(frame)
|
||||||
|
except queue.Full:
|
||||||
|
# Drop the oldest frame and enqueue the new one
|
||||||
|
try:
|
||||||
|
capture_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
capture_queue.put_nowait(frame)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
cap = VideoCapturer(camera_index)
|
|
||||||
if not cap.start(PREVIEW_DEFAULT_WIDTH, PREVIEW_DEFAULT_HEIGHT, 60):
|
|
||||||
update_status("Failed to start camera")
|
|
||||||
return
|
|
||||||
|
|
||||||
preview_label.configure(width=PREVIEW_DEFAULT_WIDTH, height=PREVIEW_DEFAULT_HEIGHT)
|
def _detection_thread_func(latest_frame_holder, detection_result, detection_lock, stop_event):
|
||||||
PREVIEW.deiconify()
|
"""Detection thread: continuously runs face detection on the latest
|
||||||
|
captured frame and stores results in detection_result under detection_lock.
|
||||||
|
|
||||||
|
This decouples face detection (~15-30ms) from face swapping (~5-10ms)
|
||||||
|
so the swap loop never blocks on detection, significantly improving
|
||||||
|
live mode FPS."""
|
||||||
|
while not stop_event.is_set():
|
||||||
|
with detection_lock:
|
||||||
|
frame = latest_frame_holder[0]
|
||||||
|
|
||||||
|
if frame is None:
|
||||||
|
time.sleep(0.005)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if modules.globals.many_faces:
|
||||||
|
many = get_many_faces(frame)
|
||||||
|
with detection_lock:
|
||||||
|
detection_result['target_face'] = None
|
||||||
|
detection_result['many_faces'] = many
|
||||||
|
else:
|
||||||
|
face = get_one_face(frame)
|
||||||
|
with detection_lock:
|
||||||
|
detection_result['target_face'] = face
|
||||||
|
detection_result['many_faces'] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _processing_thread_func(capture_queue, processed_queue, stop_event,
|
||||||
|
latest_frame_holder, detection_result, detection_lock):
|
||||||
|
"""Processing thread: takes raw frames from capture_queue, reads the
|
||||||
|
latest detection result from the shared detection_result dict, applies
|
||||||
|
face swap/enhancement, and puts results into processed_queue.
|
||||||
|
|
||||||
|
Face detection runs concurrently in _detection_thread_func — this thread
|
||||||
|
only reads cached results so it never blocks on detection."""
|
||||||
frame_processors = get_frame_processors_modules(modules.globals.frame_processors)
|
frame_processors = get_frame_processors_modules(modules.globals.frame_processors)
|
||||||
source_image = None
|
source_image = None
|
||||||
|
last_source_path = None
|
||||||
prev_time = time.time()
|
prev_time = time.time()
|
||||||
fps_update_interval = 0.5
|
fps_update_interval = 0.5
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
fps = 0
|
fps = 0
|
||||||
|
|
||||||
while True:
|
while not stop_event.is_set():
|
||||||
ret, frame = cap.read()
|
try:
|
||||||
if not ret:
|
frame = capture_queue.get(timeout=0.05)
|
||||||
break
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
temp_frame = frame.copy()
|
temp_frame = frame
|
||||||
|
|
||||||
if modules.globals.live_mirror:
|
if modules.globals.live_mirror:
|
||||||
temp_frame = cv2.flip(temp_frame, 1)
|
temp_frame = gpu_flip(temp_frame, 1)
|
||||||
|
|
||||||
if modules.globals.live_resizable:
|
# Publish the mirrored frame for the detection thread to pick up
|
||||||
temp_frame = fit_image_to_size(
|
with detection_lock:
|
||||||
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
|
latest_frame_holder[0] = temp_frame
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
temp_frame = fit_image_to_size(
|
|
||||||
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not modules.globals.map_faces:
|
if not modules.globals.map_faces:
|
||||||
if source_image is None and modules.globals.source_path:
|
if modules.globals.source_path and modules.globals.source_path != last_source_path:
|
||||||
|
last_source_path = modules.globals.source_path
|
||||||
source_image = get_one_face(cv2.imread(modules.globals.source_path))
|
source_image = get_one_face(cv2.imread(modules.globals.source_path))
|
||||||
|
|
||||||
|
# Read latest detection results (brief lock to avoid blocking detection thread)
|
||||||
|
with detection_lock:
|
||||||
|
cached_target_face = detection_result.get('target_face')
|
||||||
|
cached_many_faces = detection_result.get('many_faces')
|
||||||
|
|
||||||
for frame_processor in frame_processors:
|
for frame_processor in frame_processors:
|
||||||
if frame_processor.NAME == "DLC.FACE-ENHANCER":
|
if frame_processor.NAME == "DLC.FACE-ENHANCER":
|
||||||
if modules.globals.fp_ui["face_enhancer"]:
|
if modules.globals.fp_ui["face_enhancer"]:
|
||||||
temp_frame = frame_processor.process_frame(None, temp_frame)
|
temp_frame = frame_processor.process_frame(None, temp_frame)
|
||||||
|
elif frame_processor.NAME == "DLC.FACE-ENHANCER-GPEN256":
|
||||||
|
if modules.globals.fp_ui.get("face_enhancer_gpen256", False):
|
||||||
|
temp_frame = frame_processor.process_frame(None, temp_frame)
|
||||||
|
elif frame_processor.NAME == "DLC.FACE-ENHANCER-GPEN512":
|
||||||
|
if modules.globals.fp_ui.get("face_enhancer_gpen512", False):
|
||||||
|
temp_frame = frame_processor.process_frame(None, temp_frame)
|
||||||
|
elif frame_processor.NAME == "DLC.FACE-SWAPPER":
|
||||||
|
# Use cached face positions from detection thread
|
||||||
|
swapped_bboxes = []
|
||||||
|
if modules.globals.many_faces and cached_many_faces:
|
||||||
|
result = temp_frame.copy()
|
||||||
|
for t_face in cached_many_faces:
|
||||||
|
result = frame_processor.swap_face(source_image, t_face, result)
|
||||||
|
if hasattr(t_face, 'bbox') and t_face.bbox is not None:
|
||||||
|
swapped_bboxes.append(t_face.bbox.astype(int))
|
||||||
|
temp_frame = result
|
||||||
|
elif cached_target_face is not None:
|
||||||
|
temp_frame = frame_processor.swap_face(source_image, cached_target_face, temp_frame)
|
||||||
|
if hasattr(cached_target_face, 'bbox') and cached_target_face.bbox is not None:
|
||||||
|
swapped_bboxes.append(cached_target_face.bbox.astype(int))
|
||||||
|
# Apply post-processing (sharpening, interpolation)
|
||||||
|
temp_frame = frame_processor.apply_post_processing(temp_frame, swapped_bboxes)
|
||||||
else:
|
else:
|
||||||
temp_frame = frame_processor.process_frame(source_image, temp_frame)
|
temp_frame = frame_processor.process_frame(source_image, temp_frame)
|
||||||
else:
|
else:
|
||||||
@@ -922,6 +1195,10 @@ def create_webcam_preview(camera_index: int):
|
|||||||
if frame_processor.NAME == "DLC.FACE-ENHANCER":
|
if frame_processor.NAME == "DLC.FACE-ENHANCER":
|
||||||
if modules.globals.fp_ui["face_enhancer"]:
|
if modules.globals.fp_ui["face_enhancer"]:
|
||||||
temp_frame = frame_processor.process_frame_v2(temp_frame)
|
temp_frame = frame_processor.process_frame_v2(temp_frame)
|
||||||
|
elif frame_processor.NAME in ("DLC.FACE-ENHANCER-GPEN256", "DLC.FACE-ENHANCER-GPEN512"):
|
||||||
|
fp_key = frame_processor.NAME.split(".")[-1].lower().replace("-", "_")
|
||||||
|
if modules.globals.fp_ui.get(fp_key, False):
|
||||||
|
temp_frame = frame_processor.process_frame_v2(temp_frame)
|
||||||
else:
|
else:
|
||||||
temp_frame = frame_processor.process_frame_v2(temp_frame)
|
temp_frame = frame_processor.process_frame_v2(temp_frame)
|
||||||
|
|
||||||
@@ -944,20 +1221,114 @@ def create_webcam_preview(camera_index: int):
|
|||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
image = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)
|
# Put processed frame into output queue, dropping old frames if full
|
||||||
|
try:
|
||||||
|
processed_queue.put_nowait(temp_frame)
|
||||||
|
except queue.Full:
|
||||||
|
try:
|
||||||
|
processed_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
processed_queue.put_nowait(temp_frame)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_webcam_preview(camera_index: int):
|
||||||
|
global preview_label, PREVIEW
|
||||||
|
|
||||||
|
cap = VideoCapturer(camera_index)
|
||||||
|
if not cap.start(PREVIEW_DEFAULT_WIDTH, PREVIEW_DEFAULT_HEIGHT, 60):
|
||||||
|
update_status("Failed to start camera")
|
||||||
|
return
|
||||||
|
|
||||||
|
preview_label.configure(width=PREVIEW_DEFAULT_WIDTH, height=PREVIEW_DEFAULT_HEIGHT)
|
||||||
|
PREVIEW.deiconify()
|
||||||
|
|
||||||
|
# Queues for decoupling capture from processing and processing from display.
|
||||||
|
# Small maxsize ensures we always work on recent frames and drop stale ones.
|
||||||
|
capture_queue = queue.Queue(maxsize=2)
|
||||||
|
processed_queue = queue.Queue(maxsize=2)
|
||||||
|
stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Shared state for the detection pipeline.
|
||||||
|
# latest_frame_holder[0] is the most recent raw frame for the detection
|
||||||
|
# thread; detection_result holds the last detected faces for the
|
||||||
|
# processing thread to read. Both are guarded by detection_lock.
|
||||||
|
detection_lock = threading.Lock()
|
||||||
|
latest_frame_holder = [None]
|
||||||
|
detection_result = {'target_face': None, 'many_faces': None}
|
||||||
|
|
||||||
|
# Start capture thread
|
||||||
|
cap_thread = threading.Thread(
|
||||||
|
target=_capture_thread_func,
|
||||||
|
args=(cap, capture_queue, stop_event),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
cap_thread.start()
|
||||||
|
|
||||||
|
# Start detection thread — runs face detection asynchronously so the
|
||||||
|
# processing/swap thread never blocks on it
|
||||||
|
det_thread = threading.Thread(
|
||||||
|
target=_detection_thread_func,
|
||||||
|
args=(latest_frame_holder, detection_result, detection_lock, stop_event),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
det_thread.start()
|
||||||
|
|
||||||
|
# Start processing thread
|
||||||
|
proc_thread = threading.Thread(
|
||||||
|
target=_processing_thread_func,
|
||||||
|
args=(capture_queue, processed_queue, stop_event,
|
||||||
|
latest_frame_holder, detection_result, detection_lock),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
proc_thread.start()
|
||||||
|
|
||||||
|
# Cleanup helper called from the display loop when preview closes
|
||||||
|
def _cleanup():
|
||||||
|
stop_event.set()
|
||||||
|
cap_thread.join(timeout=2.0)
|
||||||
|
det_thread.join(timeout=2.0)
|
||||||
|
proc_thread.join(timeout=2.0)
|
||||||
|
cap.release()
|
||||||
|
PREVIEW.withdraw()
|
||||||
|
|
||||||
|
# Non-blocking display loop using ROOT.after() — avoids blocking the
|
||||||
|
# Tk event loop which could cause UI freezes or re-entrancy issues
|
||||||
|
def _display_next_frame():
|
||||||
|
if stop_event.is_set() or PREVIEW.state() == "withdrawn":
|
||||||
|
_cleanup()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_frame = processed_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
ROOT.after(16, _display_next_frame)
|
||||||
|
return
|
||||||
|
|
||||||
|
if modules.globals.live_resizable:
|
||||||
|
temp_frame = fit_image_to_size(
|
||||||
|
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
temp_frame = fit_image_to_size(
|
||||||
|
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
|
||||||
|
)
|
||||||
|
|
||||||
|
image = gpu_cvt_color(temp_frame, cv2.COLOR_BGR2RGB)
|
||||||
image = Image.fromarray(image)
|
image = Image.fromarray(image)
|
||||||
image = ImageOps.contain(
|
image = ImageOps.contain(
|
||||||
image, (temp_frame.shape[1], temp_frame.shape[0]), Image.LANCZOS
|
image, (temp_frame.shape[1], temp_frame.shape[0]), Image.LANCZOS
|
||||||
)
|
)
|
||||||
image = ctk.CTkImage(image, size=image.size)
|
image = ctk.CTkImage(image, size=image.size)
|
||||||
preview_label.configure(image=image)
|
preview_label.configure(image=image)
|
||||||
ROOT.update()
|
|
||||||
|
|
||||||
if PREVIEW.state() == "withdrawn":
|
ROOT.after(16, _display_next_frame)
|
||||||
break
|
|
||||||
|
|
||||||
cap.release()
|
# Kick off the non-blocking display loop
|
||||||
PREVIEW.withdraw()
|
ROOT.after(0, _display_next_frame)
|
||||||
|
|
||||||
|
|
||||||
def create_source_target_popup_for_webcam(
|
def create_source_target_popup_for_webcam(
|
||||||
@@ -1067,7 +1438,7 @@ def refresh_data(map: list):
|
|||||||
|
|
||||||
if "source" in item:
|
if "source" in item:
|
||||||
image = Image.fromarray(
|
image = Image.fromarray(
|
||||||
cv2.cvtColor(item["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
gpu_cvt_color(item["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
||||||
)
|
)
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
@@ -1085,7 +1456,7 @@ def refresh_data(map: list):
|
|||||||
|
|
||||||
if "target" in item:
|
if "target" in item:
|
||||||
image = Image.fromarray(
|
image = Image.fromarray(
|
||||||
cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB)
|
gpu_cvt_color(item["target"]["cv2"], cv2.COLOR_BGR2RGB)
|
||||||
)
|
)
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
@@ -1133,7 +1504,7 @@ def update_webcam_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
image = Image.fromarray(
|
image = Image.fromarray(
|
||||||
cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
gpu_cvt_color(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
|
||||||
)
|
)
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
@@ -1185,7 +1556,7 @@ def update_webcam_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
image = Image.fromarray(
|
image = Image.fromarray(
|
||||||
cv2.cvtColor(map[button_num]["target"]["cv2"], cv2.COLOR_BGR2RGB)
|
gpu_cvt_color(map[button_num]["target"]["cv2"], cv2.COLOR_BGR2RGB)
|
||||||
)
|
)
|
||||||
image = image.resize(
|
image = image.resize(
|
||||||
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
|
||||||
@@ -1203,4 +1574,4 @@ def update_webcam_target(
|
|||||||
target_label_dict_live[button_num] = target_image
|
target_label_dict_live[button_num] = target_image
|
||||||
else:
|
else:
|
||||||
update_pop_live_status("Face could not be detected in last upload!")
|
update_pop_live_status("Face could not be detected in last upload!")
|
||||||
return map
|
return map
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""Lightweight hover tooltip for CustomTkinter widgets."""
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
|
||||||
|
class ToolTip:
|
||||||
|
"""Show a floating tooltip popup when the user hovers over a widget.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ToolTip(my_button, "Helpful description text")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, widget: ctk.CTkBaseClass, text: str, delay: int = 500):
|
||||||
|
self._widget = widget
|
||||||
|
self._text = text
|
||||||
|
self._delay = delay
|
||||||
|
self._tooltip_window = None
|
||||||
|
self._after_id = None
|
||||||
|
|
||||||
|
widget.bind("<Enter>", self._schedule_show, add="+")
|
||||||
|
widget.bind("<Leave>", self._hide, add="+")
|
||||||
|
|
||||||
|
def _schedule_show(self, event=None):
|
||||||
|
self._cancel()
|
||||||
|
self._after_id = self._widget.after(self._delay, self._show)
|
||||||
|
|
||||||
|
def _show(self):
|
||||||
|
if self._tooltip_window is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
x = self._widget.winfo_rootx() + 20
|
||||||
|
y = self._widget.winfo_rooty() + self._widget.winfo_height() + 5
|
||||||
|
|
||||||
|
self._tooltip_window = tw = ctk.CTkToplevel(self._widget)
|
||||||
|
tw.withdraw()
|
||||||
|
tw.overrideredirect(True)
|
||||||
|
|
||||||
|
label = ctk.CTkLabel(
|
||||||
|
tw,
|
||||||
|
text=self._text,
|
||||||
|
fg_color="#333333",
|
||||||
|
text_color="#EEEEEE",
|
||||||
|
corner_radius=6,
|
||||||
|
padx=8,
|
||||||
|
pady=4,
|
||||||
|
)
|
||||||
|
label.pack()
|
||||||
|
|
||||||
|
tw.update_idletasks()
|
||||||
|
|
||||||
|
# Clamp to screen bounds
|
||||||
|
screen_w = tw.winfo_screenwidth()
|
||||||
|
screen_h = tw.winfo_screenheight()
|
||||||
|
tip_w = tw.winfo_reqwidth()
|
||||||
|
tip_h = tw.winfo_reqheight()
|
||||||
|
|
||||||
|
if x + tip_w > screen_w:
|
||||||
|
x = screen_w - tip_w - 5
|
||||||
|
if y + tip_h > screen_h:
|
||||||
|
y = self._widget.winfo_rooty() - tip_h - 5
|
||||||
|
|
||||||
|
tw.geometry(f"+{x}+{y}")
|
||||||
|
tw.deiconify()
|
||||||
|
|
||||||
|
def _hide(self, event=None):
|
||||||
|
self._cancel()
|
||||||
|
if self._tooltip_window is not None:
|
||||||
|
self._tooltip_window.destroy()
|
||||||
|
self._tooltip_window = None
|
||||||
|
|
||||||
|
def _cancel(self):
|
||||||
|
if self._after_id is not None:
|
||||||
|
self._widget.after_cancel(self._after_id)
|
||||||
|
self._after_id = None
|
||||||
+132
-30
@@ -15,19 +15,16 @@ import modules.globals
|
|||||||
TEMP_FILE = "temp.mp4"
|
TEMP_FILE = "temp.mp4"
|
||||||
TEMP_DIRECTORY = "temp"
|
TEMP_DIRECTORY = "temp"
|
||||||
|
|
||||||
# monkey patch ssl for mac
|
|
||||||
if platform.system().lower() == "darwin":
|
|
||||||
ssl._create_default_https_context = ssl._create_unverified_context
|
|
||||||
|
|
||||||
|
|
||||||
def run_ffmpeg(args: List[str]) -> bool:
|
def run_ffmpeg(args: List[str]) -> bool:
|
||||||
|
"""Run ffmpeg with hardware acceleration and optimized settings."""
|
||||||
commands = [
|
commands = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-hwaccel",
|
"-hwaccel", "auto", # Auto-detect hardware acceleration
|
||||||
"auto",
|
"-hwaccel_output_format", "auto", # Use hardware format when possible
|
||||||
"-loglevel",
|
"-threads", str(modules.globals.execution_threads or 0), # 0 = auto-detect optimal thread count
|
||||||
modules.globals.log_level,
|
"-loglevel", modules.globals.log_level,
|
||||||
]
|
]
|
||||||
commands.extend(args)
|
commands.extend(args)
|
||||||
try:
|
try:
|
||||||
@@ -61,39 +58,131 @@ def detect_fps(target_path: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def extract_frames(target_path: str) -> None:
|
def extract_frames(target_path: str) -> None:
|
||||||
|
"""Extract frames with hardware acceleration and optimized settings."""
|
||||||
temp_directory_path = get_temp_directory_path(target_path)
|
temp_directory_path = get_temp_directory_path(target_path)
|
||||||
|
|
||||||
|
# Use hardware-accelerated decoding and optimized pixel format
|
||||||
run_ffmpeg(
|
run_ffmpeg(
|
||||||
[
|
[
|
||||||
"-i",
|
"-i", target_path,
|
||||||
target_path,
|
"-vf", "format=rgb24", # Use video filter for format conversion (faster)
|
||||||
"-pix_fmt",
|
"-vsync", "0", # Prevent frame duplication
|
||||||
"rgb24",
|
"-frame_pts", "1", # Preserve frame timing
|
||||||
os.path.join(temp_directory_path, "%04d.png"),
|
os.path.join(temp_directory_path, "%04d.png"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_video(target_path: str, fps: float = 30.0) -> None:
|
def create_video(target_path: str, fps: float = 30.0) -> None:
|
||||||
|
"""Create video with hardware-accelerated encoding and optimized settings."""
|
||||||
temp_output_path = get_temp_output_path(target_path)
|
temp_output_path = get_temp_output_path(target_path)
|
||||||
temp_directory_path = get_temp_directory_path(target_path)
|
temp_directory_path = get_temp_directory_path(target_path)
|
||||||
run_ffmpeg(
|
|
||||||
[
|
# Determine optimal encoder based on available hardware
|
||||||
"-r",
|
encoder = modules.globals.video_encoder
|
||||||
str(fps),
|
encoder_options = []
|
||||||
"-i",
|
|
||||||
os.path.join(temp_directory_path, "%04d.png"),
|
# GPU-accelerated encoding options
|
||||||
"-c:v",
|
if 'CUDAExecutionProvider' in modules.globals.execution_providers:
|
||||||
modules.globals.video_encoder,
|
# NVIDIA GPU encoding
|
||||||
"-crf",
|
if encoder == 'libx264':
|
||||||
str(modules.globals.video_quality),
|
encoder = 'h264_nvenc'
|
||||||
"-pix_fmt",
|
encoder_options = [
|
||||||
"yuv420p",
|
"-preset", "p7", # Highest quality preset for NVENC
|
||||||
"-vf",
|
"-tune", "hq", # High quality tuning
|
||||||
"colorspace=bt709:iall=bt601-6-625:fast=1",
|
"-rc", "vbr", # Variable bitrate
|
||||||
|
"-cq", str(modules.globals.video_quality), # Quality level
|
||||||
|
"-b:v", "0", # Let CQ control bitrate
|
||||||
|
"-multipass", "fullres", # Two-pass encoding for better quality
|
||||||
|
]
|
||||||
|
elif encoder == 'libx265':
|
||||||
|
encoder = 'hevc_nvenc'
|
||||||
|
encoder_options = [
|
||||||
|
"-preset", "p7",
|
||||||
|
"-tune", "hq",
|
||||||
|
"-rc", "vbr",
|
||||||
|
"-cq", str(modules.globals.video_quality),
|
||||||
|
"-b:v", "0",
|
||||||
|
]
|
||||||
|
elif 'DmlExecutionProvider' in modules.globals.execution_providers:
|
||||||
|
# AMD/Intel GPU encoding (DirectML on Windows)
|
||||||
|
if encoder == 'libx264':
|
||||||
|
# Try AMD AMF encoder
|
||||||
|
encoder = 'h264_amf'
|
||||||
|
encoder_options = [
|
||||||
|
"-quality", "quality", # Quality mode
|
||||||
|
"-rc", "vbr_latency",
|
||||||
|
"-qp_i", str(modules.globals.video_quality),
|
||||||
|
"-qp_p", str(modules.globals.video_quality),
|
||||||
|
]
|
||||||
|
elif encoder == 'libx265':
|
||||||
|
encoder = 'hevc_amf'
|
||||||
|
encoder_options = [
|
||||||
|
"-quality", "quality",
|
||||||
|
"-rc", "vbr_latency",
|
||||||
|
"-qp_i", str(modules.globals.video_quality),
|
||||||
|
"-qp_p", str(modules.globals.video_quality),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# CPU encoding with optimized settings
|
||||||
|
if encoder == 'libx264':
|
||||||
|
encoder_options = [
|
||||||
|
"-preset", "medium", # Balance speed/quality
|
||||||
|
"-crf", str(modules.globals.video_quality),
|
||||||
|
"-tune", "film", # Optimize for film content
|
||||||
|
]
|
||||||
|
elif encoder == 'libx265':
|
||||||
|
encoder_options = [
|
||||||
|
"-preset", "medium",
|
||||||
|
"-crf", str(modules.globals.video_quality),
|
||||||
|
"-x265-params", "log-level=error",
|
||||||
|
]
|
||||||
|
elif encoder == 'libvpx-vp9':
|
||||||
|
encoder_options = [
|
||||||
|
"-crf", str(modules.globals.video_quality),
|
||||||
|
"-b:v", "0", # Constant quality mode
|
||||||
|
"-cpu-used", "2", # Speed vs quality (0-5, lower=slower/better)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Build ffmpeg command
|
||||||
|
ffmpeg_args = [
|
||||||
|
"-r", str(fps),
|
||||||
|
"-i", os.path.join(temp_directory_path, "%04d.png"),
|
||||||
|
"-c:v", encoder,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add encoder-specific options
|
||||||
|
ffmpeg_args.extend(encoder_options)
|
||||||
|
|
||||||
|
# Add common options
|
||||||
|
ffmpeg_args.extend([
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-movflags", "+faststart", # Enable fast start for web playback
|
||||||
|
"-vf", "colorspace=bt709:iall=bt601-6-625:fast=1",
|
||||||
|
"-y",
|
||||||
|
temp_output_path,
|
||||||
|
])
|
||||||
|
|
||||||
|
# Try with hardware encoder first, fallback to software if it fails
|
||||||
|
success = run_ffmpeg(ffmpeg_args)
|
||||||
|
|
||||||
|
if not success and encoder in ['h264_nvenc', 'hevc_nvenc', 'h264_amf', 'hevc_amf']:
|
||||||
|
# Fallback to software encoding
|
||||||
|
print(f"Hardware encoding with {encoder} failed, falling back to software encoding...")
|
||||||
|
fallback_encoder = 'libx264' if 'h264' in encoder else 'libx265'
|
||||||
|
ffmpeg_args_fallback = [
|
||||||
|
"-r", str(fps),
|
||||||
|
"-i", os.path.join(temp_directory_path, "%04d.png"),
|
||||||
|
"-c:v", fallback_encoder,
|
||||||
|
"-preset", "medium",
|
||||||
|
"-crf", str(modules.globals.video_quality),
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-movflags", "+faststart",
|
||||||
|
"-vf", "colorspace=bt709:iall=bt601-6-625:fast=1",
|
||||||
"-y",
|
"-y",
|
||||||
temp_output_path,
|
temp_output_path,
|
||||||
]
|
]
|
||||||
)
|
run_ffmpeg(ffmpeg_args_fallback)
|
||||||
|
|
||||||
|
|
||||||
def restore_audio(target_path: str, output_path: str) -> None:
|
def restore_audio(target_path: str, output_path: str) -> None:
|
||||||
@@ -193,8 +282,15 @@ def conditional_download(download_directory_path: str, urls: List[str]) -> None:
|
|||||||
download_directory_path, os.path.basename(url)
|
download_directory_path, os.path.basename(url)
|
||||||
)
|
)
|
||||||
if not os.path.exists(download_file_path):
|
if not os.path.exists(download_file_path):
|
||||||
request = urllib.request.urlopen(url) # type: ignore[attr-defined]
|
request = urllib.request.Request(url)
|
||||||
total = int(request.headers.get("Content-Length", 0))
|
|
||||||
|
# Create a specific SSL context for macOS to avoid globally disabling verification
|
||||||
|
ctx = None
|
||||||
|
if platform.system().lower() == "darwin":
|
||||||
|
ctx = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
response = urllib.request.urlopen(request, context=ctx)
|
||||||
|
total = int(response.headers.get("Content-Length", 0))
|
||||||
with tqdm(
|
with tqdm(
|
||||||
total=total,
|
total=total,
|
||||||
desc="Downloading",
|
desc="Downloading",
|
||||||
@@ -202,7 +298,13 @@ def conditional_download(download_directory_path: str, urls: List[str]) -> None:
|
|||||||
unit_scale=True,
|
unit_scale=True,
|
||||||
unit_divisor=1024,
|
unit_divisor=1024,
|
||||||
) as progress:
|
) as progress:
|
||||||
urllib.request.urlretrieve(url, download_file_path, reporthook=lambda count, block_size, total_size: progress.update(block_size)) # type: ignore[attr-defined]
|
with open(download_file_path, "wb") as f:
|
||||||
|
while True:
|
||||||
|
buffer = response.read(8192)
|
||||||
|
if not buffer:
|
||||||
|
break
|
||||||
|
f.write(buffer)
|
||||||
|
progress.update(len(buffer))
|
||||||
|
|
||||||
|
|
||||||
def resolve_relative_path(path: str) -> str:
|
def resolve_relative_path(path: str) -> str:
|
||||||
|
|||||||
+5
-14
@@ -1,25 +1,16 @@
|
|||||||
--extra-index-url https://download.pytorch.org/whl/cu118
|
|
||||||
|
|
||||||
numpy>=1.23.5,<2
|
numpy>=1.23.5,<2
|
||||||
typing-extensions>=4.8.0
|
typing-extensions>=4.8.0
|
||||||
opencv-python==4.10.0.84
|
opencv-python==4.10.0.84
|
||||||
cv2_enumerate_cameras==1.1.15
|
cv2_enumerate_cameras==1.1.15
|
||||||
onnx==1.16.0
|
onnx==1.18.0
|
||||||
insightface==0.7.3
|
insightface==0.7.3
|
||||||
psutil==5.9.8
|
psutil==5.9.8
|
||||||
tk==0.1.0
|
tk==0.1.0
|
||||||
customtkinter==5.2.2
|
customtkinter==5.2.2
|
||||||
pillow==11.1.0
|
pillow==12.1.1
|
||||||
torch==2.5.1+cu118; sys_platform != 'darwin'
|
|
||||||
torch==2.5.1; sys_platform == 'darwin'
|
|
||||||
torchvision==0.20.1; sys_platform != 'darwin'
|
|
||||||
torchvision==0.20.1; sys_platform == 'darwin'
|
|
||||||
onnxruntime-silicon==1.16.3; sys_platform == 'darwin' and platform_machine == 'arm64'
|
onnxruntime-silicon==1.16.3; sys_platform == 'darwin' and platform_machine == 'arm64'
|
||||||
onnxruntime-gpu==1.16.3; sys_platform != 'darwin'
|
onnxruntime-gpu==1.23.2; sys_platform != 'darwin'
|
||||||
tensorflow; sys_platform != 'darwin'
|
tensorflow; sys_platform != 'darwin'
|
||||||
opennsfw2==0.10.2
|
opennsfw2==0.10.2
|
||||||
protobuf==4.23.2
|
protobuf==4.25.1
|
||||||
tqdm==4.66.4
|
pygrabber
|
||||||
gfpgan==1.3.8
|
|
||||||
tkinterdnd2==0.4.2
|
|
||||||
pygrabber==0.2
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Import the tkinter fix to patch the ScreenChanged error
|
||||||
|
import tkinter_fix
|
||||||
|
|
||||||
from modules import core
|
from modules import core
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
os.environ.setdefault('TK_SILENCE_DEPRECATION', '1')
|
||||||
|
|
||||||
|
import tkinter
|
||||||
|
|
||||||
|
# Only needs to be imported once at the beginning of the application
|
||||||
|
def apply_patch():
|
||||||
|
# Create a monkey patch for the internal _tkinter module
|
||||||
|
original_init = tkinter.Tk.__init__
|
||||||
|
|
||||||
|
def patched_init(self, *args, **kwargs):
|
||||||
|
# Call the original init
|
||||||
|
original_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
# Define the missing ::tk::ScreenChanged procedure
|
||||||
|
self.tk.eval("""
|
||||||
|
if {[info commands ::tk::ScreenChanged] == ""} {
|
||||||
|
proc ::tk::ScreenChanged {args} {
|
||||||
|
# Do nothing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Apply the monkey patch
|
||||||
|
tkinter.Tk.__init__ = patched_init
|
||||||
|
|
||||||
|
# Apply the patch automatically when this module is imported
|
||||||
|
apply_patch()
|
||||||
Reference in New Issue
Block a user