Compare commits

..
2 Commits
Author SHA1 Message Date
KRSHH 30ffbc1042 add Drag and drop functionality 2025-03-26 13:08:18 +05:30
KRSHH 2b2caf6aba Update ui.py 2025-03-26 12:14:29 +05:30
50 changed files with 1881 additions and 7774 deletions
-16
View File
@@ -1,16 +0,0 @@
name: ruff
on:
pull_request:
push:
branches: [main]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v4.0.0
with:
version: "0.15.7"
args: "check --output-format=github"
-4
View File
@@ -25,7 +25,3 @@ models/DMDNet.pth
faceswap/ faceswap/
.vscode/ .vscode/
switch_states.json switch_states.json
/models
install.bat
/.claude
*.bat
+58 -115
View File
@@ -1,4 +1,4 @@
<h1 align="center">Deep-Live-Cam 2.1.6</h1> <h1 align="center">Deep-Live-Cam</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,45 +30,14 @@ 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.
## Pre-built Deep-Live-Cam 2.7 Ultimate!
<p align="center"> ## Quick Start - Pre-built (Windows / Nvidia)
<a href="https://deeplivecam.net/index.php/quickstart">
<img src="https://github.com/user-attachments/assets/fa2cdf79-c933-4b93-844a-b087192261ed" width="100%" alt="Lite / Ultimate Download Banner">
</a>
</p>
<p align="center"> <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" />
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=1">
<img src="https://github.com/user-attachments/assets/56b61811-3a1e-4672-9b50-cf7f6e8e6852" width="40" alt="Windows">
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=2">
<img src="https://github.com/user-attachments/assets/6538e3a6-c957-431a-b586-2d6abcf534dc" width="34" alt="Mac Silicon">
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=3">
<img src="https://github.com/user-attachments/assets/ad45142e-426c-4364-a2a9-a512670cc62c" width="40" alt="CPU">
</a>
</p>
<p align="center"> ##### This is the fastest build you can get if you have a discrete NVIDIA GPU.
<strong>Windows • Mac Silicon • CPU • NVIDIA • AMD</strong>
</p>
<p align="center"> ###### 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.
Builds optimized for your hardware.
</p>
<p align="center">
<a href="https://deeplivecam.net/index.php/quickstart">
<img src="media/Download.png" width="280" alt="Download">
</a>
</p>
> **Ultimate** includes **30+ exclusive features**, performance optimizations, and **priority support** We only have a single official website which is https://deeplivecam.net . Please be careful on where you download other versions of this application aside from that website and this github repo.
Perfect if you want the fastest setup with **zero manual installation**, pre-configured dependencies, and optimized builds for every supported platform.
## TLDR; Live Deepfake in just 3 Clicks ## TLDR; Live Deepfake in just 3 Clicks
![easysteps](https://github.com/user-attachments/assets/af825228-852c-411b-b787-ffd9aac72fc6) ![easysteps](https://github.com/user-attachments/assets/af825228-852c-411b-b787-ffd9aac72fc6)
@@ -130,7 +99,7 @@ Perfect if you want the fastest setup with **zero manual installation**, pre-con
## Installation (Manual) ## Installation (Manual)
**Please be aware that the installation requires technical skills and is not for beginners. Consider downloading the quickstart version.** **Please be aware that the installation requires technical skills and is not for beginners. Consider downloading the prebuilt version.**
<details> <details>
<summary>Click to see the process</summary> <summary>Click to see the process</summary>
@@ -141,7 +110,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.14 recommended; 3.11-3.14 supported) - Python (3.10 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)```
@@ -150,13 +119,13 @@ This is more likely to work on your computer but will be slower as it utilizes t
**2. Clone the Repository** **2. Clone the Repository**
```bash ```bash
git clone --depth 1 https://github.com/hacksider/Deep-Live-Cam.git git clone https://github.com/hacksider/Deep-Live-Cam.git
cd Deep-Live-Cam cd Deep-Live-Cam
``` ```
**3. Download the Models** **3. Download the Models**
1. [gfpgan-1024.onnx](https://huggingface.co/hacksider/deep-live-cam/resolve/main/gfpgan-1024.onnx) 1. [GFPGANv1.4](https://huggingface.co/hacksider/deep-live-cam/resolve/main/GFPGANv1.4.pth)
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.
@@ -165,34 +134,26 @@ 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.14
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
**For macOS:** **For macOS:**
Apple Silicon (M1 through M5) requires specific setup: Apple Silicon (M1/M2/M3) requires specific setup:
```bash ```bash
# Install Python 3.14 # Install Python 3.10 (specific version is important)
brew install python@3.14 brew install python@3.10
# Install tkinter package (required for the GUI) # Install tkinter package (required for the GUI)
brew install python-tk@3.14 brew install python-tk@3.10
# Create and activate virtual environment with Python 3.14 # Create and activate virtual environment with Python 3.10
python3.14 -m venv venv python3.10 -m venv venv
source venv/bin/activate source venv/bin/activate
# Install dependencies # Install dependencies
@@ -211,11 +172,6 @@ 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).
@@ -224,16 +180,12 @@ pip install git+https://github.com/TencentARC/GFPGAN.git@master
**CUDA Execution Provider (Nvidia)** **CUDA Execution Provider (Nvidia)**
1. Install [CUDA Toolkit 12.8.0](https://developer.nvidia.com/cuda-12-8-0-download-archive) 1. Install [CUDA Toolkit 11.8.0](https://developer.nvidia.com/cuda-11-8-0-download-archive)
2. Install [cuDNN v8.9.7 for CUDA 12.x](https://developer.nvidia.com/rdp/cudnn-archive) (required for onnxruntime-gpu): 2. Install dependencies:
- 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.26.0 pip install onnxruntime-gpu==1.16.3
``` ```
3. Usage: 3. Usage:
@@ -244,29 +196,26 @@ python run.py --execution-provider cuda
**CoreML Execution Provider (Apple Silicon)** **CoreML Execution Provider (Apple Silicon)**
Apple Silicon (M1 through M5) specific installation: Apple Silicon (M1/M2/M3) specific installation:
1. Make sure you've completed the macOS setup above using Python 3.14. 1. Make sure you've completed the macOS setup above using Python 3.10.
2. No extra install step is needed — `requirements.txt` pulls the official 2. Install dependencies:
`onnxruntime` build, whose macOS wheels ship the CoreML execution provider.
If you previously installed the unmaintained `onnxruntime-silicon` fork,
remove it first, as it shadows the real package:
```bash ```bash
pip uninstall onnxruntime-silicon pip uninstall onnxruntime onnxruntime-silicon
pip install -r requirements.txt pip install onnxruntime-silicon==1.13.1
``` ```
3. Usage: 3. Usage (important: specify Python 3.10):
```bash ```bash
python3.14 run.py --execution-provider coreml python3.10 run.py --execution-provider coreml
``` ```
**Important Notes for macOS:** **Important Notes for macOS:**
- Python 3.11 is the minimum (onnxruntime dropped 3.10); 3.14 is recommended - You **must** use Python 3.10, not newer versions like 3.11 or 3.13
- Always run with `python3.14` command not just `python` if you have multiple Python versions installed - Always run with `python3.10` command not just `python` if you have multiple Python versions installed
- If you get error about `_tkinter` missing, reinstall the tkinter package: `brew reinstall python-tk@3.14` - If you get error about `_tkinter` missing, reinstall the tkinter package: `brew reinstall python-tk@3.10`
- If you get model loading errors, check that your models are in the correct folder - If you get model loading errors, check that your models are in the correct folder
- If you encounter conflicts with other Python versions, consider uninstalling them: - If you encounter conflicts with other Python versions, consider uninstalling them:
```bash ```bash
@@ -274,9 +223,9 @@ python3.14 run.py --execution-provider coreml
brew list | grep python brew list | grep python
# Uninstall conflicting versions if needed # Uninstall conflicting versions if needed
brew uninstall --ignore-dependencies python@3.11 brew uninstall --ignore-dependencies python@3.11 python@3.13
# Keep only Python 3.14 # Keep only Python 3.10
brew cleanup brew cleanup
``` ```
@@ -286,7 +235,7 @@ python3.14 run.py --execution-provider coreml
```bash ```bash
pip uninstall onnxruntime onnxruntime-coreml pip uninstall onnxruntime onnxruntime-coreml
pip install onnxruntime-coreml==1.21.0 pip install onnxruntime-coreml==1.13.1
``` ```
2. Usage: 2. Usage:
@@ -301,7 +250,7 @@ python run.py --execution-provider coreml
```bash ```bash
pip uninstall onnxruntime onnxruntime-directml pip uninstall onnxruntime onnxruntime-directml
pip install onnxruntime-directml==1.21.0 pip install onnxruntime-directml==1.15.1
``` ```
2. Usage: 2. Usage:
@@ -316,25 +265,9 @@ python run.py --execution-provider directml
```bash ```bash
pip uninstall onnxruntime onnxruntime-openvino pip uninstall onnxruntime onnxruntime-openvino
pip install onnxruntime-openvino==1.21.0 pip install onnxruntime-openvino==1.15.0
``` ```
**Note:** `onnxruntime-openvino` newer than 1.21.0 must be installed together with `openvino`, and the two versions must correspond one-to-one. The supported pairings are:
| onnxruntime-openvino | OpenVINO |
| --- | --- |
| 1.24.1 | 2025.4.1 |
| 1.23.0 | 2025.3 |
| 1.22.0 | 2025.1 |
```bash
# Example: onnxruntime-openvino 1.24.1 pairs with OpenVINO 2025.4.1
pip install openvino==2025.4.1
pip install onnxruntime-openvino==1.24.1
```
See the [OpenVINO Execution Provider requirements](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html#requirements) for the full version-mapping details.
2. Usage: 2. Usage:
```bash ```bash
@@ -360,8 +293,18 @@ 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.
## Download all models in this huggingface link ## Tips and Tricks
- [**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)
@@ -392,22 +335,24 @@ Looking for a CLI mode? Using the -s/--source argument will make the run program
## Press ## Press
- [**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"* **We are always open to criticism and are ready to improve, that's why we didn't cherry-pick anything.**
- [**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
@@ -415,8 +360,6 @@ 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!
- [maxwbuckley](https://github.com/maxwbuckley): For making the effort to optimize this for mac!
- 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 ❤️
-181
View File
@@ -1,181 +0,0 @@
"""Standalone pipeline benchmark — no UI required.
Captures 200 frames from the webcam and runs the full face swap pipeline,
printing per-stage timing and effective FPS.
"""
import os, sys, time, cv2, numpy as np, queue, threading
# PATH fix for cuDNN (Windows only)
if sys.platform == "win32":
_sp = os.path.join(sys.prefix, "Lib", "site-packages")
_torch_lib = os.path.join(_sp, "torch", "lib")
if os.path.isdir(_torch_lib):
os.environ["PATH"] = _torch_lib + os.pathsep + os.environ["PATH"]
import insightface
from insightface.app import FaceAnalysis
from modules.processors.frame.face_swapper import _fast_paste_back
from modules import platform_info
platform_info.print_banner()
# Pick providers based on what's actually available on this machine.
if platform_info.HAS_CUDA_PROVIDER:
_providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
elif platform_info.HAS_COREML_PROVIDER:
_providers = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
else:
_providers = ["CPUExecutionProvider"]
# --- Init models (same as the app) ---
print(f"Loading models with providers={_providers}...")
fa = FaceAnalysis(
name="buffalo_l",
providers=_providers,
allowed_modules=["detection", "recognition", "landmark_2d_106"],
)
fa.prepare(ctx_id=0, det_size=(640, 640))
swap_model = insightface.model_zoo.get_model(
"models/inswapper_128.onnx",
providers=_providers,
)
face_size = swap_model.input_size[0]
aimg_dummy = np.empty((face_size, face_size, 3), dtype=np.uint8)
# --- Camera setup ---
# Windows: DirectShow explicit for MJPEG 1080p60 support.
# macOS/Linux: default backend (AVFoundation / V4L2).
print("Opening camera at 1080p60 MJPEG...")
if sys.platform == "win32":
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
else:
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
cap.set(cv2.CAP_PROP_FPS, 60)
time.sleep(0.5)
# Warmup + get source face
for _ in range(15):
cap.read()
ret, src_frame = cap.read()
faces = fa.get(src_frame)
if not faces:
print("ERROR: No face detected in warmup frame")
cap.release()
sys.exit(1)
source_face = faces[0]
print(f"Source face acquired. Frame: {src_frame.shape}")
# --- Capture thread (same as app) ---
capture_queue = queue.Queue(maxsize=2)
stop_event = threading.Event()
def capture_thread():
while not stop_event.is_set():
ret, frame = cap.read()
if not ret:
break
try:
capture_queue.put_nowait(frame)
except queue.Full:
try:
capture_queue.get_nowait()
except queue.Empty:
pass
try:
capture_queue.put_nowait(frame)
except queue.Full:
pass
cap_t = threading.Thread(target=capture_thread, daemon=True)
cap_t.start()
# --- Warmup processing ---
print("Warming up pipeline...")
for _ in range(20):
try:
frame = capture_queue.get(timeout=0.1)
except queue.Empty:
continue
f = frame.copy()
det_faces = fa.get(f)
if det_faces:
tgt = min(det_faces, key=lambda x: x.bbox[0])
bgr_fake, M = swap_model.get(f, tgt, source_face, paste_back=False)
_fast_paste_back(f, bgr_fake, aimg_dummy, M)
# --- Benchmark ---
N = 200
print(f"\nBenchmarking {N} frames...")
t_queue, t_det, t_onnx, t_paste, t_copy, t_cvt, t_total = [], [], [], [], [], [], []
det_count = 0
cached_face = None
for i in range(N):
tt = time.perf_counter()
t0 = time.perf_counter()
try:
frame = capture_queue.get(timeout=0.1)
except queue.Empty:
continue
t_queue.append((time.perf_counter() - t0) * 1000)
# Detection every 3rd frame — det-only (no landmark/recognition)
det_count += 1
if det_count % 3 == 0:
t0 = time.perf_counter()
from insightface.app.common import Face as _Face
bboxes, kpss = fa.det_model.detect(frame, max_num=0, metric='default')
if bboxes.shape[0] > 0:
idx = int(bboxes[:, 0].argmin())
cached_face = _Face(bbox=bboxes[idx, :4], kps=kpss[idx], det_score=bboxes[idx, 4])
t_det.append((time.perf_counter() - t0) * 1000)
if cached_face is not None:
# No frame.copy() — _fast_paste_back writes in-place, we own the frame
t0 = time.perf_counter()
bgr_fake, M = swap_model.get(frame, cached_face, source_face, paste_back=False)
t_onnx.append((time.perf_counter() - t0) * 1000)
t0 = time.perf_counter()
result = _fast_paste_back(frame, bgr_fake, aimg_dummy, M)
t_paste.append((time.perf_counter() - t0) * 1000)
# Display prep — resize then flip (no cvtColor needed)
t0 = time.perf_counter()
small = cv2.resize(result, (640, 360))
_ = small[:, :, ::-1] # BGR→RGB zero-copy
t_cvt.append((time.perf_counter() - t0) * 1000)
t_total.append((time.perf_counter() - tt) * 1000)
stop_event.set()
cap.release()
# --- Results ---
def s(name, arr):
if not arr:
return
avg = sum(arr) / len(arr)
print(f" {name:25s}: avg={avg:6.1f}ms min={min(arr):5.1f}ms max={max(arr):6.1f}ms n={len(arr)}")
print(f"\n{'='*55}")
print(f" 1080p Pipeline Benchmark ({len(t_total)} frames)")
print(f"{'='*55}")
s("queue.get (wait for cam)", t_queue)
s("detection (fa.get)", t_det)
s("frame.copy()", t_copy)
s("ONNX swap", t_onnx)
s("_fast_paste_back", t_paste)
s("cvtColor BGR->RGB", t_cvt)
s("TOTAL per frame", t_total)
avg_total = sum(t_total) / len(t_total)
avg_queue = sum(t_queue) / len(t_queue)
print(f"\n Effective FPS: {1000/avg_total:.1f}")
print(f" FPS (excl. cam wait): {1000/(avg_total - avg_queue):.1f}")
print(f"{'='*55}")
-46
View File
@@ -1,46 +0,0 @@
{
"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."
}
-46
View File
@@ -1,46 +0,0 @@
{
"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."
}
-46
View File
@@ -1,46 +0,0 @@
{
"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."
}
-45
View File
@@ -1,45 +0,0 @@
{
"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."
}
-45
View File
@@ -1,45 +0,0 @@
{
"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 បានបើករួចហើយ។"
}
-45
View File
@@ -1,45 +0,0 @@
{
"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 타겟 매퍼가 이미 열려 있습니다."
}
-46
View File
@@ -1,46 +0,0 @@
{
"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."
}
-45
View File
@@ -1,45 +0,0 @@
{
"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.": "Сопоставитель Источник-Цель уже открыт."
}
-45
View File
@@ -1,45 +0,0 @@
{
"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 ปลายทาง เปิดอยู่แล้ว"
}
+4 -4
View File
@@ -1,11 +1,11 @@
{ {
"Source x Target Mapper": "Source x Target Mapper", "Source x Target Mapper": "Source x Target Mapper",
"select a source image": "选择一个源图像", "select an source image": "选择一个源图像",
"Preview": "预览", "Preview": "预览",
"select a target image or video": "选择一个目标图像或视频", "select an target image or video": "选择一个目标图像或视频",
"save image output file": "保存图像输出文件", "save image output file": "保存图像输出文件",
"save video output file": "保存视频输出文件", "save video output file": "保存视频输出文件",
"select a target image": "选择一个目标图像", "select an target image": "选择一个目标图像",
"source": "源", "source": "源",
"Select a target": "选择一个目标", "Select a target": "选择一个目标",
"Select a face": "选择一张脸", "Select a face": "选择一张脸",
@@ -36,7 +36,7 @@
"Select source image": "请选取源图像", "Select source image": "请选取源图像",
"Select target image": "请选取目标图像", "Select target image": "请选取目标图像",
"Please provide mapping!": "请提供映射", "Please provide mapping!": "请提供映射",
"At least 1 source with target is required!": "至少需要一个来源图像与目标图像相关!", "Atleast 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:": "选择摄像头",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
just put the models in this folder - just put the models in this folder -
https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx?download=true https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx?download=true
https://huggingface.co/hacksider/deep-live-cam/resolve/main/gfpgan-1024.onnx?download=true https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth
-38
View File
@@ -1,38 +0,0 @@
import os
import cv2
import numpy as np
# Utility function to support unicode characters in file paths for reading.
# OpenCV's cv2.imread() encodes the path with the locale ANSI code page on
# Windows, so it silently returns None for paths containing non-ASCII
# characters (Chinese, Japanese, Cyrillic, accents, ...). Reading the bytes
# through NumPy (which uses Python's unicode-aware file I/O) and decoding them
# in memory sidesteps that limitation. Returns None on failure, matching
# cv2.imread() so it stays a drop-in replacement.
def imread_unicode(path, flags=cv2.IMREAD_COLOR):
try:
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
return None
return cv2.imdecode(data, flags)
except Exception:
return None
# Utility function to support unicode characters in file paths for writing.
# cv2.imwrite() has the same ANSI-path limitation, so we encode the image in
# memory and write the bytes out with NumPy's unicode-aware file I/O. Returns
# True/False like cv2.imwrite() so it stays a drop-in replacement.
def imwrite_unicode(path, img, params=None):
try:
root, ext = os.path.splitext(path)
if not ext:
ext = ".png"
result, encoded_img = cv2.imencode(ext, img, params if params is not None else [])
if not result:
return False
encoded_img.tofile(path)
return True
except Exception:
return False
+3 -9
View File
@@ -1,7 +1,6 @@
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:
@@ -14,18 +13,13 @@ def get_video_frame(video_path: str, frame_number: int = 0) -> Any:
if modules.globals.color_correction: if modules.globals.color_correction:
capture.set(cv2.CAP_PROP_CONVERT_RGB, 1) capture.set(cv2.CAP_PROP_CONVERT_RGB, 1)
frame_total = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) frame_total = capture.get(cv2.CAP_PROP_FRAME_COUNT)
if frame_total <= 0: capture.set(cv2.CAP_PROP_POS_FRAMES, min(frame_total, frame_number - 1))
capture.release()
return None
target_index = 0 if frame_number <= 1 else min(frame_total - 1, frame_number - 1)
capture.set(cv2.CAP_PROP_POS_FRAMES, target_index)
has_frame, frame = capture.read() has_frame, frame = capture.read()
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 = gpu_cvt_color(frame, cv2.COLOR_BGR2RGB) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
capture.release() capture.release()
return frame if has_frame else None return frame if has_frame else None
+1 -12
View File
@@ -1,21 +1,10 @@
import numpy as np import numpy as np
from sklearn.cluster import KMeans from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from typing import Any from typing import Any
def find_cluster_centroids(embeddings, max_k=10) -> Any: def find_cluster_centroids(embeddings, max_k=10) -> Any:
n_samples = len(embeddings)
if n_samples == 0:
raise ValueError("embeddings must not be empty")
if max_k < 1:
raise ValueError("max_k must be at least 1")
max_k = min(max_k, n_samples)
if max_k == 1:
kmeans = KMeans(n_clusters=1, random_state=0)
kmeans.fit(embeddings)
return kmeans.cluster_centers_
inertia = [] inertia = []
cluster_centroids = [] cluster_centroids = []
K = range(1, max_k+1) K = range(1, max_k+1)
+31 -127
View File
@@ -2,7 +2,7 @@ import os
import sys import sys
# single thread doubles cuda performance - needs to be set before torch import # single thread doubles cuda performance - needs to be set before torch import
if any(arg.startswith('--execution-provider') for arg in sys.argv): if any(arg.startswith('--execution-provider') for arg in sys.argv):
os.environ['OMP_NUM_THREADS'] = '6' os.environ['OMP_NUM_THREADS'] = '1'
# reduce tensorflow log level # reduce tensorflow log level
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import warnings import warnings
@@ -11,30 +11,21 @@ import platform
import signal import signal
import shutil import shutil
import argparse import argparse
try: import torch
import torch
HAS_TORCH = True
except ImportError:
HAS_TORCH = False
import onnxruntime import onnxruntime
try: import tensorflow
import tensorflow
HAS_TENSORFLOW = True
except ImportError:
HAS_TENSORFLOW = False
import modules.globals import modules.globals
import modules.metadata import modules.metadata
import modules.ui as ui import modules.ui as ui
from modules.processors.frame.core import get_frame_processors_modules, process_video_in_memory 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 HAS_TORCH and 'ROCMExecutionProvider' in modules.globals.execution_providers: if 'ROCMExecutionProvider' in modules.globals.execution_providers:
del torch del torch
warnings.filterwarnings('ignore', category=FutureWarning, module='insightface') warnings.filterwarnings('ignore', category=FutureWarning, module='insightface')
if HAS_TORCH: warnings.filterwarnings('ignore', category=UserWarning, module='torchvision')
warnings.filterwarnings('ignore', category=UserWarning, module='torchvision')
def parse_args() -> None: def parse_args() -> None:
@@ -43,7 +34,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', 'face_enhancer_gpen256', 'face_enhancer_gpen512'], nargs='+') 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('--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)
@@ -57,8 +48,8 @@ def parse_args() -> None:
program.add_argument('--live-mirror', help='The live camera display as you see it in the front-facing camera frame', dest='live_mirror', action='store_true', default=False) program.add_argument('--live-mirror', help='The live camera display as you see it in the front-facing camera frame', dest='live_mirror', action='store_true', default=False)
program.add_argument('--live-resizable', help='The live camera frame is resizable', dest='live_resizable', action='store_true', default=False) program.add_argument('--live-resizable', help='The live camera frame is resizable', dest='live_resizable', action='store_true', default=False)
program.add_argument('--max-memory', help='maximum amount of RAM in GB', dest='max_memory', type=int, default=suggest_max_memory()) program.add_argument('--max-memory', help='maximum amount of RAM in GB', dest='max_memory', type=int, default=suggest_max_memory())
program.add_argument('--execution-provider', help='execution provider', dest='execution_provider', default=[suggest_default_execution_provider()], choices=suggest_execution_providers(), nargs='+') program.add_argument('--execution-provider', help='execution provider', dest='execution_provider', default=['cpu'], choices=suggest_execution_providers(), nargs='+')
program.add_argument('--execution-threads', help='number of execution threads', dest='execution_threads', type=int, default=None) program.add_argument('--execution-threads', help='number of execution threads', dest='execution_threads', type=int, default=suggest_execution_threads())
program.add_argument('-v', '--version', action='version', version=f'{modules.metadata.name} {modules.metadata.version}') program.add_argument('-v', '--version', action='version', version=f'{modules.metadata.name} {modules.metadata.version}')
# register deprecated args # register deprecated args
@@ -90,15 +81,11 @@ 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
# The argparse default (None) avoids evaluating suggest_execution_threads() #for ENHANCER tumbler:
# before providers are decoded, and deprecated-arg overrides above may if 'face_enhancer' in args.frame_processor:
# have already set execution_threads. modules.globals.fp_ui['face_enhancer'] = True
if modules.globals.execution_threads is None: else:
modules.globals.execution_threads = suggest_execution_threads() modules.globals.fp_ui['face_enhancer'] = False
#for ENHANCER tumblers:
for enhancer_key in ('face_enhancer', 'face_enhancer_gpen256', 'face_enhancer_gpen512'):
modules.globals.fp_ui[enhancer_key] = enhancer_key in args.frame_processor
# translate deprecated args # translate deprecated args
if args.source_path_deprecated: if args.source_path_deprecated:
@@ -137,52 +124,28 @@ def suggest_max_memory() -> int:
return 16 return 16
def suggest_default_execution_provider() -> str:
"""Pick the best available provider: cuda > rocm > coreml > openvino > dml > cpu."""
available = encode_execution_providers(onnxruntime.get_available_providers())
for pref in ('cuda', 'rocm', 'coreml', 'openvino', 'dml'):
if pref in available:
return pref
return 'cpu'
def suggest_execution_providers() -> List[str]: def suggest_execution_providers() -> List[str]:
return encode_execution_providers(onnxruntime.get_available_providers()) return encode_execution_providers(onnxruntime.get_available_providers())
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
if 'CUDAExecutionProvider' in modules.globals.execution_providers: return 8
return 2
if 'OpenVINOExecutionProvider' in modules.globals.execution_providers:
return 1
# 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:
# prevent tensorflow memory leak # prevent tensorflow memory leak
if HAS_TENSORFLOW:
gpus = tensorflow.config.experimental.list_physical_devices('GPU') gpus = tensorflow.config.experimental.list_physical_devices('GPU')
for gpu in gpus: for gpu in gpus:
tensorflow.config.experimental.set_memory_growth(gpu, True) tensorflow.config.experimental.set_memory_growth(gpu, True)
# limit memory usage # limit memory usage
if modules.globals.max_memory: if modules.globals.max_memory:
# setrlimit(RLIMIT_DATA) fails with EINVAL on macOS, crashing on launch.
# See https://github.com/hacksider/Deep-Live-Cam/issues/1848
if platform.system().lower() == 'darwin':
return
memory = modules.globals.max_memory * 1024 ** 3 memory = modules.globals.max_memory * 1024 ** 3
if platform.system().lower() == 'darwin':
memory = modules.globals.max_memory * 1024 ** 6
if platform.system().lower() == 'windows': if platform.system().lower() == 'windows':
import ctypes import ctypes
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
@@ -193,7 +156,7 @@ def limit_resources() -> None:
def release_resources() -> None: def release_resources() -> None:
if 'CUDAExecutionProvider' in modules.globals.execution_providers and HAS_TORCH: if 'CUDAExecutionProvider' in modules.globals.execution_providers:
torch.cuda.empty_cache() torch.cuda.empty_cache()
@@ -213,16 +176,10 @@ 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):
@@ -236,80 +193,34 @@ 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):
elapsed = time.time() - start_time update_status('Processing to image succeed!')
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
# Detect FPS early (needed by both pipelines)
if modules.globals.keep_fps:
update_status('Detecting fps...')
fps = detect_fps(modules.globals.target_path)
else:
fps = 30.0
video_created = False
# --- In-memory pipeline (non-map_faces only) ---
# Reads frames from FFmpeg pipe, processes in memory, encodes directly.
# Eliminates all per-frame PNG disk I/O for a major speed-up.
if not modules.globals.map_faces: if not modules.globals.map_faces:
update_status(f'Processing video in-memory at {fps} fps...') update_status('Creating temp resources...')
create_temp(modules.globals.target_path)
processing_start = time.time()
video_created = process_video_in_memory(
modules.globals.source_path,
modules.globals.target_path,
fps,
)
processing_time = time.time() - processing_start
release_resources()
if video_created:
update_status(f'In-memory processing + encoding completed in {processing_time:.2f}s')
# --- Disk-based fallback (required for map_faces, or if pipe failed) ---
if not video_created:
if not modules.globals.map_faces:
update_status('Falling back to disk-based processing...')
extraction_start = time.time()
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
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 # handles fps
fps_processing = total_frames / processing_time if processing_time > 0 else 0 if modules.globals.keep_fps:
update_status(f'Frame processing completed in {processing_time:.2f}s ({fps_processing:.2f} fps)') update_status('Detecting fps...')
fps = detect_fps(modules.globals.target_path)
encoding_start = time.time()
update_status(f'Creating video with {fps} fps...') update_status(f'Creating video with {fps} fps...')
video_created = create_video(modules.globals.target_path, fps) create_video(modules.globals.target_path, fps)
encoding_time = time.time() - encoding_start else:
if video_created: update_status('Creating video with 30.0 fps...')
update_status(f'Video encoding completed in {encoding_time:.2f}s') create_video(modules.globals.target_path)
if not video_created:
update_status('Video encoding failed. No temporary output video was created.')
clean_temp(modules.globals.target_path)
return
# handle audio # handle audio
if modules.globals.keep_audio: if modules.globals.keep_audio:
if modules.globals.keep_fps: if modules.globals.keep_fps:
@@ -319,13 +230,10 @@ 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)
if is_video(modules.globals.target_path):
total_time = time.time() - start_time update_status('Processing to video succeed!')
if is_video(modules.globals.target_path) and modules.globals.output_path and os.path.isfile(modules.globals.output_path):
update_status(f'Video processing succeeded! Total time: {total_time:.2f}s')
else: else:
update_status('Processing to video failed!') update_status('Processing to video failed!')
@@ -333,8 +241,7 @@ def start() -> None:
def destroy(to_quit=True) -> None: def destroy(to_quit=True) -> None:
if modules.globals.target_path: if modules.globals.target_path:
clean_temp(modules.globals.target_path) clean_temp(modules.globals.target_path)
if to_quit: if to_quit: quit()
quit()
def run() -> None: def run() -> None:
@@ -344,9 +251,6 @@ def run() -> None:
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_check(): if not frame_processor.pre_check():
return return
# Pre-load face analyser in main thread before GUI starts
#from modules.face_analyser import get_face_analyser
#get_face_analyser()
limit_resources() limit_resources()
if modules.globals.headless: if modules.globals.headless:
start() start()
-7
View File
@@ -1,7 +0,0 @@
from typing import Any
from insightface.app.common import Face
import numpy
Face = Face
Frame = numpy.ndarray[Any, Any]
+13 -201
View File
@@ -2,10 +2,10 @@ import os
import shutil import shutil
from typing import Any from typing import Any
import insightface import insightface
import threading
import cv2
import numpy as np
import modules.globals import modules.globals
from modules import imread_unicode, imwrite_unicode
from tqdm import tqdm from tqdm import tqdm
from modules.typing import Frame from modules.typing import Frame
from modules.cluster_analysis import find_cluster_centroids, find_closest_centroid from modules.cluster_analysis import find_cluster_centroids, find_closest_centroid
@@ -13,212 +13,31 @@ 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()
DET_SIZE = (640, 640)
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:
with FACE_ANALYSER_LOCK: FACE_ANALYSER = insightface.app.FaceAnalysis(name='buffalo_l', providers=modules.globals.execution_providers)
# Double-check after acquiring lock FACE_ANALYSER.prepare(ctx_id=0, det_size=(640, 640))
if FACE_ANALYSER is None:
from modules.processors.frame._onnx_enhancer import (
build_provider_config,
)
from modules.model_downloader import ensure_insightface_pack
ensure_insightface_pack('buffalo_l')
providers = build_provider_config()
FACE_ANALYSER = insightface.app.FaceAnalysis(
name='buffalo_l',
providers=providers,
allowed_modules=['detection', 'recognition', 'landmark_2d_106']
)
FACE_ANALYSER.prepare(ctx_id=0, det_size=DET_SIZE)
_optimize_det_model(FACE_ANALYSER, providers)
return FACE_ANALYSER return FACE_ANALYSER
def _optimize_det_model(fa: Any, providers) -> None: def get_one_face(frame: Frame) -> Any:
"""Replace the detection model's ONNX session with a CoreML-optimized one. face = get_face_analyser().get(frame)
Folds dynamic Shape→Gather chains into constants (the input size is
fixed at det_size), eliminating CPU↔ANE partition boundaries in the
RetinaFace FPN upsampling path. 21ms → 4ms on M3 Max.
"""
from modules.onnx_optimize import optimize_for_coreml, IS_APPLE_SILICON
if not IS_APPLE_SILICON:
return
det_model = fa.det_model
model_path = getattr(det_model, 'model_file', None)
if model_path is None or not os.path.exists(model_path):
return
input_shape = (1, 3, DET_SIZE[1], DET_SIZE[0])
optimized_path = optimize_for_coreml(model_path, input_shape=input_shape)
if optimized_path == model_path:
return
import onnxruntime
session_options = onnxruntime.SessionOptions()
session_options.graph_optimization_level = (
onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
)
# Route detection to GPU shader cores (CPUAndGPU) instead of ANE.
# This lets detection run concurrently with the swap model on the
# ANE, overlapping the two inference calls. Detection is fast
# enough on GPU (~4ms) and this frees ANE for the heavier swap.
det_providers = []
for p in providers:
name = p[0] if isinstance(p, tuple) else p
if name == "CoreMLExecutionProvider":
det_providers.append((
"CoreMLExecutionProvider",
{"ModelFormat": "MLProgram", "MLComputeUnits": "CPUAndGPU"},
))
else:
det_providers.append(p)
det_model.session = onnxruntime.InferenceSession(
optimized_path, sess_options=session_options, providers=det_providers,
)
def _needs_landmark() -> bool:
"""Check whether any active feature requires 106-point landmarks.
Landmarks are needed by face enhancers and mouth masking, but not
by the face swapper alone.
"""
if getattr(modules.globals, "mouth_mask", False):
return True
processors = getattr(modules.globals, "frame_processors", [])
return any(p in processors for p in
("face_enhancer", "face_enhancer_gpen256", "face_enhancer_gpen512"))
def _is_dml() -> bool:
return any("DmlExecutionProvider" in p for p in modules.globals.execution_providers)
def _analyse_faces(frame: Frame) -> list:
"""Run face detection, then recognition (and optionally landmark).
Replaces InsightFace's ``FaceAnalysis.get()`` to skip the
landmark_2d_106 model when only face_swapper is active (saves ~1ms
per face and avoids an unnecessary ONNX session call).
"""
fa = get_face_analyser()
bboxes, kpss = fa.det_model.detect(frame, max_num=0, metric="default")
if bboxes.shape[0] == 0:
return []
need_landmark = _needs_landmark()
rec_model = fa.models.get("recognition")
lmk_model = fa.models.get("landmark_2d_106") if need_landmark else None
from insightface.app.common import Face
faces = []
for i in range(bboxes.shape[0]):
face = Face(bbox=bboxes[i, 0:4],
kps=kpss[i] if kpss is not None else None,
det_score=bboxes[i, 4])
if rec_model is not None:
rec_model.get(frame, face)
if lmk_model is not None:
lmk_model.get(frame, face)
faces.append(face)
return faces
def get_one_face(frame: Frame, faces: Any = None) -> Any:
if faces is None:
if _is_dml():
with modules.globals.dml_lock:
faces = _analyse_faces(frame)
else:
faces = _analyse_faces(frame)
try: try:
return min(faces, key=lambda x: x.bbox[0]) return min(face, key=lambda x: x.bbox[0])
except ValueError: except ValueError:
return None return None
def get_many_faces(frame: Frame) -> Any: def get_many_faces(frame: Frame) -> Any:
try: try:
if _is_dml(): return get_face_analyser().get(frame)
with modules.globals.dml_lock:
return _analyse_faces(frame)
else:
return _analyse_faces(frame)
except IndexError: except IndexError:
return None return None
def detect_one_face_fast(frame: Frame) -> Any:
"""Detection-only — skips landmark and recognition models.
Returns a Face with bbox, kps, det_score (enough for face swap).
~10ms vs ~16ms for full get_one_face() at 1080p.
"""
from insightface.app.common import Face
fa = get_face_analyser()
bboxes, kpss = fa.det_model.detect(frame, max_num=0, metric='default')
if bboxes.shape[0] == 0:
return None
idx = int(bboxes[:, 0].argmin())
return Face(bbox=bboxes[idx, :4], kps=kpss[idx], det_score=bboxes[idx, 4])
def detect_many_faces_fast(frame: Frame) -> Any:
"""Detection-only multi-face — skips landmark and recognition."""
from insightface.app.common import Face
fa = get_face_analyser()
bboxes, kpss = fa.det_model.detect(frame, max_num=0, metric='default')
if bboxes.shape[0] == 0:
return None
return [Face(bbox=bboxes[i, :4], kps=kpss[i], det_score=bboxes[i, 4])
for i in range(bboxes.shape[0])]
def ensure_landmarks(frame: Frame, faces: Any) -> None:
"""Run the 2d106 landmark model in-place on faces that lack it.
The fast webcam path (detect_one_face_fast / detect_many_faces_fast)
produces detection-only Face objects with no ``landmark_2d_106``.
Mouth masking needs those landmarks, so add them on demand only when
the feature is active — keeping the fast path fast otherwise.
"""
if faces is None:
return
if not isinstance(faces, (list, tuple)):
faces = [faces]
fa = get_face_analyser()
lmk_model = fa.models.get("landmark_2d_106")
if lmk_model is None:
return
for face in faces:
if face is None:
continue
# insightface Face is a dict; missing keys raise AttributeError,
# so getattr(..., None) is the safe presence check.
if getattr(face, "landmark_2d_106", None) is None:
try:
lmk_model.get(frame, face)
except Exception as e: # pragma: no cover - never break the swap
print(f"Error computing 2d106 landmarks: {e}")
def has_valid_map() -> bool: def has_valid_map() -> bool:
for map in modules.globals.source_target_map: for map in modules.globals.source_target_map:
if "source" in map and "target" in map: if "source" in map and "target" in map:
@@ -257,10 +76,8 @@ def add_blank_map() -> Any:
def get_unique_faces_from_target_image() -> Any: def get_unique_faces_from_target_image() -> Any:
try: try:
modules.globals.source_target_map = [] modules.globals.source_target_map = []
target_frame = imread_unicode(modules.globals.target_path) target_frame = cv2.imread(modules.globals.target_path)
many_faces = get_many_faces(target_frame) many_faces = get_many_faces(target_frame)
if many_faces is None:
return None
i = 0 i = 0
for face in many_faces: for face in many_faces:
@@ -293,10 +110,8 @@ def get_unique_faces_from_target_video() -> Any:
i = 0 i = 0
for temp_frame_path in tqdm(temp_frame_paths, desc="Extracting face embeddings from frames"): for temp_frame_path in tqdm(temp_frame_paths, desc="Extracting face embeddings from frames"):
temp_frame = imread_unicode(temp_frame_path) temp_frame = cv2.imread(temp_frame_path)
many_faces = get_many_faces(temp_frame) many_faces = get_many_faces(temp_frame)
if many_faces is None:
continue
for face in many_faces: for face in many_faces:
face_embeddings.append(face.normed_embedding) face_embeddings.append(face.normed_embedding)
@@ -338,9 +153,6 @@ def default_target_face():
best_frame = frame best_frame = frame
break break
if best_face is None:
continue # No faces detected in this cluster — skip
for frame in map['target_faces_in_frame']: for frame in map['target_faces_in_frame']:
for face in frame['faces']: for face in frame['faces']:
if face['det_score'] > best_face['det_score']: if face['det_score'] > best_face['det_score']:
@@ -349,7 +161,7 @@ def default_target_face():
x_min, y_min, x_max, y_max = best_face['bbox'] x_min, y_min, x_max, y_max = best_face['bbox']
target_frame = imread_unicode(best_frame['location']) target_frame = cv2.imread(best_frame['location'])
map['target'] = { map['target'] = {
'cv2' : target_frame[int(y_min):int(y_max), int(x_min):int(x_max)], 'cv2' : target_frame[int(y_min):int(y_max), int(x_min):int(x_max)],
'face' : best_face 'face' : best_face
@@ -365,7 +177,7 @@ def dump_faces(centroids: Any, frame_face_embeddings: list):
Path(temp_directory_path + f"/{i}").mkdir(parents=True, exist_ok=True) Path(temp_directory_path + f"/{i}").mkdir(parents=True, exist_ok=True)
for frame in tqdm(frame_face_embeddings, desc=f"Copying faces to temp/./{i}"): for frame in tqdm(frame_face_embeddings, desc=f"Copying faces to temp/./{i}"):
temp_frame = imread_unicode(frame['location']) temp_frame = cv2.imread(frame['location'])
j = 0 j = 0
for face in frame['faces']: for face in frame['faces']:
@@ -373,5 +185,5 @@ def dump_faces(centroids: Any, frame_face_embeddings: list):
x_min, y_min, x_max, y_max = face['bbox'] x_min, y_min, x_max, y_max = face['bbox']
if temp_frame[int(y_min):int(y_max), int(x_min):int(x_max)].size > 0: if temp_frame[int(y_min):int(y_max), int(x_min):int(x_max)].size > 0:
imwrite_unicode(temp_directory_path + f"/{i}/{frame['frame']}_{j}.png", temp_frame[int(y_min):int(y_max), int(x_min):int(x_max)]) cv2.imwrite(temp_directory_path + f"/{i}/{frame['frame']}_{j}.png", temp_frame[int(y_min):int(y_max), int(x_min):int(x_max)])
j += 1 j += 1
+34 -70
View File
@@ -1,79 +1,43 @@
# --- START OF FILE globals.py ---
import os import os
from typing import List, Dict, Any from typing import List, Dict, Any
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow")
# Canonical media extensions, defined once so the file dialogs and file_types = [
# has_image_extension never drift. GIF is intentionally excluded: OpenCV's ("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp")),
# cv2.imread/imwrite (the only image I/O this app uses) cannot decode or ("Video", ("*.mp4", "*.mkv")),
# encode GIF on 4.10 or 4.11, so offering it would silently fail. WEBP works ]
# via the libwebp bundled with opencv-python.
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
VIDEO_EXTENSIONS = (".mp4", ".mkv")
# Face Mapping Data source_target_map = []
source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing simple_map = {}
simple_map: Dict[str, Any] = {} # Stores simplified map (embeddings/faces) for live/simple mode
# Paths source_path = None
source_path: str | None = None target_path = None
target_path: str | None = None output_path = None
output_path: str | None = None
# Processing Options
frame_processors: List[str] = [] frame_processors: List[str] = []
keep_fps: bool = True keep_fps = True
keep_audio: bool = True keep_audio = True
keep_frames: bool = False keep_frames = False
many_faces: bool = False # Process all detected faces with default source many_faces = False
map_faces: bool = False # Use source_target_map or simple_map for specific swaps map_faces = False
poisson_blend: bool = False # Enable Poisson Blending for smoother face swaps color_correction = False # New global variable for color correction toggle
color_correction: bool = False # Enable color correction (implementation specific) nsfw_filter = False
nsfw_filter: bool = False video_encoder = None
video_quality = None
# Video Output Options live_mirror = False
video_encoder: str | None = None live_resizable = True
video_quality: int | None = None # Typically a CRF value or bitrate max_memory = None
execution_providers: List[str] = []
# Live Mode Options execution_threads = None
live_mirror: bool = False headless = None
live_resizable: bool = True log_level = "error"
camera_input_combobox: Any | None = None # Placeholder for UI element if needed fp_ui: Dict[str, bool] = {"face_enhancer": False}
webcam_preview_running: bool = False camera_input_combobox = None
show_fps: bool = False webcam_preview_running = False
show_fps = False
# System Configuration mouth_mask = False
max_memory: int | None = None # Memory limit in GB? (Needs clarification) show_mouth_mask_box = False
execution_providers: List[str] = [] # e.g., ['CUDAExecutionProvider', 'CPUExecutionProvider'] mask_feather_ratio = 8
execution_threads: int | None = None # Number of threads for CPU execution mask_down_size = 0.50
headless: bool | None = None # Run without UI? mask_size = 1
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 ---
import threading
dml_lock = threading.Lock()
-285
View File
@@ -1,285 +0,0 @@
# --- 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 os
import cv2
import numpy as np
from typing import Tuple
# ---------------------------------------------------------------------------
# CUDA availability detection (evaluated once at import time)
# ---------------------------------------------------------------------------
CUDA_AVAILABLE: bool = False
# OpenCV CUDA per-operation acceleration is DISABLED by default.
# Each gpu_* call uploads to GPU, processes, then downloads back to CPU.
# At webcam resolution (~960x540) this upload/download overhead far exceeds
# the time saved on the actual operation, making it slower than pure CPU.
# The heavy lifting (face detection, swap, enhancement) runs on GPU via
# ONNX Runtime's CUDAExecutionProvider, which is where GPU matters.
#
# To force-enable, set OPENCV_CUDA_PROCESSING=1 in your environment.
if os.environ.get("OPENCV_CUDA_PROCESSING") == "1":
try:
_test_mat = cv2.cuda.GpuMat()
_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 processing enabled via OPENCV_CUDA_PROCESSING=1.")
except Exception:
pass
# ---------------------------------------------------------------------------
# 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 ---
+1 -1
View File
@@ -1,3 +1,3 @@
name = 'Deep-Live-Cam' name = 'Deep-Live-Cam'
version = '2.1.5' version = '1.8'
edition = 'GitHub Edition' edition = 'GitHub Edition'
-178
View File
@@ -1,178 +0,0 @@
import os
import platform
import ssl
import threading
import urllib.error
import urllib.request
from typing import Dict, List, Optional
from tqdm import tqdm
from modules.paths import MODELS_DIR
HF_REPO_ID = "hacksider/deep-live-cam"
HF_RESOLVE_BASE = f"https://huggingface.co/{HF_REPO_ID}/resolve/main/"
MODEL_SIZES: Dict[str, int] = {
"inswapper_128.onnx": 554253681,
"inswapper_128_fp16.onnx": 277680638,
"gfpgan-1024.onnx": 365875079,
"GPEN-BFR-256.onnx": 75715262,
"GPEN-BFR-512.onnx": 284244491,
"buffalo_l/buffalo_l/1k3d68.onnx": 143607619,
"buffalo_l/buffalo_l/2d106det.onnx": 5030888,
"buffalo_l/buffalo_l/det_10g.onnx": 16923827,
"buffalo_l/buffalo_l/genderage.onnx": 1322532,
"buffalo_l/buffalo_l/w600k_r50.onnx": 174383860,
}
_LOCKS: Dict[str, threading.Lock] = {}
_LOCKS_GUARD = threading.Lock()
CHUNK_SIZE = 1024 * 256
def _ssl_context():
if platform.system().lower() == "darwin":
return ssl._create_unverified_context()
return None
def _lock_for(key: str) -> threading.Lock:
with _LOCKS_GUARD:
if key not in _LOCKS:
_LOCKS[key] = threading.Lock()
return _LOCKS[key]
def resolve_url(name: str) -> str:
return HF_RESOLVE_BASE + name.replace(os.sep, "/")
def local_path(name: str, dest_dir: Optional[str] = None) -> str:
if dest_dir is not None:
return os.path.join(dest_dir, os.path.basename(name))
return os.path.join(MODELS_DIR, *name.replace("/", os.sep).split(os.sep))
def expected_size(name: str) -> Optional[int]:
return MODEL_SIZES.get(name.replace(os.sep, "/"))
def is_present(name: str, dest_dir: Optional[str] = None) -> bool:
path = local_path(name, dest_dir)
return os.path.isfile(path) and os.path.getsize(path) > 0
def _download(name: str, url: str, target: str, size: Optional[int]) -> bool:
os.makedirs(os.path.dirname(target) or MODELS_DIR, exist_ok=True)
partial = target + ".part"
resume_from = os.path.getsize(partial) if os.path.isfile(partial) else 0
headers = {"User-Agent": "Deep-Live-Cam"}
if resume_from:
headers["Range"] = f"bytes={resume_from}-"
try:
request = urllib.request.Request(url, headers=headers)
response = urllib.request.urlopen(request, context=_ssl_context(), timeout=60)
except urllib.error.HTTPError as error:
if resume_from and error.code in (416, 501):
try:
os.remove(partial)
except OSError:
pass
return _download(name, url, target, size)
print(f"[DLC.MODELS] Failed to download {name}: HTTP {error.code}")
return False
except (urllib.error.URLError, OSError) as error:
print(f"[DLC.MODELS] Failed to download {name}: {error}")
return False
with response:
if resume_from and getattr(response, "status", 200) != 206:
resume_from = 0
remaining = int(response.headers.get("Content-Length", 0) or 0)
total = size or (resume_from + remaining) or None
mode = "ab" if resume_from else "wb"
try:
with open(partial, mode) as handle:
with tqdm(
total=total,
initial=resume_from,
desc=f"Downloading {os.path.basename(name)}",
unit="B",
unit_scale=True,
unit_divisor=1024,
) as progress:
while True:
buffer = response.read(CHUNK_SIZE)
if not buffer:
break
handle.write(buffer)
progress.update(len(buffer))
except (urllib.error.URLError, OSError) as error:
print(f"[DLC.MODELS] Download of {name} interrupted: {error}")
return False
downloaded = os.path.getsize(partial)
if size is not None and downloaded != size:
print(f"[DLC.MODELS] {name} is {downloaded} bytes, expected {size}. Discarding.")
try:
os.remove(partial)
except OSError:
pass
return False
try:
os.replace(partial, target)
except OSError as error:
print(f"[DLC.MODELS] Could not finalise {name}: {error}")
return False
return True
def ensure_model(
name: str, quiet: bool = False, dest_dir: Optional[str] = None
) -> Optional[str]:
name = name.replace(os.sep, "/")
target = local_path(name, dest_dir)
with _lock_for(target):
if is_present(name, dest_dir):
return target
if not quiet:
print(f"[DLC.MODELS] {name} not found in models folder, downloading...")
if _download(name, resolve_url(name), target, expected_size(name)):
return target
return None
def ensure_any(names: List[str]) -> Optional[str]:
for name in names:
if is_present(name):
return local_path(name)
for name in names:
path = ensure_model(name)
if path is not None:
return path
return None
def ensure_insightface_pack(name: str = "buffalo_l") -> bool:
members = [n for n in MODEL_SIZES if n.startswith(f"{name}/")]
if not members:
return False
dest_dir = os.path.join(os.path.expanduser("~"), ".insightface", "models", name)
if all(is_present(member, dest_dir) for member in members):
return True
print(f"[DLC.MODELS] insightface pack '{name}' is missing, downloading...")
ok = True
for member in members:
if ensure_model(member, quiet=True, dest_dir=dest_dir) is None:
ok = False
if not ok:
print(f"[DLC.MODELS] Could not pre-fill '{name}'; insightface will retry.")
return ok
-550
View File
@@ -1,550 +0,0 @@
"""ONNX model optimizations for CoreML execution on Apple Silicon.
Each pass eliminates a different CPU↔ANE round-trip that ORT's CoreML EP
would otherwise introduce:
1. **Shape/Gather constant folding** — Dynamic ``Shape`` → ``Gather`` chains
(e.g. for FPN upsample target sizes in RetinaFace) force ops onto CPU even
when the input dimensions are known at load time. We run ONNX shape
inference with the known input size and replace these chains with constants.
Float32-noise-level differences only (max ~6e-6).
2. **Pad(reflect) decomposition** — CoreML doesn't support ``Pad(mode=reflect)``.
Models using reflect padding (e.g. inswapper_128) get split into many CoreML
subgraphs with CPU fallbacks between each. We rewrite each ``Pad(reflect)``
as equivalent ``Slice`` + ``Concat`` ops that CoreML handles natively.
Bit-for-bit identical output. (Fixed upstream in microsoft/onnxruntime#28073.)
3. **Split → Slice decomposition** — CoreML's EP doesn't support the ONNX
``Split`` op, causing partition boundaries in models with channel-wise
splits (e.g. GFPGAN's SFT modulation). Each 2-way Split becomes two Slices.
4. **Scalar Gather widening** — ORT's CoreML EP rejects ``Gather`` nodes with
rank-0 (scalar) indices. StyleGAN-derived models (GFPGAN) slice per-layer
style codes using exactly this pattern. We widen each scalar index to
``[1]`` and squeeze the added axis on the Gather output.
(Filed upstream as microsoft/onnxruntime#28180.)
All passes are cached on disk with a ``_coreml`` suffix so the rewrite cost
is paid only once per model.
"""
import os
import platform
import numpy as np
IS_APPLE_SILICON = platform.system() == "Darwin" and platform.machine() == "arm64"
def optimize_for_coreml(model_path: str, input_shape: tuple = None) -> str:
"""Return path to a CoreML-optimized ONNX model.
Applies all applicable optimizations and caches the result next to
the original model (with ``_coreml`` suffix).
Args:
model_path: Path to the original ONNX model.
input_shape: Optional fixed input shape (e.g. ``(1, 3, 640, 640)``).
When provided, enables Shape/Gather constant folding.
Returns the optimized path, or the original path if no optimizations
apply or we're not on Apple Silicon.
"""
if not IS_APPLE_SILICON:
return model_path
base, ext = os.path.splitext(model_path)
optimized_path = f"{base}_coreml{ext}"
if os.path.exists(optimized_path):
if os.path.getmtime(optimized_path) >= os.path.getmtime(model_path):
return optimized_path
import onnx
from onnx import numpy_helper
model = onnx.load(model_path)
changed = False
if _fold_shape_gather(model, input_shape):
changed = True
# TODO(ort>=1.26): drop this pass. Fixed upstream by microsoft/onnxruntime#28073.
if _decompose_reflect_pad(model):
changed = True
if _decompose_split(model):
changed = True
# TODO: drop this pass once microsoft/onnxruntime#28180 ships. The CoreML
# Gather op builder rejects rank-0 (scalar) indices; we widen them to [1]
# + Squeeze so StyleGAN-family models (GFPGAN) stay on ANE.
if _rewrite_scalar_gather(model):
changed = True
if not changed:
return model_path
# Preserve insightface's emap convention: the INSwapper class reads
# graph.initializer[-1] as the embedding map. If the original model
# had a (512, 512) matrix as its last initializer, keep it last.
_preserve_emap_position(model, numpy_helper)
onnx.save(model, optimized_path)
return optimized_path
# ---------------------------------------------------------------------------
# Pass 1: Fold Shape → Gather chains into constants
# ---------------------------------------------------------------------------
def _fold_shape_gather(model, input_shape) -> bool:
"""Replace dynamic Shape→Gather chains with constants when input size is known.
Only removes a Shape node when ALL of its consumers are Gather nodes
that are also being folded. This prevents breaking graphs where
a Shape output feeds into other ops as well.
"""
if input_shape is None:
return False
from onnx import numpy_helper, shape_inference
graph = model.graph
# Set fixed input dimensions for shape inference
inp = graph.input[0]
dims = inp.type.tensor_type.shape.dim
for i, size in enumerate(input_shape):
if i < len(dims):
dims[i].dim_value = size
try:
model_inferred = shape_inference.infer_shapes(model)
except Exception:
return False
# Extract inferred shapes
value_shapes = {}
for vi in list(model_inferred.graph.value_info) + list(graph.input) + list(graph.output):
shape_dims = vi.type.tensor_type.shape.dim
shape = []
for d in shape_dims:
if d.dim_value > 0:
shape.append(d.dim_value)
else:
shape.append(None)
value_shapes[vi.name] = shape
inits = {init.name: numpy_helper.to_array(init) for init in graph.initializer}
# Build consumer map: output_name → list of consuming nodes
consumers = {}
for node in graph.node:
for i in node.input:
consumers.setdefault(i, []).append(node)
# Also check graph outputs — an output name consumed by the graph
# output list must not be removed
graph_output_names = {o.name for o in graph.output}
# Find Shape nodes with fully-known output
shape_constants = {}
for node in graph.node:
if node.op_type == "Shape":
inp_shape = value_shapes.get(node.input[0])
if inp_shape and all(isinstance(d, int) for d in inp_shape):
shape_constants[node.output[0]] = np.array(inp_shape, dtype=np.int64)
if not shape_constants:
return False
# Find Gather nodes consuming Shape constants
gather_constants = {}
for node in graph.node:
if node.op_type == "Gather" and node.input[0] in shape_constants:
idx_name = node.input[1]
if idx_name in inits:
idx = int(inits[idx_name])
val = int(shape_constants[node.input[0]][idx])
gather_constants[node.output[0]] = np.array(val, dtype=np.int64)
if not gather_constants:
return False
# Determine which Gather nodes to fold (always safe — we replace
# the output with a constant initializer)
gather_remove_ids = set()
for node in graph.node:
if node.op_type == "Gather" and node.output[0] in gather_constants:
gather_remove_ids.add(id(node))
# Determine which Shape nodes are safe to remove: only if ALL
# consumers of the Shape output are Gather nodes being folded,
# and the output isn't a graph output.
shape_remove_ids = set()
for node in graph.node:
if node.op_type == "Shape" and node.output[0] in shape_constants:
out_name = node.output[0]
if out_name in graph_output_names:
continue
node_consumers = consumers.get(out_name, [])
if all(id(c) in gather_remove_ids for c in node_consumers):
shape_remove_ids.add(id(node))
remove_ids = gather_remove_ids | shape_remove_ids
# Add Gather output constants as initializers
existing = {i.name for i in graph.initializer}
for name, val in gather_constants.items():
if name not in existing:
graph.initializer.append(numpy_helper.from_array(val, name=name))
new_nodes = [n for n in graph.node if id(n) not in remove_ids]
del graph.node[:]
graph.node.extend(new_nodes)
return True
# ---------------------------------------------------------------------------
# Pass 2: Decompose Pad(reflect) → Slice + Concat
#
# TEMPORARY: fixed upstream in microsoft/onnxruntime#28073 (merged 2026-04-20).
# Once the ORT floor is >= 1.26.0, MLProgram handles Pad(mode=reflect) natively
# via MIL tensor_operation.pad and this entire pass can be deleted.
# ---------------------------------------------------------------------------
def _decompose_reflect_pad(model) -> bool:
"""Rewrite Pad(reflect) as Slice+Concat sequences CoreML can handle."""
from onnx import numpy_helper, helper
graph = model.graph
inits = {init.name: numpy_helper.to_array(init) for init in graph.initializer}
reflect_pads = []
for node in graph.node:
if node.op_type == "Pad":
mode = "constant"
for attr in node.attribute:
if attr.name == "mode":
mode = attr.s.decode()
if mode == "reflect" and len(node.input) > 1 and node.input[1] in inits:
reflect_pads.append(node)
if not reflect_pads:
return False
existing_names = {i.name for i in graph.initializer}
def ensure_const(name, value):
if name not in existing_names:
graph.initializer.append(
numpy_helper.from_array(np.array(value, dtype=np.int64), name=name)
)
existing_names.add(name)
ensure_const("_rp_ax2", [2])
ensure_const("_rp_ax3", [3])
max_pad = 0
for node in reflect_pads:
pads = inits[node.input[1]].tolist()
max_pad = max(max_pad, int(pads[2]), int(pads[3]))
for v in range(1, max_pad + 2):
ensure_const(f"_rp_p{v}", [v])
ensure_const(f"_rp_n{v}", [-v])
_counter = [0]
def uid():
_counter[0] += 1
return _counter[0]
pad_ids = {id(n) for n in reflect_pads}
pad_init_names = set()
new_nodes = []
for node in graph.node:
if id(node) not in pad_ids:
new_nodes.append(node)
continue
pads = inits[node.input[1]].tolist()
h_pad, w_pad = int(pads[2]), int(pads[3])
for inp in node.input[1:]:
if inp in inits:
pad_init_names.add(inp)
current = node.input[0]
if h_pad > 0:
top = []
for i in range(h_pad, 0, -1):
name = f"_rp_t{uid()}"
new_nodes.append(helper.make_node(
"Slice",
inputs=[current, f"_rp_p{i}", f"_rp_p{i+1}", "_rp_ax2"],
outputs=[name],
))
top.append(name)
bot = []
for i in range(1, h_pad + 1):
name = f"_rp_b{uid()}"
new_nodes.append(helper.make_node(
"Slice",
inputs=[current, f"_rp_n{i+1}", f"_rp_n{i}", "_rp_ax2"],
outputs=[name],
))
bot.append(name)
h_out = f"_rp_h{uid()}"
new_nodes.append(helper.make_node(
"Concat", inputs=top + [current] + bot, outputs=[h_out], axis=2
))
current = h_out
if w_pad > 0:
left = []
for i in range(w_pad, 0, -1):
name = f"_rp_l{uid()}"
new_nodes.append(helper.make_node(
"Slice",
inputs=[current, f"_rp_p{i}", f"_rp_p{i+1}", "_rp_ax3"],
outputs=[name],
))
left.append(name)
right = []
for i in range(1, w_pad + 1):
name = f"_rp_r{uid()}"
new_nodes.append(helper.make_node(
"Slice",
inputs=[current, f"_rp_n{i+1}", f"_rp_n{i}", "_rp_ax3"],
outputs=[name],
))
right.append(name)
new_nodes.append(helper.make_node(
"Concat",
inputs=left + [current] + right,
outputs=[node.output[0]],
axis=3,
))
elif h_pad > 0:
new_nodes.append(helper.make_node(
"Identity", inputs=[current], outputs=[node.output[0]]
))
# Remove old Pad initializers
clean_inits = [i for i in graph.initializer if i.name not in pad_init_names]
del graph.initializer[:]
graph.initializer.extend(clean_inits)
del graph.node[:]
graph.node.extend(new_nodes)
return True
# ---------------------------------------------------------------------------
# Pass 3: Decompose Split → Slice pairs
# ---------------------------------------------------------------------------
def _decompose_split(model) -> bool:
"""Rewrite Split(axis=1) as Slice pairs that CoreML can handle.
CoreML's EP doesn't support the ONNX ``Split`` op, causing partition
boundaries in models that use channel-wise splits (e.g. GFPGAN's SFT
modulation layers). Each Split with two outputs becomes two Slice ops.
"""
from onnx import numpy_helper, helper
graph = model.graph
splits = []
for node in graph.node:
if node.op_type == "Split":
axis = 0
split_sizes = []
for attr in node.attribute:
if attr.name == "axis":
axis = attr.i
if attr.name == "split":
split_sizes = list(attr.ints)
if axis == 1 and len(split_sizes) == 2 and len(node.output) == 2:
splits.append((node, split_sizes))
if not splits:
return False
existing = {i.name for i in graph.initializer}
def ensure_const(name, value):
if name not in existing:
graph.initializer.append(
numpy_helper.from_array(np.array(value, dtype=np.int64), name=name)
)
existing.add(name)
ensure_const("_sp_ax1", [1])
# Collect all needed boundary constants
for _, (a, b) in splits:
ensure_const("_sp_s0", [0])
ensure_const(f"_sp_s{a}", [a])
ensure_const(f"_sp_s{a + b}", [a + b])
split_ids = {id(node) for node, _ in splits}
replacements = {}
for node, (a, b) in splits:
slice0 = helper.make_node(
"Slice",
inputs=[node.input[0], "_sp_s0", f"_sp_s{a}", "_sp_ax1"],
outputs=[node.output[0]],
)
slice1 = helper.make_node(
"Slice",
inputs=[node.input[0], f"_sp_s{a}", f"_sp_s{a + b}", "_sp_ax1"],
outputs=[node.output[1]],
)
replacements[id(node)] = [slice0, slice1]
new_nodes = []
for node in graph.node:
if id(node) in split_ids:
new_nodes.extend(replacements[id(node)])
else:
new_nodes.append(node)
del graph.node[:]
graph.node.extend(new_nodes)
return True
# ---------------------------------------------------------------------------
# Pass 4: Widen scalar Gather indices to [1] + Squeeze
#
# TEMPORARY: filed upstream as microsoft/onnxruntime#28180. ORT's CoreML EP
# GatherOpBuilder::IsOpSupportedImpl rejects rank-0 (scalar) indices with
# `Gather does not support scalar 'indices'`. The builder's own comment
# describes the workaround (promote to [1], squeeze the added axis) but
# doesn't apply it. We do the same thing at the ONNX level so StyleGAN-
# family models (GFPGAN is the hot example — 16 per-layer style-code
# slices) don't split the CoreML subgraph. Once the upstream fix ships
# and the ORT floor is raised, delete this pass.
# ---------------------------------------------------------------------------
def _rewrite_scalar_gather(model) -> bool:
"""Rewrite Gather(data, scalar_idx) as Gather(data, [scalar_idx]) + Squeeze.
Only touches Gather nodes whose index is a rank-0 int64 constant or
initializer; everything else passes through unchanged. The rewrite
is semantically identical — indices get an added leading axis, the
Squeeze removes it after the gather.
"""
from onnx import numpy_helper, helper, TensorProto
graph = model.graph
# Opset 13 moved Squeeze's axes from attribute to input.
opset = next(
(o.version for o in model.opset_import if o.domain in ("", "ai.onnx")),
11,
)
const_values = {}
for n in graph.node:
if n.op_type == "Constant":
for a in n.attribute:
if a.name == "value":
const_values[n.output[0]] = a.t
init_values = {i.name: i for i in graph.initializer}
def scalar_int64(name):
"""Return int value if `name` resolves to a rank-0 int64 constant, else None."""
tensor = const_values.get(name) or init_values.get(name)
if tensor is None or tensor.data_type != TensorProto.INT64:
return None
arr = numpy_helper.to_array(tensor)
return int(arr) if arr.ndim == 0 else None
rewrote = 0
new_nodes = []
for n in graph.node:
if n.op_type == "Gather":
val = scalar_int64(n.input[1])
if val is not None:
axis = next((a.i for a in n.attribute if a.name == "axis"), 0)
idx_1d_name = f"{n.input[1]}_1d_{rewrote}"
idx_const = helper.make_node(
"Constant",
inputs=[],
outputs=[idx_1d_name],
value=helper.make_tensor(idx_1d_name, TensorProto.INT64, [1], [val]),
)
gather_out = f"{n.output[0]}_pre_squeeze_{rewrote}"
new_gather = helper.make_node(
"Gather",
inputs=[n.input[0], idx_1d_name],
outputs=[gather_out],
name=n.name,
axis=axis,
)
if opset < 13:
squeeze = helper.make_node(
"Squeeze",
inputs=[gather_out],
outputs=[n.output[0]],
name=(n.name or "gather") + "_squeeze",
axes=[axis],
)
new_nodes.extend([idx_const, new_gather, squeeze])
else:
axes_name = f"{idx_1d_name}_sq_axes"
axes_const = helper.make_node(
"Constant",
inputs=[],
outputs=[axes_name],
value=helper.make_tensor(axes_name, TensorProto.INT64, [1], [axis]),
)
squeeze = helper.make_node(
"Squeeze",
inputs=[gather_out, axes_name],
outputs=[n.output[0]],
name=(n.name or "gather") + "_squeeze",
)
new_nodes.extend([idx_const, axes_const, new_gather, squeeze])
rewrote += 1
continue
new_nodes.append(n)
if rewrote == 0:
return False
del graph.node[:]
graph.node.extend(new_nodes)
return True
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _preserve_emap_position(model, numpy_helper):
"""Keep the insightface emap (512×512 matrix) as the last initializer."""
graph = model.graph
emap_init = None
for init in graph.initializer:
if not init.name.startswith("_rp_"):
arr = numpy_helper.to_array(init)
if len(arr.shape) == 2 and arr.shape[0] == 512 and arr.shape[1] == 512:
emap_init = init
break
if emap_init is not None:
inits = [i for i in graph.initializer if i.name != emap_init.name]
del graph.initializer[:]
graph.initializer.extend(inits)
graph.initializer.append(emap_init)
-6
View File
@@ -1,6 +0,0 @@
"""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")
-91
View File
@@ -1,91 +0,0 @@
"""Centralized platform + accelerator detection.
Imported once at startup to expose typed flags the rest of the codebase
can branch on without re-querying `platform`, `torch.cuda`, or
`onnxruntime.get_available_providers()` repeatedly.
The banner printed by :func:`print_banner` is the single user-facing
report of which code path the app will take.
"""
from __future__ import annotations
import platform as _platform
import sys
from typing import List, Tuple
IS_WINDOWS: bool = _platform.system() == "Windows"
IS_MACOS: bool = _platform.system() == "Darwin"
IS_LINUX: bool = _platform.system() == "Linux"
IS_APPLE_SILICON: bool = IS_MACOS and _platform.machine() == "arm64"
def _detect_torch_cuda() -> bool:
try:
import torch # noqa: WPS433 — local import, avoid hard dep at module load
return bool(torch.cuda.is_available())
except Exception:
return False
def _detect_onnx_providers() -> List[str]:
try:
import onnxruntime
return list(onnxruntime.get_available_providers())
except Exception:
return []
HAS_TORCH_CUDA: bool = _detect_torch_cuda()
ONNX_PROVIDERS: List[str] = _detect_onnx_providers()
HAS_CUDA_PROVIDER: bool = "CUDAExecutionProvider" in ONNX_PROVIDERS
HAS_COREML_PROVIDER: bool = "CoreMLExecutionProvider" in ONNX_PROVIDERS
HAS_DML_PROVIDER: bool = "DmlExecutionProvider" in ONNX_PROVIDERS
HAS_OPENVINO_PROVIDER: bool = "OpenVINOExecutionProvider" in ONNX_PROVIDERS
# OpenVINO execution-provider config shared by every ONNX session builder.
# AUTO:GPU,NPU,CPU lets OpenVINO pick the best available device in priority
# order (Intel GPU → NPU → CPU).
OPENVINO_PROVIDER_CONFIG = (
"OpenVINOExecutionProvider",
{"device_type": "AUTO:GPU,NPU,CPU"},
)
def camera_backends() -> List[Tuple[int, int]]:
"""Return an ordered list of ``(device_index, cv2_backend)`` attempts.
Windows prefers MSMF (60fps capable) with DirectShow as fallback.
macOS/Linux use the default backend (AVFoundation / V4L2).
"""
import cv2
if IS_WINDOWS:
return [
(0, cv2.CAP_MSMF),
(0, cv2.CAP_DSHOW),
(0, cv2.CAP_ANY),
]
return [(0, cv2.CAP_ANY)]
def accelerator_label() -> str:
if HAS_CUDA_PROVIDER:
return "CUDA (NVIDIA)"
if IS_APPLE_SILICON and HAS_COREML_PROVIDER:
return "CoreML (Apple Neural Engine)"
if HAS_COREML_PROVIDER:
return "CoreML"
if HAS_OPENVINO_PROVIDER:
return "OpenVINO (Intel)"
if HAS_DML_PROVIDER:
return "DirectML"
return "CPU"
def print_banner() -> None:
"""Print a one-line summary of the platform + accelerator selection."""
os_label = f"{_platform.system()} {_platform.machine()}"
print(
f"[platform] {os_label} | python {sys.version.split()[0]} | "
f"accelerator: {accelerator_label()} | providers: {ONNX_PROVIDERS}",
flush=True,
)
+1 -15
View File
@@ -1,22 +1,8 @@
import importlib.util
import os
import numpy import numpy
# Keras 3 defaults to the TensorFlow backend, which has no Python 3.14 wheels.
# opennsfw2 only runs inference, so any installed backend works; pick one that
# is actually present before opennsfw2 imports keras.
if "KERAS_BACKEND" not in os.environ:
for _backend in ("torch", "tensorflow", "jax"):
if importlib.util.find_spec(_backend) is not None:
os.environ["KERAS_BACKEND"] = _backend
break
import opennsfw2 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
@@ -28,7 +14,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 = gpu_cvt_color(target_frame, cv2.COLOR_BGR2RGB) target_frame = cv2.cvtColor(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)
-244
View File
@@ -1,244 +0,0 @@
"""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
from modules.platform_info import OPENVINO_PROVIDER_CONFIG
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 build_provider_config(providers=None):
"""Wrap raw provider name strings with optimised CUDA / CoreML options.
Providers that are already ``(name, options_dict)`` tuples are passed
through unchanged. Non-CUDA providers are left as bare strings.
"""
if providers is None:
providers = modules.globals.execution_providers
config = []
for p in providers:
if isinstance(p, tuple):
# Already configured pass through
config.append(p)
elif p == "CUDAExecutionProvider":
# Use bare provider — ONNX Runtime's defaults are fastest on
# modern GPUs (Blackwell/sm_120). Custom options like
# EXHAUSTIVE cudnn_conv_algo_search hurt performance on these
# architectures.
config.append(p)
elif p == "CoreMLExecutionProvider" and IS_APPLE_SILICON:
config.append((
"CoreMLExecutionProvider",
{
"ModelFormat": "MLProgram",
"MLComputeUnits": "ALL",
"AllowLowPrecisionAccumulationOnGPU": 1,
},
))
elif p == "OpenVINOExecutionProvider":
# AUTO lets OpenVINO select the best device
config.append(OPENVINO_PROVIDER_CONFIG)
else:
config.append(p)
return config
def run_inference(session: onnxruntime.InferenceSession,
input_name: str,
input_tensor: "np.ndarray") -> "np.ndarray":
"""Run ONNX inference, using IO binding when a CUDA session is active.
IO binding avoids redundant host↔device copies by transferring the
input tensor directly to GPU memory and letting ONNX Runtime allocate
the output on the device. Falls back to the standard ``session.run``
path for non-CUDA providers or if binding fails.
"""
if "CUDAExecutionProvider" in session.get_providers():
try:
io_binding = session.io_binding()
# Input: numpy → GPU
ort_input = onnxruntime.OrtValue.ortvalue_from_numpy(
input_tensor, "cuda", 0,
)
io_binding.bind_ortvalue_input(input_name, ort_input)
# Output: allocate on GPU (avoids a CPU-side allocation)
output_name = session.get_outputs()[0].name
io_binding.bind_output(output_name, "cuda", 0)
session.run_with_iobinding(io_binding)
return io_binding.get_outputs()[0].numpy()
except Exception:
# Fall back to standard path (e.g. ORT version mismatch,
# unsupported op, or VRAM pressure)
pass
return session.run(None, {input_name: input_tensor})[0]
def create_onnx_session(model_path: str) -> onnxruntime.InferenceSession:
"""Create an ONNX Runtime session with optimised provider config.
On Apple Silicon, applies CoreML graph optimizations (Pad decomposition,
Shape/Gather folding, Split decomposition) to reduce CPU↔ANE partition
boundaries.
"""
if IS_APPLE_SILICON:
from modules.onnx_optimize import optimize_for_coreml
# Infer input shape from the model for Shape/Gather folding
try:
import onnx
m = onnx.load(model_path)
inp = m.graph.input[0]
dims = inp.type.tensor_type.shape.dim
shape = tuple(d.dim_value for d in dims if d.dim_value > 0)
input_shape = shape if len(shape) == 4 else None
except Exception:
input_shape = None
model_path = optimize_for_coreml(model_path, input_shape=input_shape)
providers = build_provider_config()
session_options = onnxruntime.SessionOptions()
session_options.graph_optimization_level = (
onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
)
session = onnxruntime.InferenceSession(
model_path, sess_options=session_options, 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:
input_name = session.get_inputs()[0].name
output = run_inference(session, input_name, blob)
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)
+8 -342
View File
@@ -1,17 +1,12 @@
import os
import subprocess
import sys import sys
import importlib import importlib
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from types import ModuleType from types import ModuleType
from typing import Any, List, Callable from typing import Any, List, Callable
import numpy as np
from tqdm import tqdm from tqdm import tqdm
import modules import modules
import modules.globals import modules.globals
from modules.face_analyser import get_one_face
FRAME_PROCESSORS_MODULES: List[ModuleType] = [] FRAME_PROCESSORS_MODULES: List[ModuleType] = []
FRAME_PROCESSORS_INTERFACE = [ FRAME_PROCESSORS_INTERFACE = [
@@ -22,22 +17,12 @@ 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:
if not hasattr(frame_processor_module, method_name): if not hasattr(frame_processor_module, method_name):
print(f"Frame processor {frame_processor} is missing required method {method_name}")
sys.exit() sys.exit()
except ImportError: except ImportError:
print(f"Frame processor {frame_processor} not found") print(f"Frame processor {frame_processor} not found")
@@ -57,54 +42,27 @@ 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 and frame_processor not in current_processor_names: if state == True and frame_processor not in frame_processors:
try:
frame_processor_module = load_frame_processor_module(frame_processor) frame_processor_module = load_frame_processor_module(frame_processor)
FRAME_PROCESSORS_MODULES.append(frame_processor_module) FRAME_PROCESSORS_MODULES.append(frame_processor_module)
if frame_processor not in modules.globals.frame_processors:
modules.globals.frame_processors.append(frame_processor) modules.globals.frame_processors.append(frame_processor)
except SystemExit: if state == False:
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 not state and frame_processor in current_processor_names:
try: try:
module_to_remove = next((mod for mod in FRAME_PROCESSORS_MODULES if mod.__name__.endswith(f'.{frame_processor}')), None) frame_processor_module = load_frame_processor_module(frame_processor)
if module_to_remove: FRAME_PROCESSORS_MODULES.remove(frame_processor_module)
FRAME_PROCESSORS_MODULES.remove(module_to_remove)
if frame_processor in modules.globals.frame_processors:
modules.globals.frame_processors.remove(frame_processor) modules.globals.frame_processors.remove(frame_processor)
except Exception as e: except:
print(f"Warning: Error removing frame processor {frame_processor}: {e}") pass
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:
"""Process frames in parallel with optimized batching and memory management.""" with ThreadPoolExecutor(max_workers=modules.globals.execution_threads) as executor:
max_workers = modules.globals.execution_threads
# Determine optimal batch size based on available memory and thread count
# Process frames in batches to avoid memory overflow
batch_size = max(1, min(32, len(temp_frame_paths) // max(1, max_workers)))
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 = [] futures = []
for path in temp_frame_paths:
for path in batch:
future = executor.submit(process_frames, source_path, [path], progress) future = executor.submit(process_frames, source_path, [path], progress)
futures.append(future) futures.append(future)
# Wait for batch to complete before starting next batch
for future in futures: for future in futures:
try:
future.result() 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:
@@ -113,295 +71,3 @@ def process_video(source_path: str, frame_paths: list[str], process_frames: Call
with tqdm(total=total, desc='Processing', unit='frame', dynamic_ncols=True, bar_format=progress_bar_format) as progress: with tqdm(total=total, desc='Processing', unit='frame', dynamic_ncols=True, bar_format=progress_bar_format) as progress:
progress.set_postfix({'execution_providers': modules.globals.execution_providers, 'execution_threads': modules.globals.execution_threads, 'max_memory': modules.globals.max_memory}) progress.set_postfix({'execution_providers': modules.globals.execution_providers, 'execution_threads': modules.globals.execution_threads, 'max_memory': modules.globals.max_memory})
multi_process_frame(source_path, frame_paths, process_frames, progress) multi_process_frame(source_path, frame_paths, process_frames, progress)
def process_video_in_memory(source_path: str, target_path: str, fps: float) -> bool:
"""Process video frames in-memory using FFmpeg pipes, eliminating disk I/O.
Reads raw frames from the source video via an FFmpeg decoder pipe, runs each
frame through all active frame processors sequentially, and writes the
result directly to an FFmpeg encoder pipe. This avoids extracting frames to
PNG on disk, which is the biggest I/O bottleneck in the disk-based pipeline.
Returns True on success, False on failure (caller should fall back to the
disk-based pipeline).
"""
from modules import imread_unicode
from modules.face_analyser import get_one_face
from modules.utilities import (
get_video_dimensions,
estimate_frame_count,
get_temp_output_path,
)
temp_output_path = get_temp_output_path(target_path)
# --- Pre-load source face (needed by face_swapper in simple mode) ---
source_face = None
if source_path and os.path.exists(source_path):
source_img = imread_unicode(source_path)
if source_img is not None:
source_face = get_one_face(source_img)
del source_img
if source_face is None:
print("[DLC.CORE] Warning: No face detected in source image. "
"Face swapping will be skipped.")
# --- Collect frame processors & reset per-video state ---
frame_processors = get_frame_processors_modules(modules.globals.frame_processors)
for fp in frame_processors:
if hasattr(fp, 'PREVIOUS_FRAME_RESULT'):
fp.PREVIOUS_FRAME_RESULT = None
# --- Video metadata ---
try:
width, height = get_video_dimensions(target_path)
except Exception as e:
print(f"[DLC.CORE] Failed to get video dimensions: {e}")
return False
total_frames = estimate_frame_count(target_path, fps)
frame_size = width * height * 3
# --- Build encoder arguments ---
encoder = modules.globals.video_encoder
encoder_options: List[str] = []
is_hw_encoder = False
if 'CUDAExecutionProvider' in modules.globals.execution_providers:
if encoder == 'libx264':
encoder = 'h264_nvenc'
is_hw_encoder = True
encoder_options = [
'-preset', 'p4', '-tune', 'hq', '-rc', 'vbr',
'-cq', str(modules.globals.video_quality), '-b:v', '0',
]
elif encoder == 'libx265':
encoder = 'hevc_nvenc'
is_hw_encoder = True
encoder_options = [
'-preset', 'p4', '-tune', 'hq', '-rc', 'vbr',
'-cq', str(modules.globals.video_quality), '-b:v', '0',
]
elif 'DmlExecutionProvider' in modules.globals.execution_providers:
if encoder == 'libx264':
encoder = 'h264_amf'
is_hw_encoder = True
encoder_options = [
'-quality', 'quality', '-rc', 'vbr_latency',
'-qp_i', str(modules.globals.video_quality),
'-qp_p', str(modules.globals.video_quality),
]
elif encoder == 'libx265':
encoder = 'hevc_amf'
is_hw_encoder = True
encoder_options = [
'-quality', 'quality', '-rc', 'vbr_latency',
'-qp_i', str(modules.globals.video_quality),
'-qp_p', str(modules.globals.video_quality),
]
if not is_hw_encoder:
if encoder == 'libx264':
encoder_options = [
'-preset', 'medium',
'-crf', str(modules.globals.video_quality),
'-tune', 'film',
]
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', '-cpu-used', '2',
]
# --- Attempt pipeline (hw encoder first, then sw fallback) ---
encoders_to_try = [(encoder, encoder_options)]
if is_hw_encoder:
# Software fallback
sw_encoder = 'libx264'
sw_options = [
'-preset', 'medium',
'-crf', str(modules.globals.video_quality),
'-tune', 'film',
]
encoders_to_try.append((sw_encoder, sw_options))
for attempt, (enc, enc_opts) in enumerate(encoders_to_try):
# Reset interpolation state on retry
if attempt > 0:
for fp in frame_processors:
if hasattr(fp, 'PREVIOUS_FRAME_RESULT'):
fp.PREVIOUS_FRAME_RESULT = None
success = _run_pipe_pipeline(
target_path, temp_output_path, fps,
source_face, frame_processors,
width, height, frame_size, total_frames,
enc, enc_opts,
)
if success:
return True
if attempt == 0 and is_hw_encoder:
print(f"[DLC.CORE] Hardware encoder '{enc}' failed, "
f"retrying with software encoder...")
return False
def _run_pipe_pipeline(
target_path: str,
temp_output_path: str,
fps: float,
source_face: Any,
frame_processors: List[Any],
width: int,
height: int,
frame_size: int,
total_frames: int,
encoder: str,
encoder_options: List[str],
) -> bool:
"""Run the FFmpeg-pipe read → process → encode pipeline once."""
# --- Reader: decode source video to raw BGR24 on stdout ---
reader_cmd = [
'ffmpeg', '-hide_banner',
'-hwaccel', 'auto',
'-i', target_path,
'-f', 'rawvideo',
'-pix_fmt', 'bgr24',
'-v', 'error',
'-',
]
# --- Writer: encode raw BGR24 from stdin ---
writer_cmd = [
'ffmpeg', '-hide_banner',
'-f', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f'{width}x{height}',
'-r', str(fps),
'-i', '-',
'-c:v', encoder,
]
writer_cmd.extend(encoder_options)
writer_cmd.extend([
'-pix_fmt', 'yuv420p',
'-movflags', '+faststart',
'-vf', 'colorspace=bt709:iall=bt601-6-625:fast=1',
'-v', 'error',
'-y', temp_output_path,
])
reader = None
writer = None
try:
reader = subprocess.Popen(
reader_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
writer = subprocess.Popen(
writer_cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
)
except Exception as e:
print(f"[DLC.CORE] Failed to start FFmpeg pipes: {e}")
for proc in (reader, writer):
if proc:
try:
proc.kill()
except Exception:
pass
return False
processed_count = 0
bar_fmt = ('{l_bar}{bar}| {n_fmt}/{total_fmt} '
'[{elapsed}<{remaining}, {rate_fmt}{postfix}]')
try:
with tqdm(total=total_frames, desc='Processing', unit='frame',
dynamic_ncols=True, bar_format=bar_fmt) as progress:
progress.set_postfix({
'execution_providers': modules.globals.execution_providers,
'threads': modules.globals.execution_threads,
'mode': 'in-memory',
})
# Pipelined detection: while processing frame N (swap on
# ANE), start detecting the face in the next frame
# (detection on GPU). They use different hardware units
# so the work overlaps.
detect_executor = ThreadPoolExecutor(max_workers=1)
pending_detect = None
use_pipeline = not modules.globals.many_faces
while True:
raw = reader.stdout.read(frame_size)
if len(raw) != frame_size:
break
frame = np.frombuffer(raw, dtype=np.uint8).reshape(
(height, width, 3)
).copy()
# Get the detection result for THIS frame
if use_pipeline:
if pending_detect is not None:
target_face = pending_detect.result()
else:
target_face = get_one_face(frame)
# Start detecting on THIS frame eagerly — the result
# will be used for the next iteration. At video
# frame rates the face barely moves between frames.
# Hand the detector its own copy: the frame processors
# below mutate `frame` in place (paste-back), which
# would otherwise race with detection.
pending_detect = detect_executor.submit(
get_one_face, frame.copy())
else:
target_face = None
# Run frame through every active processor
for fp in frame_processors:
try:
frame = fp.process_frame(source_face, frame, target_face=target_face)
except TypeError:
frame = fp.process_frame(source_face, frame)
writer.stdin.write(frame.tobytes())
processed_count += 1
progress.update(1)
detect_executor.shutdown(wait=True)
# Graceful shutdown
writer.stdin.close()
writer.wait()
reader.wait()
if writer.returncode != 0:
stderr_out = writer.stderr.read().decode(errors='ignore').strip()
if stderr_out:
print(f"[DLC.CORE] FFmpeg encoder error: {stderr_out}")
return False
return processed_count > 0 and os.path.isfile(temp_output_path)
except BrokenPipeError:
print("[DLC.CORE] FFmpeg pipe broken (encoder may not be available).")
return False
except Exception as e:
print(f"[DLC.CORE] In-memory processing error: {e}")
return False
finally:
for proc in (reader, writer):
if proc:
try:
proc.kill()
except Exception:
pass
+46 -388
View File
@@ -1,20 +1,18 @@
# 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 numpy as np import gfpgan
import os import os
import onnxruntime
import modules.globals import modules.globals
import modules.processors.frame.core import modules.processors.frame.core
from modules import imread_unicode, imwrite_unicode
from modules.core import update_status from modules.core import update_status
from modules.face_analyser import get_many_faces from modules.face_analyser import get_one_face
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,
) )
@@ -23,37 +21,21 @@ FACE_ENHANCER = None
THREAD_SEMAPHORE = threading.Semaphore() THREAD_SEMAPHORE = threading.Semaphore()
THREAD_LOCK = threading.Lock() THREAD_LOCK = threading.Lock()
NAME = "DLC.FACE-ENHANCER" NAME = "DLC.FACE-ENHANCER"
MODEL_FILE = "gfpgan-1024.onnx"
abs_dir = os.path.dirname(os.path.abspath(__file__)) abs_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join( 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:
from modules.model_downloader import ensure_model download_directory_path = models_dir
conditional_download(
if ensure_model(MODEL_FILE) is None: download_directory_path,
update_status( [
f"Could not obtain {MODEL_FILE}. Place it in the models folder " "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth"
"manually or check your internet connection.", ],
NAME,
) )
return False
return True return True
@@ -66,386 +48,62 @@ def pre_start() -> bool:
return True return True
def get_face_enhancer() -> onnxruntime.InferenceSession: def get_face_enhancer() -> Any:
"""
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:
from modules.model_downloader import ensure_model model_path = os.path.join(models_dir, "GFPGANv1.4.pth")
model_path = ensure_model(MODEL_FILE) match platform.system():
case "Darwin": # Mac OS
if model_path is None: if torch.backends.mps.is_available():
raise FileNotFoundError( mps_device = torch.device("mps")
f"{NAME}: Model not found at " FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1, device=mps_device) # type: ignore[attr-defined]
f"{os.path.join(models_dir, MODEL_FILE)} and could not be " else:
"downloaded" FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1) # type: ignore[attr-defined]
) case _: # Other OS
FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1) # type: ignore[attr-defined]
try:
from modules.processors.frame._onnx_enhancer import (
create_onnx_session,
)
FACE_ENHANCER = create_onnx_session(model_path)
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( def enhance_face(temp_frame: Frame) -> Frame:
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
_HAS_TORCH_CUDA = False
try:
import torch
if torch.cuda.is_available():
_HAS_TORCH_CUDA = True
except ImportError:
pass
# Cache the feathered mask — it's the same for every call at a given size
_enhancer_cache: dict = {'mask': None, 'mask_size': 0}
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.
Optimized: operates on a tight crop around the face bbox instead of the
full frame, and uses GPU for blending when available.
"""
h, w = frame.shape[:2]
inv_matrix = cv2.invertAffineTransform(affine_matrix)
# Build or reuse cached feathered mask (uint8 — blended via cv2 SIMD ops)
if _enhancer_cache['mask_size'] != output_size:
face_mask_f = np.ones((output_size, output_size), dtype=np.float32)
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)
face_mask_f[:border, :] *= ramp_up[:, None]
face_mask_f[-border:, :] *= ramp_down[:, None]
face_mask_f[:, :border] *= ramp_up[None, :]
face_mask_f[:, -border:] *= ramp_down[None, :]
_enhancer_cache['mask'] = (face_mask_f * 255.0).astype(np.uint8)
_enhancer_cache['mask_size'] = output_size
# Compute tight bbox from affine corners (avoids full-frame warpAffine scan)
corners = np.array([[0, 0], [output_size, 0],
[output_size, output_size], [0, output_size]],
dtype=np.float32)
transformed = (inv_matrix[:, :2] @ corners.T).T + inv_matrix[:, 2]
x1 = max(0, int(np.floor(transformed[:, 0].min())))
x2 = min(w, int(np.ceil(transformed[:, 0].max())))
y1 = max(0, int(np.floor(transformed[:, 1].min())))
y2 = min(h, int(np.ceil(transformed[:, 1].max())))
if x1 >= x2 or y1 >= y2:
return frame
# Pad a few pixels for feathering
pad = max(1, int(output_size * 0.05)) + 2
y1p, y2p = max(0, y1 - pad), min(h, y2 + pad)
x1p, x2p = max(0, x1 - pad), min(w, x2 + pad)
crop_w, crop_h = x2p - x1p, y2p - y1p
# Warp enhanced face and mask into crop space only
inv_crop = inv_matrix.copy()
inv_crop[0, 2] -= x1p
inv_crop[1, 2] -= y1p
inv_restored_crop = cv2.warpAffine(
enhanced_face, inv_crop, (crop_w, crop_h),
borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0),
)
inv_mask_crop = cv2.warpAffine(
_enhancer_cache['mask'], inv_crop, (crop_w, crop_h),
borderMode=cv2.BORDER_CONSTANT, borderValue=0,
)
target_crop = frame[y1p:y2p, x1p:x2p]
if _HAS_TORCH_CUDA:
# Upload uint8 alpha — smaller transfer, scale on device.
mask_t = torch.from_numpy(inv_mask_crop).cuda().float().mul_(1.0 / 255.0).unsqueeze(2)
enhanced_t = torch.from_numpy(inv_restored_crop).float().cuda()
target_t = torch.from_numpy(target_crop).float().cuda()
blended = (mask_t * enhanced_t + (1.0 - mask_t) * target_t
).to(torch.uint8).cpu().numpy()
frame[y1p:y2p, x1p:x2p] = blended
else:
# Fused uint8 blend via cv2 SIMD — ~7× faster than the float32 round-trip.
alpha_3c = cv2.merge([inv_mask_crop, inv_mask_crop, inv_mask_crop])
inv_alpha = 255 - alpha_3c
a_enh = cv2.multiply(inv_restored_crop, alpha_3c, scale=1.0 / 255.0)
a_tgt = cv2.multiply(target_crop, inv_alpha, scale=1.0 / 255.0)
frame[y1p:y2p, x1p:x2p] = cv2.add(a_enh, a_tgt)
return frame
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, normalize, and transpose in one pass
# Fused: (x / 255.0 - 0.5) / 0.5 = x / 127.5 - 1.0
rgb = aligned_face[:, :, ::-1] # BGR->RGB zero-copy view
chw = np.transpose(rgb, (2, 0, 1)).astype(np.float32)
chw *= (1.0 / 127.5)
chw -= 1.0
return chw[np.newaxis, ...] # 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].
"""
# Fused: ((x + 1.0) / 2.0) * 255 = (x + 1.0) * 127.5
face = output[0] # remove batch dim -> (3, H, W)
face = (face + 1.0) * 127.5
np.clip(face, 0, 255, out=face)
face = face.astype(np.uint8).transpose(1, 2, 0) # CHW -> HWC
return face[:, :, ::-1].copy() # RGB -> BGR
# Cache for temporal enhancement skipping in live mode.
# GFPGAN output barely changes between consecutive frames (same face,
# same position), so we run inference every _ENH_INTERVAL frames and
# reuse the cached enhanced face + affine matrix in between.
_enh_live_cache: dict = {
'enhanced_bgr': None,
'affine_matrix': None,
'align_size': 0,
'frame_count': 0,
}
_ENH_INTERVAL = 2 # run inference every N frames, paste cached result otherwise
def enhance_face(temp_frame: Frame, detected_faces=None) -> Frame:
"""Enhances all faces in a frame using the GFPGAN ONNX model.
Args:
detected_faces: Pre-detected face list. When provided, skips
the internal detection call (saves ~15-20ms per frame).
Also enables temporal caching — inference runs every
_ENH_INTERVAL frames, reusing the cached result otherwise.
"""
session = get_face_enhancer()
# 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]
try:
align_size = int(input_shape[2])
if align_size <= 0:
align_size = 512
except (ValueError, TypeError, IndexError):
align_size = 512
# Use pre-detected faces if available, otherwise detect
faces = detected_faces if detected_faces is not None else get_many_faces(temp_frame)
if not faces:
return temp_frame
# Temporal caching: only available when faces are pre-detected (live mode)
# AND we're in single-face mode — the cache holds exactly one enhancement,
# so reusing it in many_faces mode would paste the same face onto every
# detected target.
many_faces_mode = getattr(modules.globals, "many_faces", False)
use_cache = detected_faces is not None and not many_faces_mode
if use_cache:
_enh_live_cache['frame_count'] += 1
run_inference_this_frame = (_enh_live_cache['frame_count'] % _ENH_INTERVAL == 0
or _enh_live_cache['enhanced_bgr'] is None)
else:
run_inference_this_frame = True
for face in faces:
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
if run_inference_this_frame:
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: with THREAD_SEMAPHORE:
from modules.processors.frame._onnx_enhancer import ( _, _, temp_frame = get_face_enhancer().enhance(temp_frame, paste_back=True)
run_inference,
)
input_tensor = _preprocess_face(aligned_face)
output_tensor = run_inference(session, input_name, input_tensor)
enhanced_bgr = _postprocess_face(output_tensor)
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,
)
# Cache for reuse on next frame
if use_cache:
_enh_live_cache['enhanced_bgr'] = enhanced_bgr
_enh_live_cache['affine_matrix'] = affine_matrix
_enh_live_cache['align_size'] = align_size
_paste_back(
temp_frame, enhanced_bgr, affine_matrix, output_size=align_size
)
except Exception as e:
print(f"{NAME}: Error enhancing a face: {e}")
continue
else:
# Reuse cached enhanced face — just paste back onto current frame
cached = _enh_live_cache
if cached['enhanced_bgr'] is not None:
_paste_back(
temp_frame, cached['enhanced_bgr'],
cached['affine_matrix'],
output_size=cached['align_size'],
)
if not many_faces_mode:
break # single-face live mode — only process first face
return temp_frame return temp_frame
def process_frame(source_face: Face | None, temp_frame: Frame, def process_frame(source_face: Face, temp_frame: Frame) -> Frame:
detected_faces=None) -> Frame: target_face = get_one_face(temp_frame)
"""Processes a frame: enhances face if detected.""" if target_face:
return enhance_face(temp_frame, detected_faces=detected_faces) temp_frame = enhance_face(temp_frame)
return temp_frame
def process_frame_v2(temp_frame: Frame, detected_faces=None) -> Frame:
"""Processes a frame without source face (used by live webcam preview)."""
return enhance_face(temp_frame, detected_faces=detected_faces)
def process_frames( def process_frames(
source_path: str | None, temp_frame_paths: List[str], progress: Any = None source_path: str, 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): temp_frame = cv2.imread(temp_frame_path)
print( result = process_frame(None, temp_frame)
f"{NAME}: Warning: Frame path not found {temp_frame_path}, skipping." cv2.imwrite(temp_frame_path, result)
)
if progress:
progress.update(1)
continue
temp_frame = imread_unicode(temp_frame_path)
if temp_frame is None:
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)
imwrite_unicode(temp_frame_path, result_frame)
if progress: if progress:
progress.update(1) progress.update(1)
def process_image( def process_image(source_path: str, target_path: str, output_path: str) -> None:
source_path: str | None, target_path: str, output_path: str target_frame = cv2.imread(target_path)
) -> None: result = process_frame(None, target_frame)
"""Processes a single image file.""" cv2.imwrite(output_path, result)
target_frame = imread_unicode(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)
imwrite_unicode(output_path, result_frame)
print(f"{NAME}: Enhanced image saved to {output_path}")
def process_video( def process_video(source_path: str, temp_frame_paths: List[str]) -> None:
source_path: str | None, temp_frame_paths: List[str] modules.processors.frame.core.process_video(None, temp_frame_paths, process_frames)
) -> None:
"""Processes video frames using the frame processor core."""
modules.processors.frame.core.process_video( def process_frame_v2(temp_frame: Frame) -> Frame:
source_path, temp_frame_paths, process_frames target_face = get_one_face(temp_frame)
) if target_face:
temp_frame = enhance_face(temp_frame)
return temp_frame
@@ -1,146 +0,0 @@
"""GPEN-BFR-256 face enhancer — ONNX-based face restoration at 256x256."""
from typing import Any, List
import os
import threading
import modules.globals
import modules.processors.frame.core
from modules import imread_unicode, imwrite_unicode
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_MIRROR_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 _obtain_model():
from modules.model_downloader import ensure_model
model_path = ensure_model(MODEL_FILE)
if model_path is not None:
return model_path
update_status(f"Retrying {MODEL_FILE} from the mirror...", NAME)
from modules.utilities import conditional_download
try:
conditional_download(models_dir, [MODEL_MIRROR_URL])
except Exception as error:
update_status(f"Mirror download failed: {error}", NAME)
return None
fallback = os.path.join(models_dir, MODEL_FILE)
return fallback if os.path.exists(fallback) else None
def pre_check() -> bool:
if _obtain_model() is None:
update_status(
f"Could not obtain {MODEL_FILE}. Place it in the models folder "
"manually or check your internet connection.",
NAME,
)
return False
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 = _obtain_model()
if model_path is None:
raise FileNotFoundError(
f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
)
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, detected_faces=None) -> Frame:
if detected_faces:
target_face = detected_faces[0]
else:
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 = imread_unicode(temp_frame_path)
if temp_frame is None:
if progress:
progress.update(1)
continue
result = process_frame(None, temp_frame)
imwrite_unicode(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 = imread_unicode(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)
imwrite_unicode(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)
@@ -1,146 +0,0 @@
"""GPEN-BFR-512 face enhancer — ONNX-based face restoration at 512x512."""
from typing import Any, List
import os
import threading
import modules.globals
import modules.processors.frame.core
from modules import imread_unicode, imwrite_unicode
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_MIRROR_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 _obtain_model():
from modules.model_downloader import ensure_model
model_path = ensure_model(MODEL_FILE)
if model_path is not None:
return model_path
update_status(f"Retrying {MODEL_FILE} from the mirror...", NAME)
from modules.utilities import conditional_download
try:
conditional_download(models_dir, [MODEL_MIRROR_URL])
except Exception as error:
update_status(f"Mirror download failed: {error}", NAME)
return None
fallback = os.path.join(models_dir, MODEL_FILE)
return fallback if os.path.exists(fallback) else None
def pre_check() -> bool:
if _obtain_model() is None:
update_status(
f"Could not obtain {MODEL_FILE}. Place it in the models folder "
"manually or check your internet connection.",
NAME,
)
return False
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 = _obtain_model()
if model_path is None:
raise FileNotFoundError(
f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
)
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, detected_faces=None) -> Frame:
if detected_faces:
target_face = detected_faces[0]
else:
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 = imread_unicode(temp_frame_path)
if temp_frame is None:
if progress:
progress.update(1)
continue
result = process_frame(None, temp_frame)
imwrite_unicode(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 = imread_unicode(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)
imwrite_unicode(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)
-577
View File
@@ -1,577 +0,0 @@
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
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
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env python3
# Import the tkinter fix to patch the ScreenChanged error (module patches Tk on import)
import tkinter_fix # noqa: F401
import core
if __name__ == '__main__':
core.run()
-26
View File
@@ -1,26 +0,0 @@
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()
+1050 -1344
View File
File diff suppressed because it is too large Load Diff
-74
View File
@@ -1,74 +0,0 @@
"""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
+34 -178
View File
@@ -15,27 +15,26 @@ 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", "auto", # Auto-detect hardware acceleration "-hwaccel",
"-hwaccel_output_format", "auto", # Use hardware format when possible "auto",
"-threads", str(modules.globals.execution_threads or 0), # 0 = auto-detect optimal thread count "-loglevel",
"-loglevel", modules.globals.log_level, modules.globals.log_level,
] ]
commands.extend(args) commands.extend(args)
try: try:
subprocess.check_output(commands, stderr=subprocess.STDOUT) subprocess.check_output(commands, stderr=subprocess.STDOUT)
return True return True
except subprocess.CalledProcessError as error: except Exception:
output = error.output.decode(errors="ignore").strip() pass
if output:
print(output)
except Exception as error:
print(f"ffmpeg execution failed: {error}")
return False return False
@@ -62,132 +61,39 @@ 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)
# Write a contiguous image sequence so the later "%04d.png" input pattern
# used during encoding can consume every frame reliably.
run_ffmpeg( run_ffmpeg(
[ [
"-i", target_path, "-i",
"-vf", "format=rgb24", # Use video filter for format conversion (faster) target_path,
"-vsync", "0", # Prevent frame duplication "-pix_fmt",
"rgb24",
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) -> bool: 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 [
encoder = modules.globals.video_encoder "-r",
encoder_options = [] str(fps),
"-i",
# GPU-accelerated encoding options os.path.join(temp_directory_path, "%04d.png"),
if 'CUDAExecutionProvider' in modules.globals.execution_providers: "-c:v",
# NVIDIA GPU encoding modules.globals.video_encoder,
if encoder == 'libx264': "-crf",
encoder = 'h264_nvenc' str(modules.globals.video_quality),
encoder_options = [ "-pix_fmt",
"-preset", "p7", # Highest quality preset for NVENC "yuv420p",
"-tune", "hq", # High quality tuning "-vf",
"-rc", "vbr", # Variable bitrate "colorspace=bt709:iall=bt601-6-625:fast=1",
"-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,
] ]
success = run_ffmpeg(ffmpeg_args_fallback) )
return success and os.path.isfile(temp_output_path)
def restore_audio(target_path: str, output_path: str) -> None: def restore_audio(target_path: str, output_path: str) -> None:
@@ -262,16 +168,11 @@ def clean_temp(target_path: str) -> None:
def has_image_extension(image_path: str) -> bool: def has_image_extension(image_path: str) -> bool:
# splitext so only the real extension counts (e.g. "photo.png.bak" is not return image_path.lower().endswith(("png", "jpg", "jpeg"))
# an image); the set is centralized in globals to stay in sync with dialogs.
return os.path.splitext(image_path)[1].lower() in modules.globals.IMAGE_EXTENSIONS
def is_image(image_path: str) -> bool: def is_image(image_path: str) -> bool:
if image_path and os.path.isfile(image_path): if image_path and os.path.isfile(image_path):
# Extension check first — Windows mimetypes doesn't always register webp
if has_image_extension(image_path):
return True
mimetype, _ = mimetypes.guess_type(image_path) mimetype, _ = mimetypes.guess_type(image_path)
return bool(mimetype and mimetype.startswith("image/")) return bool(mimetype and mimetype.startswith("image/"))
return False return False
@@ -292,15 +193,8 @@ 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.Request(url) request = urllib.request.urlopen(url) # type: ignore[attr-defined]
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",
@@ -308,46 +202,8 @@ 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:
with open(download_file_path, "wb") as f: urllib.request.urlretrieve(url, download_file_path, reporthook=lambda count, block_size, total_size: progress.update(block_size)) # type: ignore[attr-defined]
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:
return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) return os.path.abspath(os.path.join(os.path.dirname(__file__), path))
def get_video_dimensions(target_path: str) -> tuple:
"""Get video width and height using ffprobe."""
command = [
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=p=0:s=x",
target_path,
]
output = subprocess.check_output(command).decode().strip()
width, height = map(int, output.split("x"))
return width, height
def estimate_frame_count(target_path: str, fps: float = None) -> int:
"""Estimate total frame count from video duration and fps."""
if fps is None:
fps = detect_fps(target_path)
command = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "csv=p=0",
target_path,
]
try:
output = subprocess.check_output(command).decode().strip()
duration = float(output)
return int(duration * fps)
except Exception:
return 0
+8 -74
View File
@@ -1,6 +1,5 @@
import cv2 import cv2
import numpy as np import numpy as np
import time
from typing import Optional, Tuple, Callable from typing import Optional, Tuple, Callable
import platform import platform
import threading import threading
@@ -18,10 +17,6 @@ class VideoCapturer:
self._frame_ready = threading.Event() self._frame_ready = threading.Event()
self.is_running = False self.is_running = False
self.cap = None self.cap = None
# Actual values reported by the camera after configuration
self.actual_width: int = 0
self.actual_height: int = 0
self.actual_fps: float = 0.0
# Initialize Windows-specific components if on Windows # Initialize Windows-specific components if on Windows
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -37,72 +32,34 @@ class VideoCapturer:
"""Initialize and start video capture""" """Initialize and start video capture"""
try: try:
if platform.system() == "Windows": if platform.system() == "Windows":
# device_index comes from pygrabber.FilterGraph (DirectShow # Windows-specific capture methods
# enumeration), so open with DSHOW first to preserve mapping.
# MSMF and DirectShow enumerate cameras in different orders, so
# opening MSMF with a DSHOW index silently selects the wrong
# camera. MSMF/ANY remain as fallbacks for cameras DSHOW can't
# open.
#
# Pass codec + resolution + fps as construction params (OpenCV
# 4.6+). DSHOW locks the pixel format at open time and ignores
# later cap.set(CAP_PROP_FOURCC, ...) — without this, DSHOW
# falls back to uncompressed YUYV at 1080p, which is USB-
# bandwidth-limited to ~5 fps. Setting MJPG at construction
# negotiates compressed frames from the first read.
mjpg = cv2.VideoWriter_fourcc(*'MJPG')
open_params = [
cv2.CAP_PROP_FOURCC, mjpg,
cv2.CAP_PROP_FRAME_WIDTH, width,
cv2.CAP_PROP_FRAME_HEIGHT, height,
cv2.CAP_PROP_FPS, fps,
]
capture_methods = [ capture_methods = [
(self.device_index, cv2.CAP_DSHOW), (self.device_index, cv2.CAP_DSHOW), # Try DirectShow first
(self.device_index, cv2.CAP_MSMF), (self.device_index, cv2.CAP_ANY), # Then try default backend
(self.device_index, cv2.CAP_ANY), (-1, cv2.CAP_ANY), # Try -1 as fallback
(0, cv2.CAP_ANY), # Finally try 0 without specific backend
] ]
for dev_id, backend in capture_methods: for dev_id, backend in capture_methods:
try: try:
self.cap = cv2.VideoCapture(dev_id, backend, open_params) self.cap = cv2.VideoCapture(dev_id, backend)
if self.cap.isOpened(): if self.cap.isOpened():
break break
self.cap.release() self.cap.release()
except Exception: except Exception:
continue continue
elif platform.system() == "Linux":
self.cap = cv2.VideoCapture(f"/dev/video{self.device_index}")
else: else:
# Unix-like systems (Linux/Mac) capture method
self.cap = cv2.VideoCapture(self.device_index) self.cap = cv2.VideoCapture(self.device_index)
if not self.cap or not self.cap.isOpened(): if not self.cap or not self.cap.isOpened():
raise RuntimeError("Failed to open camera") raise RuntimeError("Failed to open camera")
# Belt-and-braces: also set via cap.set() for backends that honor # Configure format
# post-open changes (MSMF, V4L2). DSHOW ignores these, but the
# construction params above already handled it.
if platform.system() != "Windows":
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
self.cap.set(cv2.CAP_PROP_FPS, fps) self.cap.set(cv2.CAP_PROP_FPS, fps)
# Read back resolution (usually reliable)
self.actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# CAP_PROP_FPS is unreliable on DirectShow — often reports 30
# even when the camera delivers 60. Measure empirically by
# timing a burst of frames.
reported_fps = self.cap.get(cv2.CAP_PROP_FPS)
self.actual_fps = self._measure_fps(warmup=10, sample=30,
fallback=reported_fps or fps)
print(f"[VideoCapturer] {self.actual_width}x{self.actual_height} "
f"@ {self.actual_fps:.1f}fps (reported={reported_fps:.0f})",
flush=True)
self.is_running = True self.is_running = True
return True return True
@@ -132,29 +89,6 @@ class VideoCapturer:
self.is_running = False self.is_running = False
self.cap = None self.cap = None
def _measure_fps(self, warmup: int = 10, sample: int = 30,
fallback: float = 30.0) -> float:
"""Read warmup+sample frames and return measured FPS.
This is more reliable than CAP_PROP_FPS which often lies on
DirectShow. Takes ~0.5-1s at startup but gives a ground-truth
number for adaptive polling/detection intervals.
"""
try:
for _ in range(warmup):
self.cap.read()
t0 = time.perf_counter()
for _ in range(sample):
ret, _ = self.cap.read()
if not ret:
return fallback
elapsed = time.perf_counter() - t0
if elapsed <= 0:
return fallback
return sample / elapsed
except Exception:
return fallback
def set_frame_callback(self, callback: Callable[[np.ndarray], None]) -> None: def set_frame_callback(self, callback: Callable[[np.ndarray], None]) -> None:
"""Set callback for frame processing""" """Set callback for frame processing"""
self.frame_callback = callback self.frame_callback = callback
-9
View File
@@ -1,9 +0,0 @@
[tool.ruff]
target-version = "py310"
[tool.ruff.lint]
# Deterministic, low-risk rules enforced in CI. Other rules (F841, E402, F821)
# surface real findings but require human judgement to fix safely, so they are
# left out of the gate for now. Intentional side-effect imports should be
# annotated with `# noqa: F401`.
select = ["E701", "E711", "E712", "F401", "F541"]
+24 -17
View File
@@ -1,18 +1,25 @@
numpy>=2.0,<3 --extra-index-url https://download.pytorch.org/whl/cu118
typing-extensions>=4.15.0
opencv-python==4.14.0.94 numpy>=1.23.5,<2
opencv-python-headless==4.14.0.94 typing-extensions>=4.8.0
cv2_enumerate_cameras==1.3.3 opencv-python==4.10.0.84
onnx==1.22.0 cv2_enumerate_cameras==1.1.15
onnx==1.16.0
insightface==0.7.3 insightface==0.7.3
psutil==7.2.2 psutil==5.9.8
PySide6>=6.7,<7 tk==0.1.0
pillow==12.3.0 customtkinter==5.2.2
tqdm>=4.66.3 pillow==11.1.0
onnxruntime==1.28.0; sys_platform == 'darwin' and platform_machine == 'arm64' torch==2.5.1+cu118; sys_platform != 'darwin'
onnxruntime==1.23.0; sys_platform == 'darwin' and platform_machine != 'arm64' torch==2.5.1; sys_platform == 'darwin'
onnxruntime-gpu==1.26.0; sys_platform != 'darwin' torchvision==0.20.1; sys_platform != 'darwin'
opennsfw2==0.18.0 torchvision==0.20.1; sys_platform == 'darwin'
keras>=3.0.0 onnxruntime-silicon==1.16.3; sys_platform == 'darwin' and platform_machine == 'arm64'
protobuf>=6.33.5,<8 onnxruntime-gpu==1.16.3; sys_platform != 'darwin'
pygrabber; sys_platform == 'win32' tensorflow; sys_platform != 'darwin'
opennsfw2==0.10.2
protobuf==4.23.2
tqdm==4.66.4
gfpgan==1.3.8
tkinterdnd2==0.4.2
pygrabber==0.2
-93
View File
@@ -1,98 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import sys
# Add the project root to PATH so bundled ffmpeg/ffprobe are found
project_root = os.path.dirname(os.path.abspath(__file__))
os.environ["PATH"] = project_root + os.pathsep + os.environ.get("PATH", "")
# On Windows, register NVIDIA CUDA DLL directories so onnxruntime-gpu can
# find cuDNN/cublas. Python 3.8+ ignores PATH for extension-module native deps —
# os.add_dll_directory() is required. Also keep PATH for child processes/ffmpeg.
if sys.platform == "win32":
_site_packages = os.path.join(sys.prefix, "Lib", "site-packages")
_venv_site_packages = os.path.join(project_root, "venv", "Lib", "site-packages")
for _sp in (_site_packages, _venv_site_packages):
_candidate_dirs = []
_torch_lib = os.path.join(_sp, "torch", "lib")
if os.path.isdir(_torch_lib):
_candidate_dirs.append(_torch_lib)
_nvidia_dir = os.path.join(_sp, "nvidia")
if os.path.isdir(_nvidia_dir):
for _pkg in os.listdir(_nvidia_dir):
_bin_dir = os.path.join(_nvidia_dir, _pkg, "bin")
if os.path.isdir(_bin_dir):
_candidate_dirs.append(_bin_dir)
for _d in _candidate_dirs:
os.environ["PATH"] = _d + os.pathsep + os.environ["PATH"]
try:
os.add_dll_directory(_d)
except (OSError, AttributeError):
pass
# On Windows, register OpenVINO DLL directories so onnxruntime's
# OpenVINOExecutionProvider can find openvino.dll. This must happen
# before any ONNX InferenceSession is created. Failure is non-fatal:
# OpenVINO simply isn't installed, and onnxruntime will fall back to CPU.
try:
from onnxruntime.tools.add_openvino_win_libs import ( # type: ignore[import-untyped] # noqa: E501
add_openvino_libs_to_path,
)
add_openvino_libs_to_path()
except ImportError:
# onnxruntime build without the OpenVINO tooling module — no-op.
pass
except FileNotFoundError:
# OpenVINO site-packages dir absent — no-op.
pass
except SystemExit as exc:
# add_openvino_libs_to_path() calls sys.exit() when OpenVINO libs
# can't be located (e.g. OPENVINO_LIB_PATHS unset). Log the message
# it raised with so the failure is visible, but keep startup alive.
print(
f"[startup] OpenVINO DLL registration skipped: {exc}",
flush=True,
)
# On Linux, pre-load NVIDIA shared libraries (cuDNN, cuBLAS, nvrtc...) shipped
# inside the venv via pip wheels (nvidia-cudnn-cu12, etc.). LD_LIBRARY_PATH
# cannot be set after Python starts, so we use ctypes.CDLL with RTLD_GLOBAL
# instead. This makes symbols available to onnxruntime when it dlopens its
# CUDA provider.
if sys.platform.startswith("linux"):
import ctypes
import glob
_py_lib = f"python{sys.version_info.major}.{sys.version_info.minor}"
_site_packages_candidates = [
os.path.join(project_root, "venv", "lib", _py_lib, "site-packages"),
os.path.join(sys.prefix, "lib", _py_lib, "site-packages"),
]
for _sp in _site_packages_candidates:
_nvidia_dir = os.path.join(_sp, "nvidia")
if not os.path.isdir(_nvidia_dir):
continue
for _pkg in os.listdir(_nvidia_dir):
_lib_dir = os.path.join(_nvidia_dir, _pkg, "lib")
if not os.path.isdir(_lib_dir):
continue
# Also expose the directory to child processes, without
# duplicating an entry that is already present.
_ldp = os.environ.get("LD_LIBRARY_PATH", "")
if _lib_dir not in _ldp.split(os.pathsep):
os.environ["LD_LIBRARY_PATH"] = (
_lib_dir + (os.pathsep + _ldp if _ldp else "")
)
for _so in sorted(glob.glob(os.path.join(_lib_dir, "lib*.so*"))):
try:
ctypes.CDLL(_so, mode=ctypes.RTLD_GLOBAL)
except OSError:
pass
break
from modules import platform_info
platform_info.print_banner()
from modules import core from modules import core
if __name__ == '__main__': if __name__ == '__main__':
-137
View File
@@ -1,137 +0,0 @@
import importlib
import sys
import types
import unittest
from contextlib import contextmanager
from unittest.mock import patch
@contextmanager
def _patched_core_import_stubs(calls, pipe_result=False):
class Processor:
NAME = "test_processor"
def pre_start(self):
return True
def pre_check(self):
return True
def process_image(self, *_args, **_kwargs):
raise AssertionError("image path should not be used")
def process_video(self, source_path, frame_paths):
calls.append(("process_video", source_path, tuple(frame_paths)))
stubs = {
"cv2": types.SimpleNamespace(
IMREAD_COLOR=1,
imdecode=lambda *_args, **_kwargs: None,
imencode=lambda *_args, **_kwargs: (
True,
types.SimpleNamespace(tofile=lambda *_a, **_k: None),
),
),
"numpy": types.SimpleNamespace(uint8=object, fromfile=lambda *_args, **_kwargs: b""),
"torch": types.SimpleNamespace(
cuda=types.SimpleNamespace(empty_cache=lambda: None)
),
"onnxruntime": types.SimpleNamespace(
get_available_providers=lambda: ["CPUExecutionProvider"]
),
"tensorflow": types.SimpleNamespace(),
"modules.metadata": types.SimpleNamespace(name="Deep-Live-Cam", version="test"),
"modules.ui": types.SimpleNamespace(
check_and_ignore_nsfw=lambda *_args, **_kwargs: False,
update_status=lambda *_args, **_kwargs: None,
init=lambda *_args, **_kwargs: types.SimpleNamespace(mainloop=lambda: None),
),
"modules.processors.frame.core": types.SimpleNamespace(
get_frame_processors_modules=lambda _names: [Processor()],
process_video_in_memory=lambda *_args, **_kwargs: calls.append(("pipe",))
or pipe_result,
),
"modules.utilities": types.SimpleNamespace(
has_image_extension=lambda _path: False,
is_image=lambda _path: False,
is_video=lambda _path: True,
detect_fps=lambda _path: 24.0,
create_video=lambda target_path, fps: calls.append(
("create_video", target_path, fps)
)
or True,
extract_frames=lambda target_path: calls.append(
("extract_frames", target_path)
),
get_temp_frame_paths=lambda target_path: [f"{target_path}/0001.png"],
restore_audio=lambda *_args, **_kwargs: calls.append(("restore_audio",)),
create_temp=lambda target_path: calls.append(("create_temp", target_path)),
move_temp=lambda target_path, output_path: calls.append(
("move_temp", target_path, output_path)
),
clean_temp=lambda target_path: calls.append(("clean_temp", target_path)),
normalize_output_path=lambda _source, _target, output: output,
),
}
with patch.dict(sys.modules, stubs, clear=False):
sys.modules.pop("modules.core", None)
yield importlib.import_module("modules.core")
sys.modules.pop("modules.core", None)
def _configure_video_run(core, *, map_faces):
core.modules.globals.source_path = "source.jpg"
core.modules.globals.target_path = "target.mp4"
core.modules.globals.output_path = "output.mp4"
core.modules.globals.frame_processors = ["face_swapper"]
core.modules.globals.headless = True
core.modules.globals.keep_fps = False
core.modules.globals.keep_audio = False
core.modules.globals.keep_frames = False
core.modules.globals.map_faces = map_faces
core.modules.globals.nsfw_filter = False
core.modules.globals.execution_threads = 1
core.modules.globals.execution_providers = ["CPUExecutionProvider"]
core.modules.globals.max_memory = None
class MapFacesFallbackTests(unittest.TestCase):
def test_map_faces_disk_fallback_extracts_frames_before_processing(self):
calls = []
with _patched_core_import_stubs(calls, pipe_result=False) as core:
_configure_video_run(core, map_faces=True)
with patch.object(core.os.path, "isfile", return_value=True):
core.start()
self.assertNotIn(("pipe",), calls)
self.assertIn(("create_temp", "target.mp4"), calls)
self.assertIn(("extract_frames", "target.mp4"), calls)
self.assertIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
self.assertIn(("create_video", "target.mp4", 30.0), calls)
self.assertIn(("move_temp", "target.mp4", "output.mp4"), calls)
step_indices = {}
for index, call in enumerate(calls):
step_indices.setdefault(call[0], index)
self.assertLess(step_indices["create_temp"], step_indices["extract_frames"])
self.assertLess(step_indices["extract_frames"], step_indices["process_video"])
self.assertLess(step_indices["process_video"], step_indices["create_video"])
self.assertLess(step_indices["create_video"], step_indices["move_temp"])
def test_non_map_faces_pipe_success_does_not_extract_frames(self):
calls = []
with _patched_core_import_stubs(calls, pipe_result=True) as core:
_configure_video_run(core, map_faces=False)
with patch.object(core.os.path, "isfile", return_value=True):
core.start()
self.assertIn(("pipe",), calls)
self.assertNotIn(("extract_frames", "target.mp4"), calls)
self.assertNotIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
if __name__ == "__main__":
unittest.main()
-97
View File
@@ -1,97 +0,0 @@
import importlib
import sys
import types
import unittest
from unittest.mock import patch
def _install_import_stubs():
sys.modules.setdefault(
"insightface",
types.SimpleNamespace(app=types.SimpleNamespace(FaceAnalysis=object)),
)
sys.modules.setdefault(
"cv2",
types.SimpleNamespace(
IMREAD_COLOR=1,
imread=lambda *_args, **_kwargs: None,
imdecode=lambda *_args, **_kwargs: None,
imencode=lambda *_args, **_kwargs: (
True,
types.SimpleNamespace(tofile=lambda *_a, **_k: None),
),
),
)
sys.modules.setdefault(
"numpy",
types.SimpleNamespace(uint8=object, fromfile=lambda *_args, **_kwargs: b""),
)
sys.modules.setdefault(
"tqdm",
types.SimpleNamespace(tqdm=lambda iterable, **_kwargs: iterable),
)
sys.modules["modules.typing"] = types.SimpleNamespace(Frame=object)
sys.modules["modules.cluster_analysis"] = types.SimpleNamespace(
find_cluster_centroids=lambda *args, **kwargs: [],
find_closest_centroid=lambda *args, **kwargs: (0, None),
)
sys.modules["modules.utilities"] = types.SimpleNamespace(
get_temp_directory_path=lambda path: path,
create_temp=lambda path: None,
extract_frames=lambda path: None,
clean_temp=lambda path: None,
get_temp_frame_paths=lambda path: [],
)
def _load_face_analyser():
_install_import_stubs()
sys.modules.pop("modules.face_analyser", None)
return importlib.import_module("modules.face_analyser")
class Face:
def __init__(self, left):
self.bbox = [left, 0, 10, 10]
class GetOneFaceTests(unittest.TestCase):
def test_uses_supplied_detected_faces_without_reanalysing_frame(self):
face_analyser = _load_face_analyser()
right = Face(20)
left = Face(5)
with patch.object(
face_analyser,
"_analyse_faces",
side_effect=AssertionError("should not analyse"),
):
self.assertIs(face_analyser.get_one_face("frame", [right, left]), left)
def test_supplied_empty_detected_faces_returns_none(self):
face_analyser = _load_face_analyser()
with patch.object(
face_analyser,
"_analyse_faces",
side_effect=AssertionError("should not analyse"),
):
self.assertIsNone(face_analyser.get_one_face("frame", []))
def test_without_supplied_faces_preserves_existing_detection_path(self):
face_analyser = _load_face_analyser()
right = Face(30)
left = Face(3)
with patch.object(face_analyser, "_is_dml", return_value=False), patch.object(
face_analyser,
"_analyse_faces",
return_value=[right, left],
) as analyse:
self.assertIs(face_analyser.get_one_face("frame"), left)
analyse.assert_called_once_with("frame")
if __name__ == "__main__":
unittest.main()
-29
View File
@@ -1,29 +0,0 @@
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()