From fe9b2bc8e58f958580961165d35dc5813055e81e Mon Sep 17 00:00:00 2001 From: Henry Ruhs Date: Wed, 19 Jul 2023 13:32:58 +0200 Subject: [PATCH] Next (#734) * Fix video frames lower than threads * Skip target audio (#656) * Improve return typing * Use face enhancer device according to execution provider * Lock face by reference (#679) * Lock face by position of a face reference * Prevent exception for get_many_faces * Finalize face reference implementation * Fix potential exception * Use sys.exit() over quit() * Split frame processor error to reduce confusion * Improve face reference by introducing more CLI args * Prevent AttributeError if face is None * Update dependencies * Move reference creation to process_video * Allow to initialize UI with source path and target path * Allow to initialize UI with source path and target path * Allow to initialize UI with source path and target path * Use onnxruntime-coreml for old MacOS * Fix typing * Fix typing * Fix typing * Temp fix for enhancer * Temp fix for enhancer * Keyboard bindings to change reference face via Up/Down * Fix slow preview * ignore * Update README and ISSUE TEMPLATES * Right/Left to update frames by +10/-10 * Fix fps mismatch * Add fps parameter to extract_frames() * Minor wording cosmetics * Improve enhancer performance by using cropped face * Fix suggested threads and memory * Extract frames with FPS output * Remove default max-memory * Remove release_resources() as it does not work * Ignore torch import * Add drag and drop for source and target * Fix typing * Bump version * Limit Left/Right binding to videos * Add key binding hits to preview --- .flake8 | 2 +- .github/ISSUE_TEMPLATE/bug.md | 44 +++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 39 ---------- .github/ISSUE_TEMPLATE/installation.md | 12 +++ .github/ISSUE_TEMPLATE/suggestion.md | 11 --- README.md | 19 +++-- gui-demo.png | Bin 23444 -> 26915 bytes requirements-ci.txt | 9 ++- requirements.txt | 14 ++-- roop/capturer.py | 6 +- roop/core.py | 82 ++++++++++---------- roop/face_analyser.py | 41 +++++++--- roop/face_reference.py | 21 +++++ roop/globals.py | 7 +- roop/metadata.py | 2 +- roop/{predicter.py => predictor.py} | 22 +++++- roop/processors/frame/core.py | 9 ++- roop/processors/frame/face_enhancer.py | 47 ++++++++---- roop/processors/frame/face_swapper.py | 28 +++++-- roop/ui.json | 3 + roop/ui.py | 102 +++++++++++++++++++------ roop/utilities.py | 18 ++--- 22 files changed, 350 insertions(+), 188 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/installation.md delete mode 100644 .github/ISSUE_TEMPLATE/suggestion.md create mode 100644 roop/face_reference.py rename roop/{predicter.py => predictor.py} (63%) diff --git a/.flake8 b/.flake8 index 43a1b76..4c9027e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] select = E3, E4, F -per-file-ignores = roop/core.py:E402 \ No newline at end of file +per-file-ignores = roop/core.py:E402,F401 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..ee03573 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,44 @@ +--- +name: Bug +about: Report a bug +title: '[Bug]' +labels: 'bug' + +--- + +## Description + +A concise description of the bug and how to reproduce it. + +## Error + +Paste the error or exception from your console: + +``` + +``` + +## Details + +What operating system are you using? + +- [ ] Windows +- [ ] MacOS (Apple Silicon) +- [ ] MacOS (Apple Legacy) +- [ ] Linux +- [ ] Linux in WSL + +What execution provider are you using? + +- [ ] CPU +- [ ] CUDA +- [ ] CoreML +- [ ] DirectML +- [ ] OpenVINO +- [ ] Other + +What version of Roop are you using? + +- [ ] 1.0.0 +- [ ] 1.1.0 +- [ ] next diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 2addecc..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Details** -What OS are you using? -- [ ] Linux -- [ ] Linux in WSL -- [ ] Windows -- [ ] Mac - -Are you try to use a GPU? -- [ ] No. I am not using the `---gpu` flag -- [ ] NVIDIA -- [ ] AMD -- [ ] Intel -- [ ] Mac - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Sanity Check** -- [ ] I have the latest code from the github repository -- [ ] I have followed the installation guide diff --git a/.github/ISSUE_TEMPLATE/installation.md b/.github/ISSUE_TEMPLATE/installation.md new file mode 100644 index 0000000..966417b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/installation.md @@ -0,0 +1,12 @@ +--- +name: Installation +about: Platform and installation issues +title: '[Installation]' +labels: 'installation' + +--- + +Please **DO NOT OPEN** platform and installation issues! + +- Check the [troubleshooting](https://github.com/s0md3v/roop/wiki/4.-Troubleshooting) that covers many issues. +- Join our helpful community on [Discord](https://discord.gg/Y9p4ZQ2sB9) for instant help. diff --git a/.github/ISSUE_TEMPLATE/suggestion.md b/.github/ISSUE_TEMPLATE/suggestion.md deleted file mode 100644 index 180c384..0000000 --- a/.github/ISSUE_TEMPLATE/suggestion.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Suggestion -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Describe your suggestion** -A clear and concise description of what you want to happen. diff --git a/README.md b/README.md index ca3e92a..e2d7920 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,24 @@ You can watch some demos [here](https://drive.google.com/drive/folders/1KHv8n_rd ## Disclaimer This software is meant to be a productive contribution to the rapidly growing AI-generated media industry. It will help artists with tasks such as animating a custom character or using the character as a model for clothing etc. -The developers of this software are aware of its possible unethical applicaitons and are committed to take preventative measures against them. It has a built-in check which prevents the program from working on inappropriate media including but not limited to nudity, graphic content, sensitive material such as war footage etc. We will continue to develop this project in the positive direction while adhering to law and ethics. This project may be shut down or include watermarks on the output if requested by law. +The developers of this software are aware of its possible unethical applications and are committed to take preventative measures against them. It has a built-in check which prevents the program from working on inappropriate media including but not limited to nudity, graphic content, sensitive material such as war footage etc. We will continue to develop this project in the positive direction while adhering to law and ethics. This project may be shut down or include watermarks on the output if requested by law. Users of this software are expected to use this software responsibly while abiding the local law. If face of a real person is being used, users are suggested to get consent from the concerned person and clearly mention that it is a deepfake when posting content online. Developers of this software will not be responsible for actions of end-users. ## How do I install it? -**Issues regarding installation will be closed from now on, we cannot handle the amount of requests.** +### Basic -- **Basic:** It is more likely to work on your computer but it will also be very slow. You can follow instructions for the basic install [here](https://github.com/s0md3v/roop/wiki/1.-Installation). +It is more likely to work on your computer but it will also be very slow. You can follow instructions for the basic install [here](https://github.com/s0md3v/roop/wiki/1.-Installation). -- **Acceleration:** If you have a good GPU and are ready for solving any software issues you may face, you can enable GPU which is wayyy faster. To do this, first follow the basic install instructions given above and then follow GPU-specific instructions [here](https://github.com/s0md3v/roop/wiki/2.-Acceleration). +### Acceleration + +If you have a good GPU and are ready for solving any software issues you may face, you can enable GPU which is wayyy faster. To do this, first follow the basic install instructions given above and then follow GPU-specific instructions [here](https://github.com/s0md3v/roop/wiki/2.-Acceleration). ## How do I use it? -> Note: When you run this program for the first time, it will download some models ~300MB in size. Executing `python run.py` command will launch this window: + ![gui-demo](gui-demo.png) Choose a face (image with desired face) and the target image/video (image/video in which you want to replace the face) and click on `Start`. Open file explorer and navigate to the directory you select your output to be in. You will find a directory named `` where you can see the frames being swapped in realtime. Once the processing is done, it will create the output file. That's it. @@ -36,10 +38,13 @@ options: -t TARGET_PATH, --target TARGET_PATH select an target image or video -o OUTPUT_PATH, --output OUTPUT_PATH select output file or directory --frame-processor FRAME_PROCESSOR [FRAME_PROCESSOR ...] frame processors (choices: face_swapper, face_enhancer, ...) - --keep-fps keep original fps - --keep-audio keep original audio + --keep-fps keep target fps --keep-frames keep temporary frames + --skip-audio skip target audio --many-faces process every face + --reference-face-position REFERENCE_FACE_POSITION position of the reference face + --reference-frame-number REFERENCE_FRAME_NUMBER number of the reference frame + --similar-face-distance SIMILAR_FACE_DISTANCE face distance used for recognition --video-encoder {libx264,libx265,libvpx-vp9} adjust output video encoder --video-quality [0-51] adjust output video quality --max-memory MAX_MEMORY maximum amount of RAM in GB diff --git a/gui-demo.png b/gui-demo.png index b76a54da3ae235549d12f3046f8bdaa1882be2ba..345c420f4881740eef2d5164ff7d0c124715a275 100644 GIT binary patch literal 26915 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 Any: + +def get_video_frame(video_path: str, frame_number: int = 0) -> Optional[Frame]: capture = cv2.VideoCapture(video_path) frame_total = capture.get(cv2.CAP_PROP_FRAME_COUNT) capture.set(cv2.CAP_PROP_POS_FRAMES, min(frame_total, frame_number - 1)) diff --git a/roop/core.py b/roop/core.py index b70d854..32a6d39 100755 --- a/roop/core.py +++ b/roop/core.py @@ -15,18 +15,17 @@ import shutil import argparse import torch import onnxruntime +if not 'CUDAExecutionProvider' in onnxruntime.get_available_providers(): + del torch import tensorflow import roop.globals import roop.metadata import roop.ui as ui -from roop.predicter import predict_image, predict_video +from roop.predictor import predict_image, predict_video from roop.processors.frame.core import get_frame_processors_modules from roop.utilities import has_image_extension, is_image, is_video, detect_fps, create_video, extract_frames, get_temp_frame_paths, restore_audio, create_temp, move_temp, clean_temp, normalize_output_path -if 'ROCMExecutionProvider' in roop.globals.execution_providers: - del torch - warnings.filterwarnings('ignore', category=FutureWarning, module='insightface') warnings.filterwarnings('ignore', category=UserWarning, module='torchvision') @@ -38,13 +37,16 @@ def parse_args() -> None: 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('--frame-processor', help='frame processors (choices: face_swapper, face_enhancer, ...)', dest='frame_processor', default=['face_swapper'], nargs='+') - 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-frames', help='keep temporary frames', dest='keep_frames', action='store_true', default=False) - program.add_argument('--many-faces', help='process every face', dest='many_faces', action='store_true', default=False) + program.add_argument('--keep-fps', help='keep target fps', dest='keep_fps', action='store_true') + program.add_argument('--keep-frames', help='keep temporary frames', dest='keep_frames', action='store_true') + program.add_argument('--skip-audio', help='skip target audio', dest='skip_audio', action='store_true') + program.add_argument('--many-faces', help='process every face', dest='many_faces', action='store_true') + program.add_argument('--reference-face-position', help='position of the reference face', dest='reference_face_position', type=int, default=0) + program.add_argument('--reference-frame-number', help='number of the reference frame', dest='reference_frame_number', type=int, default=0) + program.add_argument('--similar-face-distance', help='face distance used for recognition', dest='similar_face_distance', type=float, default=0.85) program.add_argument('--video-encoder', help='adjust output video encoder', dest='video_encoder', default='libx264', choices=['libx264', 'libx265', 'libvpx-vp9']) program.add_argument('--video-quality', help='adjust output video quality', dest='video_quality', type=int, default=18, choices=range(52), metavar='[0-51]') - 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) program.add_argument('--execution-provider', help='available execution provider (choices: cpu, ...)', 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=suggest_execution_threads()) program.add_argument('-v', '--version', action='version', version=f'{roop.metadata.name} {roop.metadata.version}') @@ -53,13 +55,16 @@ def parse_args() -> None: roop.globals.source_path = args.source_path roop.globals.target_path = args.target_path - roop.globals.output_path = normalize_output_path(roop.globals.source_path, roop.globals.target_path, args.output_path) + roop.globals.output_path = normalize_output_path(roop.globals.source_path, roop.globals.target_path, args.output_path) # type: ignore + roop.globals.headless = roop.globals.source_path and roop.globals.target_path and roop.globals.output_path roop.globals.frame_processors = args.frame_processor - roop.globals.headless = args.source_path or args.target_path or args.output_path roop.globals.keep_fps = args.keep_fps - roop.globals.keep_audio = args.keep_audio roop.globals.keep_frames = args.keep_frames + roop.globals.skip_audio = args.skip_audio roop.globals.many_faces = args.many_faces + roop.globals.reference_face_position = args.reference_face_position + roop.globals.reference_frame_number = args.reference_frame_number + roop.globals.similar_face_distance = args.similar_face_distance roop.globals.video_encoder = args.video_encoder roop.globals.video_quality = args.video_quality roop.globals.max_memory = args.max_memory @@ -76,22 +81,14 @@ def decode_execution_providers(execution_providers: List[str]) -> List[str]: if any(execution_provider in encoded_execution_provider for execution_provider in execution_providers)] -def suggest_max_memory() -> int: - if platform.system().lower() == 'darwin': - return 4 - return 16 - - def suggest_execution_providers() -> List[str]: return encode_execution_providers(onnxruntime.get_available_providers()) def suggest_execution_threads() -> int: - if 'DmlExecutionProvider' in roop.globals.execution_providers: - return 1 - if 'ROCMExecutionProvider' in roop.globals.execution_providers: - return 1 - return 8 + if 'CUDAExecutionProvider' in onnxruntime.get_available_providers(): + return 8 + return 1 def limit_resources() -> None: @@ -115,11 +112,6 @@ def limit_resources() -> None: resource.setrlimit(resource.RLIMIT_DATA, (memory, memory)) -def release_resources() -> None: - if 'CUDAExecutionProvider' in roop.globals.execution_providers: - torch.cuda.empty_cache() - - def pre_check() -> bool: if sys.version_info < (3, 9): update_status('Python version is not supported - please upgrade to 3.9 or higher.') @@ -145,11 +137,12 @@ def start() -> None: if predict_image(roop.globals.target_path): destroy() shutil.copy2(roop.globals.target_path, roop.globals.output_path) + # process frame for frame_processor in get_frame_processors_modules(roop.globals.frame_processors): update_status('Progressing...', frame_processor.NAME) frame_processor.process_image(roop.globals.source_path, roop.globals.output_path, roop.globals.output_path) frame_processor.post_process() - release_resources() + # validate image if is_image(roop.globals.target_path): update_status('Processing to image succeed!') else: @@ -160,34 +153,41 @@ def start() -> None: destroy() update_status('Creating temp resources...') create_temp(roop.globals.target_path) - update_status('Extracting frames...') - extract_frames(roop.globals.target_path) + # extract frames + if roop.globals.keep_fps: + fps = detect_fps(roop.globals.target_path) + update_status(f'Extracting frames with {fps} FPS...') + extract_frames(roop.globals.target_path, fps) + else: + update_status('Extracting frames with 30 FPS...') + extract_frames(roop.globals.target_path) + # process frame temp_frame_paths = get_temp_frame_paths(roop.globals.target_path) for frame_processor in get_frame_processors_modules(roop.globals.frame_processors): update_status('Progressing...', frame_processor.NAME) frame_processor.process_video(roop.globals.source_path, temp_frame_paths) frame_processor.post_process() - release_resources() - # handles fps + # create video if roop.globals.keep_fps: - update_status('Detecting fps...') fps = detect_fps(roop.globals.target_path) - update_status(f'Creating video with {fps} fps...') + update_status(f'Creating video with {fps} FPS...') create_video(roop.globals.target_path, fps) else: - update_status('Creating video with 30.0 fps...') + update_status('Creating video with 30 FPS...') create_video(roop.globals.target_path) # handle audio - if roop.globals.keep_audio: + if roop.globals.skip_audio: + move_temp(roop.globals.target_path, roop.globals.output_path) + update_status('Skipping audio...') + else: if roop.globals.keep_fps: update_status('Restoring audio...') else: update_status('Restoring audio might cause issues as fps are not kept...') restore_audio(roop.globals.target_path, roop.globals.output_path) - else: - move_temp(roop.globals.target_path, roop.globals.output_path) - # clean and validate + # clean temp clean_temp(roop.globals.target_path) + # validate video if is_video(roop.globals.target_path): update_status('Processing to video succeed!') else: @@ -197,7 +197,7 @@ def start() -> None: def destroy() -> None: if roop.globals.target_path: clean_temp(roop.globals.target_path) - quit() + sys.exit() def run() -> None: diff --git a/roop/face_analyser.py b/roop/face_analyser.py index 9c0afe4..4c1a350 100644 --- a/roop/face_analyser.py +++ b/roop/face_analyser.py @@ -1,9 +1,10 @@ import threading -from typing import Any +from typing import Any, Optional, List import insightface +import numpy import roop.globals -from roop.typing import Frame +from roop.typing import Frame, Face FACE_ANALYSER = None THREAD_LOCK = threading.Lock() @@ -15,20 +16,38 @@ def get_face_analyser() -> Any: with THREAD_LOCK: if FACE_ANALYSER is None: FACE_ANALYSER = insightface.app.FaceAnalysis(name='buffalo_l', providers=roop.globals.execution_providers) - FACE_ANALYSER.prepare(ctx_id=0, det_size=(640, 640)) + FACE_ANALYSER.prepare(ctx_id=0) return FACE_ANALYSER -def get_one_face(frame: Frame) -> Any: - face = get_face_analyser().get(frame) +def clear_face_analyser() -> Any: + global FACE_ANALYSER + + FACE_ANALYSER = None + + +def get_one_face(frame: Frame, position: int = 0) -> Optional[Face]: + faces = get_many_faces(frame) + if faces: + try: + return faces[position] + except IndexError: + return faces[-1] + return None + + +def get_many_faces(frame: Frame) -> Optional[List[Face]]: try: - return min(face, key=lambda x: x.bbox[0]) + return get_face_analyser().get(frame) except ValueError: return None -def get_many_faces(frame: Frame) -> Any: - try: - return get_face_analyser().get(frame) - except IndexError: - return None +def find_similar_face(frame: Frame, reference_face: Face) -> Optional[Face]: + faces = get_many_faces(frame) + for face in faces: + if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'): + distance = numpy.sum(numpy.square(face.normed_embedding - reference_face.normed_embedding)) + if distance < roop.globals.similar_face_distance: + return face + return None diff --git a/roop/face_reference.py b/roop/face_reference.py new file mode 100644 index 0000000..3c3e1f1 --- /dev/null +++ b/roop/face_reference.py @@ -0,0 +1,21 @@ +from typing import Optional + +from roop.typing import Face + +FACE_REFERENCE = None + + +def get_face_reference() -> Optional[Face]: + return FACE_REFERENCE + + +def set_face_reference(face: Face) -> None: + global FACE_REFERENCE + + FACE_REFERENCE = face + + +def clear_face_reference() -> None: + global FACE_REFERENCE + + FACE_REFERENCE = None diff --git a/roop/globals.py b/roop/globals.py index 77fd391..3b8bdeb 100644 --- a/roop/globals.py +++ b/roop/globals.py @@ -3,15 +3,18 @@ from typing import List source_path = None target_path = None output_path = None +headless = None frame_processors: List[str] = [] keep_fps = None -keep_audio = None keep_frames = None +skip_audio = None many_faces = None +reference_face_position = None +reference_frame_number = None +similar_face_distance = None video_encoder = None video_quality = None max_memory = None execution_providers: List[str] = [] execution_threads = None -headless = None log_level = 'error' diff --git a/roop/metadata.py b/roop/metadata.py index 35b0f02..0f4e051 100644 --- a/roop/metadata.py +++ b/roop/metadata.py @@ -1,2 +1,2 @@ name = 'roop' -version = '1.1.0' +version = '1.2.0' diff --git a/roop/predicter.py b/roop/predictor.py similarity index 63% rename from roop/predicter.py rename to roop/predictor.py index 7ebc2b6..b59fee9 100644 --- a/roop/predicter.py +++ b/roop/predictor.py @@ -1,18 +1,36 @@ +import threading import numpy import opennsfw2 from PIL import Image +from keras import Model from roop.typing import Frame +PREDICTOR = None +THREAD_LOCK = threading.Lock() MAX_PROBABILITY = 0.85 +def get_predictor() -> Model: + global PREDICTOR + + with THREAD_LOCK: + if PREDICTOR is None: + PREDICTOR = opennsfw2.make_open_nsfw_model() + return PREDICTOR + + +def clear_predictor() -> None: + global PREDICTOR + + PREDICTOR = None + + def predict_frame(target_frame: Frame) -> bool: image = Image.fromarray(target_frame) image = opennsfw2.preprocess_image(image, opennsfw2.Preprocessing.YAHOO) - model = opennsfw2.make_open_nsfw_model() views = numpy.expand_dims(image, axis=0) - _, probability = model.predict(views)[0] + _, probability = get_predictor().predict(views)[0] return probability > MAX_PROBABILITY diff --git a/roop/processors/frame/core.py b/roop/processors/frame/core.py index c225f9d..498169d 100644 --- a/roop/processors/frame/core.py +++ b/roop/processors/frame/core.py @@ -1,4 +1,5 @@ import os +import sys import importlib import psutil from concurrent.futures import ThreadPoolExecutor, as_completed @@ -27,8 +28,10 @@ def load_frame_processor_module(frame_processor: str) -> Any: for method_name in FRAME_PROCESSORS_INTERFACE: if not hasattr(frame_processor_module, method_name): raise NotImplementedError - except (ImportError, NotImplementedError): - quit(f'Frame processor {frame_processor} crashed.') + except ModuleNotFoundError: + sys.exit(f'Frame processor {frame_processor} not found.') + except NotImplementedError: + sys.exit(f'Frame processor {frame_processor} not implemented correctly.') return frame_processor_module @@ -46,7 +49,7 @@ def multi_process_frame(source_path: str, temp_frame_paths: List[str], process_f with ThreadPoolExecutor(max_workers=roop.globals.execution_threads) as executor: futures = [] queue = create_queue(temp_frame_paths) - queue_per_future = len(temp_frame_paths) // roop.globals.execution_threads + queue_per_future = max(len(temp_frame_paths) // roop.globals.execution_threads, 1) while not queue.empty(): future = executor.submit(process_frames, source_path, pick_queue(queue, queue_per_future), update) futures.append(future) diff --git a/roop/processors/frame/face_enhancer.py b/roop/processors/frame/face_enhancer.py index 3ff92ce..7f9b0bb 100644 --- a/roop/processors/frame/face_enhancer.py +++ b/roop/processors/frame/face_enhancer.py @@ -1,12 +1,12 @@ from typing import Any, List, Callable import cv2 import threading -import gfpgan +from gfpgan.utils import GFPGANer import roop.globals import roop.processors.frame.core from roop.core import update_status -from roop.face_analyser import get_one_face +from roop.face_analyser import get_many_faces from roop.typing import Frame, Face from roop.utilities import conditional_download, resolve_relative_path, is_image, is_video @@ -22,11 +22,25 @@ def get_face_enhancer() -> Any: with THREAD_LOCK: if FACE_ENHANCER is None: model_path = resolve_relative_path('../models/GFPGANv1.4.pth') - # todo: set models path https://github.com/TencentARC/GFPGAN/issues/399 - FACE_ENHANCER = gfpgan.GFPGANer(model_path=model_path, upscale=1) # type: ignore[attr-defined] + # todo: set models path -> https://github.com/TencentARC/GFPGAN/issues/399 + FACE_ENHANCER = GFPGANer(model_path=model_path, upscale=1, device=get_device()) return FACE_ENHANCER +def get_device() -> str: + if 'CUDAExecutionProvider' in roop.globals.execution_providers: + return 'cuda' + if 'CoreMLExecutionProvider' in roop.globals.execution_providers: + return 'mps' + return 'cpu' + + +def clear_face_enhancer() -> None: + global FACE_ENHANCER + + FACE_ENHANCER = None + + def pre_check() -> bool: download_directory_path = resolve_relative_path('../models') conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/GFPGANv1.4.pth']) @@ -41,31 +55,32 @@ def pre_start() -> bool: def post_process() -> None: - global FACE_ENHANCER - - FACE_ENHANCER = None + clear_face_enhancer() -def enhance_face(temp_frame: Frame) -> Frame: +def enhance_face(target_face: Face, temp_frame: Frame) -> Frame: + start_x, start_y, end_x, end_y = map(int, target_face['bbox']) with THREAD_SEMAPHORE: - _, _, temp_frame = get_face_enhancer().enhance( - temp_frame, + _, _, temp_face = get_face_enhancer().enhance( + temp_frame[start_y:end_y, start_x:end_x], paste_back=True ) + temp_frame[start_y:end_y, start_x:end_x] = temp_face return temp_frame -def process_frame(source_face: Face, temp_frame: Frame) -> Frame: - target_face = get_one_face(temp_frame) - if target_face: - temp_frame = enhance_face(temp_frame) +def process_frame(source_face: Face, reference_face: Face, temp_frame: Frame) -> Frame: + many_faces = get_many_faces(temp_frame) + if many_faces: + for target_face in many_faces: + temp_frame = enhance_face(target_face, temp_frame) return temp_frame def process_frames(source_path: str, temp_frame_paths: List[str], update: Callable[[], None]) -> None: for temp_frame_path in temp_frame_paths: temp_frame = cv2.imread(temp_frame_path) - result = process_frame(None, temp_frame) + result = process_frame(None, None, temp_frame) cv2.imwrite(temp_frame_path, result) if update: update() @@ -73,7 +88,7 @@ def process_frames(source_path: str, temp_frame_paths: List[str], update: Callab def process_image(source_path: str, target_path: str, output_path: str) -> None: target_frame = cv2.imread(target_path) - result = process_frame(None, target_frame) + result = process_frame(None, None, target_frame) cv2.imwrite(output_path, result) diff --git a/roop/processors/frame/face_swapper.py b/roop/processors/frame/face_swapper.py index c53b5b8..3aa5257 100644 --- a/roop/processors/frame/face_swapper.py +++ b/roop/processors/frame/face_swapper.py @@ -6,7 +6,8 @@ import threading import roop.globals import roop.processors.frame.core from roop.core import update_status -from roop.face_analyser import get_one_face, get_many_faces +from roop.face_analyser import get_one_face, get_many_faces, find_similar_face +from roop.face_reference import get_face_reference, set_face_reference, clear_face_reference from roop.typing import Face, Frame from roop.utilities import conditional_download, resolve_relative_path, is_image, is_video @@ -25,6 +26,12 @@ def get_face_swapper() -> Any: return FACE_SWAPPER +def clear_face_swapper() -> None: + global FACE_SWAPPER + + FACE_SWAPPER = None + + def pre_check() -> bool: download_directory_path = resolve_relative_path('../models') conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/inswapper_128.onnx']) @@ -45,23 +52,22 @@ def pre_start() -> bool: def post_process() -> None: - global FACE_SWAPPER - - FACE_SWAPPER = None + clear_face_swapper() + clear_face_reference() def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: return get_face_swapper().get(temp_frame, target_face, source_face, paste_back=True) -def process_frame(source_face: Face, temp_frame: Frame) -> Frame: +def process_frame(source_face: Face, reference_face: Face, temp_frame: Frame) -> Frame: if roop.globals.many_faces: many_faces = get_many_faces(temp_frame) if many_faces: for target_face in many_faces: temp_frame = swap_face(source_face, target_face, temp_frame) else: - target_face = get_one_face(temp_frame) + target_face = find_similar_face(temp_frame, reference_face) if target_face: temp_frame = swap_face(source_face, target_face, temp_frame) return temp_frame @@ -69,9 +75,10 @@ def process_frame(source_face: Face, temp_frame: Frame) -> Frame: def process_frames(source_path: str, temp_frame_paths: List[str], update: Callable[[], None]) -> None: source_face = get_one_face(cv2.imread(source_path)) + reference_face = get_face_reference() for temp_frame_path in temp_frame_paths: temp_frame = cv2.imread(temp_frame_path) - result = process_frame(source_face, temp_frame) + result = process_frame(source_face, reference_face, temp_frame) cv2.imwrite(temp_frame_path, result) if update: update() @@ -80,9 +87,14 @@ def process_frames(source_path: str, temp_frame_paths: List[str], update: Callab def process_image(source_path: str, target_path: str, output_path: str) -> None: source_face = get_one_face(cv2.imread(source_path)) target_frame = cv2.imread(target_path) - result = process_frame(source_face, target_frame) + reference_face = get_one_face(target_frame, roop.globals.reference_face_position) + result = process_frame(source_face, reference_face, target_frame) cv2.imwrite(output_path, result) def process_video(source_path: str, temp_frame_paths: List[str]) -> None: + if not get_face_reference(): + reference_frame = cv2.imread(temp_frame_paths[roop.globals.reference_frame_number]) + reference_face = get_one_face(reference_frame, roop.globals.reference_face_position) + set_face_reference(reference_face) roop.processors.frame.core.process_video(source_path, temp_frame_paths, process_frames) diff --git a/roop/ui.json b/roop/ui.json index 4930991..cf63a22 100644 --- a/roop/ui.json +++ b/roop/ui.json @@ -152,6 +152,9 @@ "weight": "normal" } }, + "RoopDropArea": { + "fg_color": ["gray90", "gray13"] + }, "RoopDonate": { "text_color": ["#3a7ebf", "gray60"] } diff --git a/roop/ui.py b/roop/ui.py index ba693da..67ec32a 100644 --- a/roop/ui.py +++ b/roop/ui.py @@ -1,7 +1,9 @@ import os +import sys import webbrowser import customtkinter as ctk -from typing import Callable, Tuple +from tkinterdnd2 import TkinterDnD, DND_ALL +from typing import Any, Callable, Tuple, Optional import cv2 from PIL import Image, ImageOps @@ -9,7 +11,8 @@ import roop.globals import roop.metadata from roop.face_analyser import get_one_face from roop.capturer import get_video_frame, get_video_frame_total -from roop.predicter import predict_frame +from roop.face_reference import get_face_reference, set_face_reference, clear_face_reference +from roop.predictor import predict_frame, clear_predictor from roop.processors.frame.core import get_frame_processors_modules from roop.utilities import is_image, is_video, resolve_relative_path @@ -32,6 +35,13 @@ target_label = None status_label = None +# todo: remove by native support -> https://github.com/TomSchimansky/CustomTkinter/issues/934 +class CTk(ctk.CTk, TkinterDnD.DnDWrapper): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.TkdndVersion = TkinterDnD._require(self) + + def init(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.CTk: global ROOT, PREVIEW @@ -48,17 +58,25 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ctk.set_appearance_mode('system') ctk.set_default_color_theme(resolve_relative_path('ui.json')) - root = ctk.CTk() + root = CTk() root.minsize(ROOT_WIDTH, ROOT_HEIGHT) root.title(f'{roop.metadata.name} {roop.metadata.version}') root.configure() root.protocol('WM_DELETE_WINDOW', lambda: destroy()) - source_label = ctk.CTkLabel(root, text=None) + source_label = ctk.CTkLabel(root, text=None, fg_color=ctk.ThemeManager.theme.get('RoopDropArea').get('fg_color')) source_label.place(relx=0.1, rely=0.1, relwidth=0.3, relheight=0.25) + source_label.drop_target_register(DND_ALL) + source_label.dnd_bind('<>', lambda event: select_source_path(event.data)) + if roop.globals.source_path: + select_source_path(roop.globals.source_path) - target_label = ctk.CTkLabel(root, text=None) + target_label = ctk.CTkLabel(root, text=None, fg_color=ctk.ThemeManager.theme.get('RoopDropArea').get('fg_color')) target_label.place(relx=0.6, rely=0.1, relwidth=0.3, relheight=0.25) + target_label.drop_target_register(DND_ALL) + target_label.dnd_bind('<>', lambda event: select_target_path(event.data)) + if roop.globals.target_path: + select_target_path(roop.globals.target_path) source_button = ctk.CTkButton(root, text='Select a face', cursor='hand2', command=lambda: select_source_path()) source_button.place(relx=0.1, rely=0.4, relwidth=0.3, relheight=0.1) @@ -67,16 +85,16 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1) keep_fps_value = ctk.BooleanVar(value=roop.globals.keep_fps) - keep_fps_checkbox = ctk.CTkSwitch(root, text='Keep fps', variable=keep_fps_value, cursor='hand2', command=lambda: setattr(roop.globals, 'keep_fps', not roop.globals.keep_fps)) + keep_fps_checkbox = ctk.CTkSwitch(root, text='Keep target fps', variable=keep_fps_value, cursor='hand2', command=lambda: setattr(roop.globals, 'keep_fps', not roop.globals.keep_fps)) keep_fps_checkbox.place(relx=0.1, rely=0.6) keep_frames_value = ctk.BooleanVar(value=roop.globals.keep_frames) - keep_frames_switch = ctk.CTkSwitch(root, text='Keep frames', variable=keep_frames_value, cursor='hand2', command=lambda: setattr(roop.globals, 'keep_frames', keep_frames_value.get())) + keep_frames_switch = ctk.CTkSwitch(root, text='Keep temporary frames', variable=keep_frames_value, cursor='hand2', command=lambda: setattr(roop.globals, 'keep_frames', keep_frames_value.get())) keep_frames_switch.place(relx=0.1, rely=0.65) - keep_audio_value = ctk.BooleanVar(value=roop.globals.keep_audio) - keep_audio_switch = ctk.CTkSwitch(root, text='Keep audio', variable=keep_audio_value, cursor='hand2', command=lambda: setattr(roop.globals, 'keep_audio', keep_audio_value.get())) - keep_audio_switch.place(relx=0.6, rely=0.6) + skip_audio_value = ctk.BooleanVar(value=roop.globals.skip_audio) + skip_audio_switch = ctk.CTkSwitch(root, text='Skip target audio', variable=skip_audio_value, cursor='hand2', command=lambda: setattr(roop.globals, 'skip_audio', skip_audio_value.get())) + skip_audio_switch.place(relx=0.6, rely=0.6) many_faces_value = ctk.BooleanVar(value=roop.globals.many_faces) many_faces_switch = ctk.CTkSwitch(root, text='Many faces', variable=many_faces_value, cursor='hand2', command=lambda: setattr(roop.globals, 'many_faces', many_faces_value.get())) @@ -107,7 +125,6 @@ def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel: preview = ctk.CTkToplevel(parent) preview.withdraw() - preview.title('Preview') preview.configure() preview.protocol('WM_DELETE_WINDOW', lambda: toggle_preview()) preview.resizable(width=False, height=False) @@ -117,6 +134,8 @@ def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel: preview_slider = ctk.CTkSlider(preview, from_=0, to=0, command=lambda frame_value: update_preview(frame_value)) + preview.bind('', lambda event: update_face_reference(1)) + preview.bind('', lambda event: update_face_reference(-1)) return preview @@ -125,13 +144,15 @@ def update_status(text: str) -> None: ROOT.update() -def select_source_path() -> None: +def select_source_path(source_path: Optional[str] = None) -> None: global RECENT_DIRECTORY_SOURCE - PREVIEW.withdraw() - source_path = ctk.filedialog.askopenfilename(title='select an source image', initialdir=RECENT_DIRECTORY_SOURCE) + if PREVIEW: + PREVIEW.withdraw() + if source_path is None: + source_path = ctk.filedialog.askopenfilename(title='select an source image', initialdir=RECENT_DIRECTORY_SOURCE) if is_image(source_path): - roop.globals.source_path = source_path + roop.globals.source_path = source_path # type: ignore RECENT_DIRECTORY_SOURCE = os.path.dirname(roop.globals.source_path) image = render_image_preview(roop.globals.source_path, (200, 200)) source_label.configure(image=image) @@ -140,18 +161,21 @@ def select_source_path() -> None: source_label.configure(image=None) -def select_target_path() -> None: +def select_target_path(target_path: Optional[str] = None) -> None: global RECENT_DIRECTORY_TARGET - PREVIEW.withdraw() - target_path = ctk.filedialog.askopenfilename(title='select an target image or video', initialdir=RECENT_DIRECTORY_TARGET) + if PREVIEW: + PREVIEW.withdraw() + clear_face_reference() + if target_path is None: + target_path = ctk.filedialog.askopenfilename(title='select an target image or video', initialdir=RECENT_DIRECTORY_TARGET) if is_image(target_path): - roop.globals.target_path = target_path + roop.globals.target_path = target_path # type: ignore RECENT_DIRECTORY_TARGET = os.path.dirname(roop.globals.target_path) image = render_image_preview(roop.globals.target_path, (200, 200)) target_label.configure(image=image) elif is_video(target_path): - roop.globals.target_path = target_path + roop.globals.target_path = target_path # type: ignore RECENT_DIRECTORY_TARGET = os.path.dirname(roop.globals.target_path) video_frame = render_video_preview(target_path, (200, 200)) target_label.configure(image=video_frame) @@ -198,34 +222,64 @@ def render_video_preview(video_path: str, size: Tuple[int, int], frame_number: i def toggle_preview() -> None: if PREVIEW.state() == 'normal': + PREVIEW.unbind('') + PREVIEW.unbind('') PREVIEW.withdraw() + clear_predictor() elif roop.globals.source_path and roop.globals.target_path: init_preview() - update_preview() + update_preview(roop.globals.reference_frame_number) PREVIEW.deiconify() def init_preview() -> None: + PREVIEW.title('Preview [ ↕ Reference face ]') if is_image(roop.globals.target_path): preview_slider.pack_forget() if is_video(roop.globals.target_path): video_frame_total = get_video_frame_total(roop.globals.target_path) + if video_frame_total > 0: + PREVIEW.title('Preview [ ↕ Reference face ] [ ↔ Frame number ]') + PREVIEW.bind('', lambda event: update_frame(int(video_frame_total / 20))) + PREVIEW.bind('', lambda event: update_frame(int(video_frame_total / -20))) preview_slider.configure(to=video_frame_total) preview_slider.pack(fill='x') - preview_slider.set(0) + preview_slider.set(roop.globals.reference_frame_number) def update_preview(frame_number: int = 0) -> None: if roop.globals.source_path and roop.globals.target_path: temp_frame = get_video_frame(roop.globals.target_path, frame_number) if predict_frame(temp_frame): - quit() + sys.exit() + source_face = get_one_face(cv2.imread(roop.globals.source_path)) + if not get_face_reference(): + reference_frame = get_video_frame(roop.globals.target_path, roop.globals.reference_frame_number) + reference_face = get_one_face(reference_frame, roop.globals.reference_face_position) + set_face_reference(reference_face) + else: + reference_face = get_face_reference() for frame_processor in get_frame_processors_modules(roop.globals.frame_processors): temp_frame = frame_processor.process_frame( - get_one_face(cv2.imread(roop.globals.source_path)), + source_face, + reference_face, temp_frame ) image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)) image = ImageOps.contain(image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS) image = ctk.CTkImage(image, size=image.size) preview_label.configure(image=image) + + +def update_face_reference(steps: int) -> None: + clear_face_reference() + reference_frame_number = preview_slider.get() + roop.globals.reference_face_position += steps # type: ignore + roop.globals.reference_frame_number = reference_frame_number + update_preview(reference_frame_number) + + +def update_frame(steps: int) -> None: + frame_number = preview_slider.get() + steps + preview_slider.set(frame_number) + update_preview(preview_slider.get()) diff --git a/roop/utilities.py b/roop/utilities.py index 90c8d98..c84eeb6 100644 --- a/roop/utilities.py +++ b/roop/utilities.py @@ -7,7 +7,7 @@ import ssl import subprocess import urllib from pathlib import Path -from typing import List, Any +from typing import List, Optional from tqdm import tqdm import roop.globals @@ -39,15 +39,15 @@ def detect_fps(target_path: str) -> float: return numerator / denominator except Exception: pass - return 30.0 + return 30 -def extract_frames(target_path: str) -> None: +def extract_frames(target_path: str, fps: float = 30) -> None: temp_directory_path = get_temp_directory_path(target_path) - run_ffmpeg(['-i', target_path, '-pix_fmt', 'rgb24', os.path.join(temp_directory_path, '%04d.png')]) + run_ffmpeg(['-i', target_path, '-pix_fmt', 'rgb24', '-vf', 'fps=' + str(fps), os.path.join(temp_directory_path, '%04d.png')]) -def create_video(target_path: str, fps: float = 30.0) -> None: +def create_video(target_path: str, fps: float = 30) -> None: temp_output_path = get_temp_output_path(target_path) temp_directory_path = get_temp_directory_path(target_path) run_ffmpeg(['-r', str(fps), '-i', os.path.join(temp_directory_path, '%04d.png'), '-c:v', roop.globals.video_encoder, '-crf', str(roop.globals.video_quality), '-pix_fmt', 'yuv420p', '-vf', 'colorspace=bt709:iall=bt601-6-625:fast=1', '-y', temp_output_path]) @@ -76,8 +76,8 @@ def get_temp_output_path(target_path: str) -> str: return os.path.join(temp_directory_path, TEMP_FILE) -def normalize_output_path(source_path: str, target_path: str, output_path: str) -> Any: - if source_path and target_path: +def normalize_output_path(source_path: str, target_path: str, output_path: str) -> Optional[str]: + if source_path and target_path and output_path: source_name, _ = os.path.splitext(os.path.basename(source_path)) target_name, target_extension = os.path.splitext(os.path.basename(target_path)) if os.path.isdir(output_path): @@ -131,10 +131,10 @@ def conditional_download(download_directory_path: str, urls: List[str]) -> None: for url in urls: download_file_path = os.path.join(download_directory_path, os.path.basename(url)) if not os.path.exists(download_file_path): - request = urllib.request.urlopen(url) # type: ignore[attr-defined] + request = urllib.request.urlopen(url) # type: ignore[attr-defined] total = int(request.headers.get('Content-Length', 0)) with tqdm(total=total, desc='Downloading', unit='B', unit_scale=True, unit_divisor=1024) as progress: - urllib.request.urlretrieve(url, download_file_path, reporthook=lambda count, block_size, total_size: progress.update(block_size)) # type: ignore[attr-defined] + urllib.request.urlretrieve(url, download_file_path, reporthook=lambda count, block_size, total_size: progress.update(block_size)) # type: ignore[attr-defined] def resolve_relative_path(path: str) -> str: