diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..b88a39dcf36b90aae0763caaee5e3afe0cc4159f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = tab +trim_trailing_whitespace = true diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..483099ec923006e15d56f654bbacb4fdde5699d0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +select = E3, E4, F +per-file-ignores = facefusion/core.py:E402, facefusion/installer.py:E402 diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..7f9eeea1e6b947f4eda67a13ea14febe5d664997 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +.github/preview.png filter=lfs diff=lfs merge=lfs -text +temp/target/test-1703080901.png filter=lfs diff=lfs merge=lfs -text +temp/target/test-1703080957.png filter=lfs diff=lfs merge=lfs -text +temp/target/test-1703080999.png filter=lfs diff=lfs merge=lfs -text +temp/target/test-1703081020.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..718d8a695a46024d6d04f1b183f42c1d51b02a46 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: henryruhs +custom: https://paypal.me/henryruhs diff --git a/.github/preview.png b/.github/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..8da17158a0bd718dd57bae9df7415f161b1cfc89 --- /dev/null +++ b/.github/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b390233d30eb7a755fae40fcce26aec0936c2d5ab2cb2787a97c6dd436b063 +size 1204888 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..39bf14ee1a22452f2e014c3203c1e1b26a2d31da --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: ci + +on: [ push, pull_request ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - run: pip install flake8 + - run: pip install mypy + - run: flake8 run.py facefusion tests + - run: mypy run.py facefusion tests + test: + strategy: + matrix: + os: [ macos-latest, ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up ffmpeg + uses: FedericoCarboni/setup-ffmpeg@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - run: python install.py --torch cpu --onnxruntime default + - run: pip install pytest + - run: pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..33106b3ddd19c5b3f70c30f0ed20aa09c32778ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.assets +.idea +.vscode +**/venv/ +**/__pycache__/ +**/pyvenv.cfg +**/local_cache/ +**/*.DS_Store +**/*.jpg +**/*.mp4 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..936415117a7be0cc3359ab7da17cebd8b208b60c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:20.04 + +RUN apt-get update && apt-get install -y ffmpeg + +# 安装项目依赖 +RUN pip install -r requirements.txt + +# 复制项目代码到容器 +COPY . /app + +# 设置工作目录 +WORKDIR /app + +# 启动 FaceFusion API +CMD ["python", "run.py", "--api"] \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..a93bed3fd5ec357ae9b8133d0a237eac88665da3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,3 @@ +MIT license + +Copyright (c) 2023 Henry Ruhs diff --git a/README.md b/README.md index 6623ac66ad57b9edb0f2f29fcac86b1141c5b635..6168427b248b237360e302dab69ccb3fe248ba9b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ --- -title: Facefusionapi -emoji: 📈 -colorFrom: pink -colorTo: yellow +title: facefusionapi +emoji: 🐳 +colorFrom: purple +colorTo: gray sdk: docker -pinned: false +app_port: 7860 --- - Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/facefusion/__init__.py b/facefusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/api/__init__.py b/facefusion/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/api/core.py b/facefusion/api/core.py new file mode 100644 index 0000000000000000000000000000000000000000..464a9d43d4b77693a8883cade24d3cf64ba1379d --- /dev/null +++ b/facefusion/api/core.py @@ -0,0 +1,81 @@ +import os +import base64 +import time +from fastapi import FastAPI, APIRouter, Body + +from facefusion.api.model import Params, print_globals +import facefusion.globals as globals +import facefusion.processors.frame.globals as frame_processors_globals +from facefusion import core +from facefusion.utilities import normalize_output_path + +app = FastAPI() +router = APIRouter() + +@router.post("/") +async def process_frames(params: Params = Body(...)) -> dict: + delete_files_in_directory('/workspaces/facefusion-api/facefusion/api/temp/source') + delete_files_in_directory('/workspaces/facefusion-api/facefusion/api/temp/target') + delete_files_in_directory('/workspaces/facefusion-api/facefusion/api/temp/output') + + if not (params.source or params.target): + return {"message": "Source image or path is required"} + + update_global_variables(params) + + globals.source_path = f"/workspaces/facefusion-api/facefusion/api/temp/source/{params.user_id}-{int(time.time())}.{params.source_type}" + globals.target_path = f"/workspaces/facefusion-api/facefusion/api/temp/target/{params.user_id}-{int(time.time())}.{params.target_type}" + globals.output_path = f"/workspaces/facefusion-api/facefusion/api/temp/output/{params.user_id}-{int(time.time())}.{params.target_type}" + + print(globals.output_path) + + print_globals() + + # save_file(globals.source_path, params.source) + # save_file(globals.target_path, params.target) + + try: + core.api_conditional_process() + except Exception as e: + print(e) + return {"message": "Error"} + output = image_to_base64_str(globals.output_path) + return {'output': output} + +def update_global_variables(params: Params): + for var_name, value in vars(params).items(): + if value is not None: + if hasattr(globals, var_name): + setattr(globals, var_name, value) + elif hasattr(frame_processors_globals, var_name): + setattr(frame_processors_globals, var_name, value) + +def image_to_base64_str(image_path): + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()) + return encoded_string.decode('utf-8') + +def save_file(file_path: str, encoded_image: str): + data = base64.b64decode(encoded_image) + + directory = os.path.dirname(file_path) + + if not os.path.exists(directory): + os.makedirs(directory) + + with open(file_path, "wb") as file: + file.write(data) + +def delete_files_in_directory(directory_path): + for filename in os.listdir(directory_path): + file_path = os.path.join(directory_path, filename) + if os.path.isfile(file_path): + os.remove(file_path) + print(f"Deleted {file_path}") + + +app.include_router(router) + +def launch(): + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=7860) \ No newline at end of file diff --git a/facefusion/api/model.py b/facefusion/api/model.py new file mode 100644 index 0000000000000000000000000000000000000000..dae4daa6e21f2894864c2f1d794428e6888c712a --- /dev/null +++ b/facefusion/api/model.py @@ -0,0 +1,99 @@ +from pydantic import BaseModel +from typing import Optional, List +from facefusion.typing import FaceSelectorMode, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, OutputVideoEncoder, FaceDetectorModel, FaceRecognizerModel, TempFrameFormat, Padding +from facefusion.processors.frame.typings import FaceSwapperModel, FaceEnhancerModel, FrameEnhancerModel, FaceDebuggerItem + +class Params(BaseModel): + user_id : str + source : Optional[str] + target : str + source_type: str + target_type: str + + # execution + execution_providers: Optional[List[str]] = ['CPUExecutionProvider'] + execution_thread_count: Optional[int] = 4 + execution_queue_count: Optional[int] = 1 + max_memory: Optional[int] = 0 + + # face analyser + face_analyser_order: Optional[FaceAnalyserOrder] = 'left-right' + face_analyser_age: Optional[FaceAnalyserAge] = None + face_analyser_gender: Optional[FaceAnalyserGender] = None + face_detector_model: Optional[FaceDetectorModel] = 'retinaface' + face_detector_size: Optional[str] = '640x640' + face_detector_score: Optional[float] = 0.5 + face_recognizer_model: Optional[FaceRecognizerModel] = 'arcface_inswapper' + + # face selector + face_selector_mode: Optional[FaceSelectorMode] = 'reference' + reference_face_position: Optional[int] = 0 + reference_face_distance: Optional[float] = 0.6 + reference_frame_number: Optional[int] = 0 + + # face mask + face_mask_blur: Optional[float] = 0.3 + face_mask_padding: Optional[Padding] = (0, 0, 0, 0) + + # frame extraction + trim_frame_start: Optional[int] = None + trim_frame_end: Optional[int] = None + temp_frame_format: Optional[TempFrameFormat] = 'jpg' + temp_frame_quality: Optional[int] = 100 + keep_temp: Optional[bool] = False + + # output creation + output_image_quality: Optional[int] = 80 + output_video_encoder: Optional[OutputVideoEncoder] = 'libx264' + output_video_quality: Optional[int] = 80 + keep_fps: Optional[bool] = False + skip_audio: Optional[bool] = False + + # frame processors + frame_processors: List[str] = ['face_blur'] + + face_swapper_model: Optional[FaceSwapperModel] = 'inswapper_128' + face_enhancer_model: Optional[FaceEnhancerModel] = 'gfpgan_1.4' + face_enhancer_blend: Optional[int] = 80 + frame_enhancer_model: Optional[FrameEnhancerModel] = 'real_esrgan_x2plus' + frame_enhancer_blend: Optional[int] = 80 + face_debugger_items: Optional[List[FaceDebuggerItem]] = ['kps', 'face-mask'] + + +import facefusion.globals as globals +import facefusion.processors.frame.globals as frame_processors_globals +def print_globals(): + print(f'execution_providers: {globals.execution_providers}') + print(f'execution_thread_count: {globals.execution_thread_count}') + print(f'execution_queue_count: {globals.execution_queue_count}') + print(f'max_memory: {globals.max_memory}') + print(f'face_analyser_order: {globals.face_analyser_order}') + print(f'face_analyser_age: {globals.face_analyser_age}') + print(f'face_analyser_gender: {globals.face_analyser_gender}') + print(f'face_detector_model: {globals.face_detector_model}') + print(f'face_detector_size: {globals.face_detector_size}') + print(f'face_detector_score: {globals.face_detector_score}') + print(f'face_recognizer_model: {globals.face_recognizer_model}') + print(f'face_selector_mode: {globals.face_selector_mode}') + print(f'reference_face_position: {globals.reference_face_position}') + print(f'reference_face_distance: {globals.reference_face_distance}') + print(f'reference_frame_number: {globals.reference_frame_number}') + print(f'face_mask_blur: {globals.face_mask_blur}') + print(f'face_mask_padding: {globals.face_mask_padding}') + print(f'trim_frame_start: {globals.trim_frame_start}') + print(f'trim_frame_end: {globals.trim_frame_end}') + print(f'temp_frame_format: {globals.temp_frame_format}') + print(f'temp_frame_quality: {globals.temp_frame_quality}') + print(f'keep_temp: {globals.keep_temp}') + print(f'output_image_quality: {globals.output_image_quality}') + print(f'output_video_encoder: {globals.output_video_encoder}') + print(f'output_video_quality: {globals.output_video_quality}') + print(f'keep_fps: {globals.keep_fps}') + print(f'skip_audio: {globals.skip_audio}') + print(f'frame_processors: {globals.frame_processors}') + print(f'face_swapper_model: {frame_processors_globals.face_swapper_model}') + print(f'face_enhancer_model: {frame_processors_globals.face_enhancer_model}') + print(f'face_enhancer_blend: {frame_processors_globals.face_enhancer_blend}') + print(f'frame_enhancer_model: {frame_processors_globals.frame_enhancer_model}') + print(f'frame_enhancer_blend: {frame_processors_globals.frame_enhancer_blend}') + print(f'face_debugger_items: {frame_processors_globals.face_debugger_items}') \ No newline at end of file diff --git a/facefusion/api/test/test.py b/facefusion/api/test/test.py new file mode 100644 index 0000000000000000000000000000000000000000..5d7aa882b6c823ab970d1ca4b616ced7f11fde07 --- /dev/null +++ b/facefusion/api/test/test.py @@ -0,0 +1,48 @@ +import base64 +import requests +import time + +def image_to_base64_str(image_path): + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()) + return encoded_string.decode('utf-8') + +source_image_path = 'source.jpg' +target_image_path = 'target.jpg' + +source_str = image_to_base64_str(source_image_path) +target_str = image_to_base64_str(target_image_path) + +params = { + 'user_id': 'test', + 'source': source_str, + 'target': target_str, + 'source_type': 'jpg', + 'target_type': 'jpg', + 'frame_processors': ['face_swapper','face_enhancer'],#,'face_enhancer' + # 'face_mask_blur': 0.5, + # 'face_mask_padding': [5, 5, 5, 5], + 'face_enhancer_model': 'gfpgan_1.4', + 'keep_fps': True, + "output_image_quality":100, + 'execution_thread_count': 40, + 'face_selector_mode': 'one', +} + +url = 'http://0.0.0.0:7860/' +response = requests.post(url, json=params) + +# ステータスコードとレスポンスの内容を確認 +print("Status Code:", response.status_code) +print("Response Body:") +start=time.time() +# ステータスコードが200の場合のみ処理を進める +if response.status_code == 200: + output_data = base64.b64decode(response.json()['output']) + # print("response.json()",response.json()) + with open(f'/workspaces/facefusion-api/facefusion/api/temp/output/{int(time.time())}a.jpg', 'wb') as f: + f.write(output_data) + end=time.time() + print("时间",end-start) +else: + print("Error: The request did not succeed.") diff --git a/facefusion/choices.py b/facefusion/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..cadeda67a5af3f4bda33a05d95a5e74cbebf4fd8 --- /dev/null +++ b/facefusion/choices.py @@ -0,0 +1,26 @@ +from typing import List + +import numpy + +from facefusion.typing import FaceSelectorMode, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, TempFrameFormat, OutputVideoEncoder + + +face_analyser_orders : List[FaceAnalyserOrder] = [ 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best' ] +face_analyser_ages : List[FaceAnalyserAge] = [ 'child', 'teen', 'adult', 'senior' ] +face_analyser_genders : List[FaceAnalyserGender] = [ 'male', 'female' ] +face_detector_models : List[str] = [ 'retinaface', 'yunet' ] +face_detector_sizes : List[str] = [ '160x160', '320x320', '480x480', '512x512', '640x640', '768x768', '960x960', '1024x1024' ] +face_selector_modes : List[FaceSelectorMode] = [ 'reference', 'one', 'many' ] +temp_frame_formats : List[TempFrameFormat] = [ 'jpg', 'png' ] +output_video_encoders : List[OutputVideoEncoder] = [ 'libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc' ] + +execution_thread_count_range : List[int] = numpy.arange(1, 129, 1).tolist() +execution_queue_count_range : List[int] = numpy.arange(1, 33, 1).tolist() +max_memory_range : List[int] = numpy.arange(0, 129, 1).tolist() +face_detector_score_range : List[float] = numpy.arange(0.0, 1.05, 0.05).tolist() +face_mask_blur_range : List[float] = numpy.arange(0.0, 1.05, 0.05).tolist() +face_mask_padding_range : List[float] = numpy.arange(0, 101, 1).tolist() +reference_face_distance_range : List[float] = numpy.arange(0.0, 1.55, 0.05).tolist() +temp_frame_quality_range : List[int] = numpy.arange(0, 101, 1).tolist() +output_image_quality_range : List[int] = numpy.arange(0, 101, 1).tolist() +output_video_quality_range : List[int] = numpy.arange(0, 101, 1).tolist() diff --git a/facefusion/content_analyser.py b/facefusion/content_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..2111effa0e21a9b7bce800c25b6a2312d8e0cf4f --- /dev/null +++ b/facefusion/content_analyser.py @@ -0,0 +1,102 @@ +from typing import Any, Dict +from functools import lru_cache +import threading +import cv2 +import numpy +import onnxruntime +from tqdm import tqdm + +import facefusion.globals +from facefusion import wording +from facefusion.typing import Frame, ModelValue +from facefusion.vision import get_video_frame, count_video_frame_total, read_image, detect_fps +from facefusion.utilities import resolve_relative_path, conditional_download + +CONTENT_ANALYSER = None +THREAD_LOCK : threading.Lock = threading.Lock() +MODELS : Dict[str, ModelValue] =\ +{ + 'open_nsfw': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/open_nsfw.onnx', + 'path': resolve_relative_path('../.assets/models/open_nsfw.onnx') + } +} +MAX_PROBABILITY = 0.80 +MAX_RATE = 5 +STREAM_COUNTER = 0 + + +def get_content_analyser() -> Any: + global CONTENT_ANALYSER + + with THREAD_LOCK: + if CONTENT_ANALYSER is None: + model_path = MODELS.get('open_nsfw').get('path') + CONTENT_ANALYSER = onnxruntime.InferenceSession(model_path, providers = facefusion.globals.execution_providers) + return CONTENT_ANALYSER + + +def clear_content_analyser() -> None: + global CONTENT_ANALYSER + + CONTENT_ANALYSER = None + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_url = MODELS.get('open_nsfw').get('url') + conditional_download(download_directory_path, [ model_url ]) + return True + + +def analyse_stream(frame : Frame, fps : float) -> bool: + global STREAM_COUNTER + + STREAM_COUNTER = STREAM_COUNTER + 1 + if STREAM_COUNTER % int(fps) == 0: + return analyse_frame(frame) + return False + + +def prepare_frame(frame : Frame) -> Frame: + frame = cv2.resize(frame, (224, 224)).astype(numpy.float32) + frame -= numpy.array([ 104, 117, 123 ]).astype(numpy.float32) + frame = numpy.expand_dims(frame, axis = 0) + return frame + + +def analyse_frame(frame : Frame) -> bool: + content_analyser = get_content_analyser() + frame = prepare_frame(frame) + probability = content_analyser.run(None, + { + 'input:0': frame + })[0][0][1] + return probability > MAX_PROBABILITY + + +@lru_cache(maxsize = None) +def analyse_image(image_path : str) -> bool: + frame = read_image(image_path) + return analyse_frame(frame) + + +@lru_cache(maxsize = None) +def analyse_video(video_path : str, start_frame : int, end_frame : int) -> bool: + video_frame_total = count_video_frame_total(video_path) + fps = detect_fps(video_path) + frame_range = range(start_frame or 0, end_frame or video_frame_total) + rate = 0.0 + counter = 0 + with tqdm(total = len(frame_range), desc = wording.get('analysing'), unit = 'frame', ascii = ' =') as progress: + for frame_number in frame_range: + if frame_number % int(fps) == 0: + frame = get_video_frame(video_path, frame_number) + if analyse_frame(frame): + counter += 1 + rate = counter * int(fps) / len(frame_range) * 100 + progress.update() + progress.set_postfix(rate = rate) + return rate > MAX_RATE diff --git a/facefusion/core.py b/facefusion/core.py new file mode 100644 index 0000000000000000000000000000000000000000..969905a0f7b7a6c72990d8bd9402963d0f5f5d90 --- /dev/null +++ b/facefusion/core.py @@ -0,0 +1,362 @@ +import os + +os.environ['OMP_NUM_THREADS'] = '1' + +import signal +import sys +import warnings +import platform +import shutil +import onnxruntime +from argparse import ArgumentParser, HelpFormatter + +import facefusion.choices +import facefusion.globals +from facefusion.face_analyser import get_one_face +from facefusion.face_reference import get_face_reference, set_face_reference +from facefusion.vision import get_video_frame, read_image +from facefusion import face_analyser, content_analyser, metadata, wording +from facefusion.content_analyser import analyse_image, analyse_video +from facefusion.processors.frame.core import get_frame_processors_modules, load_frame_processor_module, api_get_frame_processors_modules +from facefusion.utilities import is_image, is_video, detect_fps, compress_image, merge_video, extract_frames, get_temp_frame_paths, restore_audio, create_temp, move_temp, clear_temp, list_module_names, encode_execution_providers, decode_execution_providers, normalize_output_path, normalize_padding, create_metavar, update_status + +onnxruntime.set_default_logger_severity(3) +warnings.filterwarnings('ignore', category = UserWarning, module = 'gradio') +warnings.filterwarnings('ignore', category = UserWarning, module = 'torchvision') + + +def cli() -> None: + signal.signal(signal.SIGINT, lambda signal_number, frame: destroy()) + program = ArgumentParser(formatter_class = lambda prog: HelpFormatter(prog, max_help_position = 120), add_help = False) + # api + program.add_argument('--api', help='Run in API mode', action='store_true', dest='api_mode') + # general + program.add_argument('-s', '--source', help = wording.get('source_help'), dest = 'source_path') + program.add_argument('-t', '--target', help = wording.get('target_help'), dest = 'target_path') + program.add_argument('-o', '--output', help = wording.get('output_help'), dest = 'output_path') + program.add_argument('-v', '--version', version = metadata.get('name') + ' ' + metadata.get('version'), action = 'version') + # misc + group_misc = program.add_argument_group('misc') + group_misc.add_argument('--skip-download', help = wording.get('skip_download_help'), dest = 'skip_download', action = 'store_true') + group_misc.add_argument('--headless', help = wording.get('headless_help'), dest = 'headless', action = 'store_true') + # execution + group_execution = program.add_argument_group('execution') + group_execution.add_argument('--execution-providers', help = wording.get('execution_providers_help'), dest = 'execution_providers', default = [ 'cpu' ], choices = encode_execution_providers(onnxruntime.get_available_providers()), nargs = '+') + group_execution.add_argument('--execution-thread-count', help = wording.get('execution_thread_count_help'), dest = 'execution_thread_count', type = int, default = 4, choices = facefusion.choices.execution_thread_count_range, metavar = create_metavar(facefusion.choices.execution_thread_count_range)) + group_execution.add_argument('--execution-queue-count', help = wording.get('execution_queue_count_help'), dest = 'execution_queue_count', type = int, default = 1, choices = facefusion.choices.execution_queue_count_range, metavar = create_metavar(facefusion.choices.execution_queue_count_range)) + group_execution.add_argument('--max-memory', help = wording.get('max_memory_help'), dest = 'max_memory', type = int, choices = facefusion.choices.max_memory_range, metavar = create_metavar(facefusion.choices.max_memory_range)) + # face analyser + group_face_analyser = program.add_argument_group('face analyser') + group_face_analyser.add_argument('--face-analyser-order', help = wording.get('face_analyser_order_help'), dest = 'face_analyser_order', default = 'left-right', choices = facefusion.choices.face_analyser_orders) + group_face_analyser.add_argument('--face-analyser-age', help = wording.get('face_analyser_age_help'), dest = 'face_analyser_age', choices = facefusion.choices.face_analyser_ages) + group_face_analyser.add_argument('--face-analyser-gender', help = wording.get('face_analyser_gender_help'), dest = 'face_analyser_gender', choices = facefusion.choices.face_analyser_genders) + group_face_analyser.add_argument('--face-detector-model', help = wording.get('face_detector_model_help'), dest = 'face_detector_model', default = 'retinaface', choices = facefusion.choices.face_detector_models) + group_face_analyser.add_argument('--face-detector-size', help = wording.get('face_detector_size_help'), dest = 'face_detector_size', default = '640x640', choices = facefusion.choices.face_detector_sizes) + group_face_analyser.add_argument('--face-detector-score', help = wording.get('face_detector_score_help'), dest = 'face_detector_score', type = float, default = 0.5, choices = facefusion.choices.face_detector_score_range, metavar = create_metavar(facefusion.choices.face_detector_score_range)) + # face selector + group_face_selector = program.add_argument_group('face selector') + group_face_selector.add_argument('--face-selector-mode', help = wording.get('face_selector_mode_help'), dest = 'face_selector_mode', default = 'reference', choices = facefusion.choices.face_selector_modes) + group_face_selector.add_argument('--reference-face-position', help = wording.get('reference_face_position_help'), dest = 'reference_face_position', type = int, default = 0) + group_face_selector.add_argument('--reference-face-distance', help = wording.get('reference_face_distance_help'), dest = 'reference_face_distance', type = float, default = 0.6, choices = facefusion.choices.reference_face_distance_range, metavar = create_metavar(facefusion.choices.reference_face_distance_range)) + group_face_selector.add_argument('--reference-frame-number', help = wording.get('reference_frame_number_help'), dest = 'reference_frame_number', type = int, default = 0) + # face mask + group_face_mask = program.add_argument_group('face mask') + group_face_mask.add_argument('--face-mask-blur', help = wording.get('face_mask_blur_help'), dest = 'face_mask_blur', type = float, default = 0.3, choices = facefusion.choices.face_mask_blur_range, metavar = create_metavar(facefusion.choices.face_mask_blur_range)) + group_face_mask.add_argument('--face-mask-padding', help = wording.get('face_mask_padding_help'), dest = 'face_mask_padding', type = int, default = [ 0, 0, 0, 0 ], nargs = '+') + # frame extraction + group_frame_extraction = program.add_argument_group('frame extraction') + group_frame_extraction.add_argument('--trim-frame-start', help = wording.get('trim_frame_start_help'), dest = 'trim_frame_start', type = int) + group_frame_extraction.add_argument('--trim-frame-end', help = wording.get('trim_frame_end_help'), dest = 'trim_frame_end', type = int) + group_frame_extraction.add_argument('--temp-frame-format', help = wording.get('temp_frame_format_help'), dest = 'temp_frame_format', default = 'jpg', choices = facefusion.choices.temp_frame_formats) + group_frame_extraction.add_argument('--temp-frame-quality', help = wording.get('temp_frame_quality_help'), dest = 'temp_frame_quality', type = int, default = 100, choices = facefusion.choices.temp_frame_quality_range, metavar = create_metavar(facefusion.choices.temp_frame_quality_range)) + group_frame_extraction.add_argument('--keep-temp', help = wording.get('keep_temp_help'), dest = 'keep_temp', action = 'store_true') + # output creation + group_output_creation = program.add_argument_group('output creation') + group_output_creation.add_argument('--output-image-quality', help = wording.get('output_image_quality_help'), dest = 'output_image_quality', type = int, default = 80, choices = facefusion.choices.output_image_quality_range, metavar = create_metavar(facefusion.choices.output_image_quality_range)) + group_output_creation.add_argument('--output-video-encoder', help = wording.get('output_video_encoder_help'), dest = 'output_video_encoder', default = 'libx264', choices = facefusion.choices.output_video_encoders) + group_output_creation.add_argument('--output-video-quality', help = wording.get('output_video_quality_help'), dest = 'output_video_quality', type = int, default = 80, choices = facefusion.choices.output_video_quality_range, metavar = create_metavar(facefusion.choices.output_video_quality_range)) + group_output_creation.add_argument('--keep-fps', help = wording.get('keep_fps_help'), dest = 'keep_fps', action = 'store_true') + group_output_creation.add_argument('--skip-audio', help = wording.get('skip_audio_help'), dest = 'skip_audio', action = 'store_true') + # frame processors + available_frame_processors = list_module_names('facefusion/processors/frame/modules') + program = ArgumentParser(parents = [ program ], formatter_class = program.formatter_class, add_help = True) + group_frame_processors = program.add_argument_group('frame processors') + group_frame_processors.add_argument('--frame-processors', help = wording.get('frame_processors_help').format(choices = ', '.join(available_frame_processors)), dest = 'frame_processors', default = [ 'face_swapper' ], nargs = '+') + for frame_processor in available_frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + frame_processor_module.register_args(group_frame_processors) + # uis + group_uis = program.add_argument_group('uis') + group_uis.add_argument('--ui-layouts', help = wording.get('ui_layouts_help').format(choices = ', '.join(list_module_names('facefusion/uis/layouts'))), dest = 'ui_layouts', default = [ 'default' ], nargs = '+') + run(program) + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + # api + facefusion.globals.api_mode = args.api_mode + # general + facefusion.globals.source_path = args.source_path + facefusion.globals.target_path = args.target_path + facefusion.globals.output_path = normalize_output_path(facefusion.globals.source_path, facefusion.globals.target_path, args.output_path) + # misc + facefusion.globals.skip_download = args.skip_download + facefusion.globals.headless = args.headless + # execution + facefusion.globals.execution_providers = decode_execution_providers(args.execution_providers) + facefusion.globals.execution_thread_count = args.execution_thread_count + facefusion.globals.execution_queue_count = args.execution_queue_count + facefusion.globals.max_memory = args.max_memory + # face analyser + facefusion.globals.face_analyser_order = args.face_analyser_order + facefusion.globals.face_analyser_age = args.face_analyser_age + facefusion.globals.face_analyser_gender = args.face_analyser_gender + facefusion.globals.face_detector_model = args.face_detector_model + facefusion.globals.face_detector_size = args.face_detector_size + facefusion.globals.face_detector_score = args.face_detector_score + # face selector + facefusion.globals.face_selector_mode = args.face_selector_mode + facefusion.globals.reference_face_position = args.reference_face_position + facefusion.globals.reference_face_distance = args.reference_face_distance + facefusion.globals.reference_frame_number = args.reference_frame_number + # face mask + facefusion.globals.face_mask_blur = args.face_mask_blur + facefusion.globals.face_mask_padding = normalize_padding(args.face_mask_padding) + # frame extraction + facefusion.globals.trim_frame_start = args.trim_frame_start + facefusion.globals.trim_frame_end = args.trim_frame_end + facefusion.globals.temp_frame_format = args.temp_frame_format + facefusion.globals.temp_frame_quality = args.temp_frame_quality + facefusion.globals.keep_temp = args.keep_temp + # output creation + facefusion.globals.output_image_quality = args.output_image_quality + facefusion.globals.output_video_encoder = args.output_video_encoder + facefusion.globals.output_video_quality = args.output_video_quality + facefusion.globals.keep_fps = args.keep_fps + facefusion.globals.skip_audio = args.skip_audio + # frame processors + available_frame_processors = list_module_names('facefusion/processors/frame/modules') + # facefusion.globals.frame_processors = args.frame_processors + facefusion.globals.frame_processors = ['face_swapper','face_enhancer'] + print("有哪些参数") + for frame_processor in available_frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + frame_processor_module.apply_args(program) + # uis + facefusion.globals.ui_layouts = args.ui_layouts + + +def run(program : ArgumentParser) -> None: + apply_args(program) + limit_resources() + if not pre_check() or not content_analyser.pre_check() or not face_analyser.pre_check(): + return + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + if not frame_processor_module.pre_check(): + return + if facefusion.globals.headless: + conditional_process() + elif facefusion.globals.api_mode: + import facefusion.api.core as api + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + print("开始检查",frame_processor_module) + if not frame_processor_module.pre_check(): + return + api.launch() + else: + import facefusion.uis.core as ui + + for ui_layout in ui.get_ui_layouts_modules(facefusion.globals.ui_layouts): + if not ui_layout.pre_check(): + return + ui.launch() + + +def destroy() -> None: + if facefusion.globals.target_path: + clear_temp(facefusion.globals.target_path) + sys.exit() + + +def limit_resources() -> None: + if facefusion.globals.max_memory: + memory = facefusion.globals.max_memory * 1024 ** 3 + if platform.system().lower() == 'darwin': + memory = facefusion.globals.max_memory * 1024 ** 6 + if platform.system().lower() == 'windows': + import ctypes + kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] + kernel32.SetProcessWorkingSetSize(-1, ctypes.c_size_t(memory), ctypes.c_size_t(memory)) + else: + import resource + resource.setrlimit(resource.RLIMIT_DATA, (memory, memory)) + + +def pre_check() -> bool: + if sys.version_info < (3, 9): + update_status(wording.get('python_not_supported').format(version = '3.9')) + return False + if not shutil.which('ffmpeg'): + update_status(wording.get('ffmpeg_not_installed')) + return False + return True + + +def conditional_process() -> None: + conditional_set_face_reference() + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + if not frame_processor_module.pre_process('output'): + return + if is_image(facefusion.globals.target_path): + process_image() + if is_video(facefusion.globals.target_path): + process_video() + + +def api_conditional_process() -> None: + conditional_set_face_reference() + for frame_processor_module in api_get_frame_processors_modules(facefusion.globals.frame_processors): + if not frame_processor_module.pre_process('output'): + return + if is_image(facefusion.globals.target_path): + api_process_image() + if is_video(facefusion.globals.target_path): + api_process_video() + + +def conditional_set_face_reference() -> None: + if 'reference' in facefusion.globals.face_selector_mode and not get_face_reference(): + if is_video(facefusion.globals.target_path): + reference_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) + else: + reference_frame = read_image(facefusion.globals.target_path) + reference_face = get_one_face(reference_frame, facefusion.globals.reference_face_position) + set_face_reference(reference_face) + + +def process_image() -> None: + # if analyse_image(facefusion.globals.target_path): + # return + shutil.copy2(facefusion.globals.target_path, facefusion.globals.output_path) + # process frame + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + update_status(wording.get('processing'), frame_processor_module.NAME) + frame_processor_module.process_image(facefusion.globals.source_path, facefusion.globals.output_path, facefusion.globals.output_path) + frame_processor_module.post_process() + # compress image + update_status(wording.get('compressing_image')) + if not compress_image(facefusion.globals.output_path): + update_status(wording.get('compressing_image_failed')) + # validate image + if is_image(facefusion.globals.output_path): + update_status(wording.get('processing_image_succeed')) + else: + update_status(wording.get('processing_image_failed')) + + +def process_video() -> None: + # if analyse_video(facefusion.globals.target_path, facefusion.globals.trim_frame_start, facefusion.globals.trim_frame_end): + # return + fps = detect_fps(facefusion.globals.target_path) if facefusion.globals.keep_fps else 25.0 + # create temp + update_status(wording.get('creating_temp')) + create_temp(facefusion.globals.target_path) + # extract frames + update_status(wording.get('extracting_frames_fps').format(fps = fps)) + extract_frames(facefusion.globals.target_path, fps) + # process frame + temp_frame_paths = get_temp_frame_paths(facefusion.globals.target_path) + if temp_frame_paths: + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + update_status(wording.get('processing'), frame_processor_module.NAME) + frame_processor_module.process_video(facefusion.globals.source_path, temp_frame_paths) + frame_processor_module.post_process() + else: + update_status(wording.get('temp_frames_not_found')) + return + # merge video + update_status(wording.get('merging_video_fps').format(fps = fps)) + if not merge_video(facefusion.globals.target_path, fps): + update_status(wording.get('merging_video_failed')) + return + # handle audio + if facefusion.globals.skip_audio: + update_status(wording.get('skipping_audio')) + move_temp(facefusion.globals.target_path, facefusion.globals.output_path) + else: + update_status(wording.get('restoring_audio')) + if not restore_audio(facefusion.globals.target_path, facefusion.globals.output_path): + update_status(wording.get('restoring_audio_failed')) + move_temp(facefusion.globals.target_path, facefusion.globals.output_path) + # clear temp + update_status(wording.get('clearing_temp')) + clear_temp(facefusion.globals.target_path) + # validate video + if is_video(facefusion.globals.output_path): + update_status(wording.get('processing_video_succeed')) + else: + update_status(wording.get('processing_video_failed')) + + +def api_process_image() -> None: + # if analyse_image(facefusion.globals.target_path): + # return + shutil.copy2(facefusion.globals.target_path, facefusion.globals.output_path) + # process frame + for frame_processor_module in api_get_frame_processors_modules(facefusion.globals.frame_processors): + update_status(wording.get('processing'), frame_processor_module.NAME) + frame_processor_module.process_image(facefusion.globals.source_path, facefusion.globals.output_path, facefusion.globals.output_path) + frame_processor_module.post_process() + # compress image + update_status(wording.get('compressing_image')) + if not compress_image(facefusion.globals.output_path): + update_status(wording.get('compressing_image_failed')) + # validate image + if is_image(facefusion.globals.output_path): + update_status(wording.get('processing_image_succeed')) + else: + update_status(wording.get('processing_image_failed')) + + +def api_process_video() -> None: + # if analyse_video(facefusion.globals.target_path, facefusion.globals.trim_frame_start, facefusion.globals.trim_frame_end): + # return + fps = detect_fps(facefusion.globals.target_path) if facefusion.globals.keep_fps else 25.0 + # create temp + update_status(wording.get('creating_temp')) + create_temp(facefusion.globals.target_path) + # extract frames + update_status(wording.get('extracting_frames_fps').format(fps = fps)) + extract_frames(facefusion.globals.target_path, fps) + # process frame + temp_frame_paths = get_temp_frame_paths(facefusion.globals.target_path) + if temp_frame_paths: + for frame_processor_module in api_get_frame_processors_modules(facefusion.globals.frame_processors): + update_status(wording.get('processing'), frame_processor_module.NAME) + frame_processor_module.process_video(facefusion.globals.source_path, temp_frame_paths) + frame_processor_module.post_process() + else: + update_status(wording.get('temp_frames_not_found')) + return + # merge video + update_status(wording.get('merging_video_fps').format(fps = fps)) + if not merge_video(facefusion.globals.target_path, fps): + update_status(wording.get('merging_video_failed')) + return + # handle audio + if facefusion.globals.skip_audio: + update_status(wording.get('skipping_audio')) + move_temp(facefusion.globals.target_path, facefusion.globals.output_path) + else: + update_status(wording.get('restoring_audio')) + if not restore_audio(facefusion.globals.target_path, facefusion.globals.output_path): + update_status(wording.get('restoring_audio_failed')) + move_temp(facefusion.globals.target_path, facefusion.globals.output_path) + # clear temp + update_status(wording.get('clearing_temp')) + clear_temp(facefusion.globals.target_path) + # validate video + if is_video(facefusion.globals.output_path): + update_status(wording.get('processing_video_succeed')) + else: + update_status(wording.get('processing_video_failed')) \ No newline at end of file diff --git a/facefusion/face_analyser.py b/facefusion/face_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..97ee5f589ccd0dbd1016e2e854b2095f3c485fc5 --- /dev/null +++ b/facefusion/face_analyser.py @@ -0,0 +1,309 @@ +from typing import Any, Optional, List, Dict, Tuple +import threading +import cv2 +import numpy +import onnxruntime + +import facefusion.globals +from facefusion.face_cache import get_faces_cache, set_faces_cache +from facefusion.face_helper import warp_face, create_static_anchors, distance_to_kps, distance_to_bbox, apply_nms +from facefusion.typing import Frame, Face, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, ModelValue, Bbox, Kps, Score, Embedding +from facefusion.utilities import resolve_relative_path, conditional_download +from facefusion.vision import resize_frame_dimension + +FACE_ANALYSER = None +THREAD_SEMAPHORE : threading.Semaphore = threading.Semaphore() +THREAD_LOCK : threading.Lock = threading.Lock() +MODELS : Dict[str, ModelValue] =\ +{ + 'face_detector_retinaface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/retinaface_10g.onnx', + 'path': resolve_relative_path('../.assets/models/retinaface_10g.onnx') + }, + 'face_detector_yunet': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/yunet_2023mar.onnx', + 'path': resolve_relative_path('../.assets/models/yunet_2023mar.onnx') + }, + 'face_recognizer_arcface_blendface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + }, + 'face_recognizer_arcface_inswapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + }, + 'face_recognizer_arcface_simswap': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/arcface_simswap.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_simswap.onnx') + }, + 'gender_age': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gender_age.onnx', + 'path': resolve_relative_path('../.assets/models/gender_age.onnx') + } +} + + +def get_face_analyser() -> Any: + global FACE_ANALYSER + + with THREAD_LOCK: + if FACE_ANALYSER is None: + if facefusion.globals.face_detector_model == 'retinaface': + face_detector = onnxruntime.InferenceSession(MODELS.get('face_detector_retinaface').get('path'), providers = facefusion.globals.execution_providers) + if facefusion.globals.face_detector_model == 'yunet': + face_detector = cv2.FaceDetectorYN.create(MODELS.get('face_detector_yunet').get('path'), '', (0, 0)) + if facefusion.globals.face_recognizer_model == 'arcface_blendface': + face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_blendface').get('path'), providers = facefusion.globals.execution_providers) + if facefusion.globals.face_recognizer_model == 'arcface_inswapper': + face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_inswapper').get('path'), providers = facefusion.globals.execution_providers) + if facefusion.globals.face_recognizer_model == 'arcface_simswap': + face_recognizer = onnxruntime.InferenceSession(MODELS.get('face_recognizer_arcface_simswap').get('path'), providers = facefusion.globals.execution_providers) + gender_age = onnxruntime.InferenceSession(MODELS.get('gender_age').get('path'), providers = facefusion.globals.execution_providers) + FACE_ANALYSER =\ + { + 'face_detector': face_detector, + 'face_recognizer': face_recognizer, + 'gender_age': gender_age + } + return FACE_ANALYSER + + +def clear_face_analyser() -> Any: + global FACE_ANALYSER + + FACE_ANALYSER = None + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_urls =\ + [ + MODELS.get('face_detector_retinaface').get('url'), + MODELS.get('face_detector_yunet').get('url'), + MODELS.get('face_recognizer_arcface_inswapper').get('url'), + MODELS.get('face_recognizer_arcface_simswap').get('url'), + MODELS.get('gender_age').get('url') + ] + conditional_download(download_directory_path, model_urls) + return True + + +def extract_faces(frame: Frame) -> List[Face]: + face_detector_width, face_detector_height = map(int, facefusion.globals.face_detector_size.split('x')) + frame_height, frame_width, _ = frame.shape + temp_frame = resize_frame_dimension(frame, face_detector_width, face_detector_height) + temp_frame_height, temp_frame_width, _ = temp_frame.shape + ratio_height = frame_height / temp_frame_height + ratio_width = frame_width / temp_frame_width + if facefusion.globals.face_detector_model == 'retinaface': + bbox_list, kps_list, score_list = detect_with_retinaface(temp_frame, temp_frame_height, temp_frame_width, face_detector_height, face_detector_width, ratio_height, ratio_width) + return create_faces(frame, bbox_list, kps_list, score_list) + elif facefusion.globals.face_detector_model == 'yunet': + bbox_list, kps_list, score_list = detect_with_yunet(temp_frame, temp_frame_height, temp_frame_width, ratio_height, ratio_width) + return create_faces(frame, bbox_list, kps_list, score_list) + return [] + + +def detect_with_retinaface(temp_frame : Frame, temp_frame_height : int, temp_frame_width : int, face_detector_height : int, face_detector_width : int, ratio_height : float, ratio_width : float) -> Tuple[List[Bbox], List[Kps], List[Score]]: + face_detector = get_face_analyser().get('face_detector') + bbox_list = [] + kps_list = [] + score_list = [] + feature_strides = [ 8, 16, 32 ] + feature_map_channel = 3 + anchor_total = 2 + prepare_frame = numpy.zeros((face_detector_height, face_detector_width, 3)) + prepare_frame[:temp_frame_height, :temp_frame_width, :] = temp_frame + temp_frame = (prepare_frame - 127.5) / 128.0 + temp_frame = numpy.expand_dims(temp_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + with THREAD_SEMAPHORE: + detections = face_detector.run(None, + { + face_detector.get_inputs()[0].name: temp_frame + }) + for index, feature_stride in enumerate(feature_strides): + keep_indices = numpy.where(detections[index] >= facefusion.globals.face_detector_score)[0] + if keep_indices.any(): + stride_height = face_detector_height // feature_stride + stride_width = face_detector_width // feature_stride + anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width) + bbox_raw = (detections[index + feature_map_channel] * feature_stride) + kps_raw = detections[index + feature_map_channel * 2] * feature_stride + for bbox in distance_to_bbox(anchors, bbox_raw)[keep_indices]: + bbox_list.append(numpy.array( + [ + bbox[0] * ratio_width, + bbox[1] * ratio_height, + bbox[2] * ratio_width, + bbox[3] * ratio_height + ])) + for kps in distance_to_kps(anchors, kps_raw)[keep_indices]: + kps_list.append(kps * [ ratio_width, ratio_height ]) + for score in detections[index][keep_indices]: + score_list.append(score[0]) + return bbox_list, kps_list, score_list + + +def detect_with_yunet(temp_frame : Frame, temp_frame_height : int, temp_frame_width : int, ratio_height : float, ratio_width : float) -> Tuple[List[Bbox], List[Kps], List[Score]]: + face_detector = get_face_analyser().get('face_detector') + face_detector.setInputSize((temp_frame_width, temp_frame_height)) + face_detector.setScoreThreshold(facefusion.globals.face_detector_score) + bbox_list = [] + kps_list = [] + score_list = [] + with THREAD_SEMAPHORE: + _, detections = face_detector.detect(temp_frame) + if detections.any(): + for detection in detections: + bbox_list.append(numpy.array( + [ + detection[0] * ratio_width, + detection[1] * ratio_height, + (detection[0] + detection[2]) * ratio_width, + (detection[1] + detection[3]) * ratio_height + ])) + kps_list.append(detection[4:14].reshape((5, 2)) * [ ratio_width, ratio_height]) + score_list.append(detection[14]) + return bbox_list, kps_list, score_list + + +def create_faces(frame : Frame, bbox_list : List[Bbox], kps_list : List[Kps], score_list : List[Score]) -> List[Face] : + faces : List[Face] = [] + if facefusion.globals.face_detector_score > 0: + keep_indices = apply_nms(bbox_list, 0.4) + for index in keep_indices: + bbox = bbox_list[index] + kps = kps_list[index] + score = score_list[index] + embedding, normed_embedding = calc_embedding(frame, kps) + gender, age = detect_gender_age(frame, kps) + faces.append(Face( + bbox = bbox, + kps = kps, + score = score, + embedding = embedding, + normed_embedding = normed_embedding, + gender = gender, + age = age + )) + return faces + + +def calc_embedding(temp_frame : Frame, kps : Kps) -> Tuple[Embedding, Embedding]: + face_recognizer = get_face_analyser().get('face_recognizer') + crop_frame, matrix = warp_face(temp_frame, kps, 'arcface_v2', (112, 112)) + crop_frame = crop_frame.astype(numpy.float32) / 127.5 - 1 + crop_frame = crop_frame[:, :, ::-1].transpose(2, 0, 1) + crop_frame = numpy.expand_dims(crop_frame, axis = 0) + embedding = face_recognizer.run(None, + { + face_recognizer.get_inputs()[0].name: crop_frame + })[0] + embedding = embedding.ravel() + normed_embedding = embedding / numpy.linalg.norm(embedding) + return embedding, normed_embedding + + +def detect_gender_age(frame : Frame, kps : Kps) -> Tuple[int, int]: + gender_age = get_face_analyser().get('gender_age') + crop_frame, affine_matrix = warp_face(frame, kps, 'arcface_v2', (96, 96)) + crop_frame = numpy.expand_dims(crop_frame, axis = 0).transpose(0, 3, 1, 2).astype(numpy.float32) + prediction = gender_age.run(None, + { + gender_age.get_inputs()[0].name: crop_frame + })[0][0] + gender = int(numpy.argmax(prediction[:2])) + age = int(numpy.round(prediction[2] * 100)) + return gender, age + + +def get_one_face(frame : Frame, position : int = 0) -> Optional[Face]: + many_faces = get_many_faces(frame) + if many_faces: + try: + return many_faces[position] + except IndexError: + return many_faces[-1] + return None + + +def get_many_faces(frame : Frame) -> List[Face]: + try: + faces_cache = get_faces_cache(frame) + if faces_cache: + faces = faces_cache + else: + faces = extract_faces(frame) + set_faces_cache(frame, faces) + if facefusion.globals.face_analyser_order: + faces = sort_by_order(faces, facefusion.globals.face_analyser_order) + if facefusion.globals.face_analyser_age: + faces = filter_by_age(faces, facefusion.globals.face_analyser_age) + if facefusion.globals.face_analyser_gender: + faces = filter_by_gender(faces, facefusion.globals.face_analyser_gender) + return faces + except (AttributeError, ValueError): + return [] + + +def find_similar_faces(frame : Frame, reference_face : Face, face_distance : float) -> List[Face]: + many_faces = get_many_faces(frame) + similar_faces = [] + if many_faces: + for face in many_faces: + if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'): + current_face_distance = 1 - numpy.dot(face.normed_embedding, reference_face.normed_embedding) + if current_face_distance < face_distance: + similar_faces.append(face) + return similar_faces + + +def sort_by_order(faces : List[Face], order : FaceAnalyserOrder) -> List[Face]: + if order == 'left-right': + return sorted(faces, key = lambda face: face.bbox[0]) + if order == 'right-left': + return sorted(faces, key = lambda face: face.bbox[0], reverse = True) + if order == 'top-bottom': + return sorted(faces, key = lambda face: face.bbox[1]) + if order == 'bottom-top': + return sorted(faces, key = lambda face: face.bbox[1], reverse = True) + if order == 'small-large': + return sorted(faces, key = lambda face: (face.bbox[2] - face.bbox[0]) * (face.bbox[3] - face.bbox[1])) + if order == 'large-small': + return sorted(faces, key = lambda face: (face.bbox[2] - face.bbox[0]) * (face.bbox[3] - face.bbox[1]), reverse = True) + if order == 'best-worst': + return sorted(faces, key = lambda face: face.score, reverse = True) + if order == 'worst-best': + return sorted(faces, key = lambda face: face.score) + return faces + + +def filter_by_age(faces : List[Face], age : FaceAnalyserAge) -> List[Face]: + filter_faces = [] + for face in faces: + if face.age < 13 and age == 'child': + filter_faces.append(face) + elif face.age < 19 and age == 'teen': + filter_faces.append(face) + elif face.age < 60 and age == 'adult': + filter_faces.append(face) + elif face.age > 59 and age == 'senior': + filter_faces.append(face) + return filter_faces + + +def filter_by_gender(faces : List[Face], gender : FaceAnalyserGender) -> List[Face]: + filter_faces = [] + for face in faces: + if face.gender == 0 and gender == 'female': + filter_faces.append(face) + if face.gender == 1 and gender == 'male': + filter_faces.append(face) + return filter_faces diff --git a/facefusion/face_cache.py b/facefusion/face_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..8730509a12d620ea9ba2619470addcb1e757bfac --- /dev/null +++ b/facefusion/face_cache.py @@ -0,0 +1,29 @@ +from typing import Optional, List, Dict +import hashlib + +from facefusion.typing import Frame, Face + +FACES_CACHE : Dict[str, List[Face]] = {} + + +def get_faces_cache(frame : Frame) -> Optional[List[Face]]: + frame_hash = create_frame_hash(frame) + if frame_hash in FACES_CACHE: + return FACES_CACHE[frame_hash] + return None + + +def set_faces_cache(frame : Frame, faces : List[Face]) -> None: + frame_hash = create_frame_hash(frame) + if frame_hash: + FACES_CACHE[frame_hash] = faces + + +def clear_faces_cache() -> None: + global FACES_CACHE + + FACES_CACHE = {} + + +def create_frame_hash(frame : Frame) -> Optional[str]: + return hashlib.sha1(frame.tobytes()).hexdigest() if frame.any() else None diff --git a/facefusion/face_helper.py b/facefusion/face_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..b6a4273cccee7f5ecc66e5c97fceb7ee4e821de0 --- /dev/null +++ b/facefusion/face_helper.py @@ -0,0 +1,149 @@ +from typing import Any, Dict, Tuple, List +from functools import lru_cache +from cv2.typing import Size +import cv2 +import numpy + +from facefusion.typing import Bbox, Kps, Frame, Matrix, Template, Padding + +TEMPLATES : Dict[Template, numpy.ndarray[Any, Any]] =\ +{ + 'arcface_v1': numpy.array( + [ + [ 39.7300, 51.1380 ], + [ 72.2700, 51.1380 ], + [ 56.0000, 68.4930 ], + [ 42.4630, 87.0100 ], + [ 69.5370, 87.0100 ] + ]), + 'arcface_v2': numpy.array( + [ + [ 38.2946, 51.6963 ], + [ 73.5318, 51.5014 ], + [ 56.0252, 71.7366 ], + [ 41.5493, 92.3655 ], + [ 70.7299, 92.2041 ] + ]), + 'ffhq': numpy.array( + [ + [ 192.98138, 239.94708 ], + [ 318.90277, 240.1936 ], + [ 256.63416, 314.01935 ], + [ 201.26117, 371.41043 ], + [ 313.08905, 371.15118 ] + ]) +} + + +def warp_face(temp_frame : Frame, kps : Kps, template : Template, size : Size) -> Tuple[Frame, Matrix]: + normed_template = TEMPLATES.get(template) * size[1] / size[0] + affine_matrix = cv2.estimateAffinePartial2D(kps, normed_template, method = cv2.LMEDS)[0] + crop_frame = cv2.warpAffine(temp_frame, affine_matrix, (size[1], size[1]), borderMode = cv2.BORDER_REPLICATE) + return crop_frame, affine_matrix + + +def paste_back(temp_frame : Frame, crop_frame: Frame, affine_matrix : Matrix, face_mask_blur : float, face_mask_padding : Padding) -> Frame: + inverse_matrix = cv2.invertAffineTransform(affine_matrix) + temp_frame_size = temp_frame.shape[:2][::-1] + mask_size = tuple(crop_frame.shape[:2]) + mask_frame = create_static_mask_frame(mask_size, face_mask_blur, face_mask_padding) + inverse_mask_frame = cv2.warpAffine(mask_frame, inverse_matrix, temp_frame_size).clip(0, 1) + inverse_crop_frame = cv2.warpAffine(crop_frame, inverse_matrix, temp_frame_size, borderMode = cv2.BORDER_REPLICATE) + paste_frame = temp_frame.copy() + paste_frame[:, :, 0] = inverse_mask_frame * inverse_crop_frame[:, :, 0] + (1 - inverse_mask_frame) * temp_frame[:, :, 0] + paste_frame[:, :, 1] = inverse_mask_frame * inverse_crop_frame[:, :, 1] + (1 - inverse_mask_frame) * temp_frame[:, :, 1] + paste_frame[:, :, 2] = inverse_mask_frame * inverse_crop_frame[:, :, 2] + (1 - inverse_mask_frame) * temp_frame[:, :, 2] + return paste_frame + + +def paste_back_ellipse(temp_frame : Frame, crop_frame: Frame, affine_matrix : Matrix, face_mask_blur : float, face_mask_padding : Padding) -> Frame: + inverse_matrix = cv2.invertAffineTransform(affine_matrix) + temp_frame_size = temp_frame.shape[:2][::-1] + mask_size = tuple(crop_frame.shape[:2]) + mask_frame = create_ellipse_mask_frame(mask_size, face_mask_blur, face_mask_padding) + inverse_mask_frame = cv2.warpAffine(mask_frame, inverse_matrix, temp_frame_size).clip(0, 1) + inverse_crop_frame = cv2.warpAffine(crop_frame, inverse_matrix, temp_frame_size, borderMode = cv2.BORDER_REPLICATE) + paste_frame = temp_frame.copy() + paste_frame[:, :, 0] = inverse_mask_frame * inverse_crop_frame[:, :, 0] + (1 - inverse_mask_frame) * temp_frame[:, :, 0] + paste_frame[:, :, 1] = inverse_mask_frame * inverse_crop_frame[:, :, 1] + (1 - inverse_mask_frame) * temp_frame[:, :, 1] + paste_frame[:, :, 2] = inverse_mask_frame * inverse_crop_frame[:, :, 2] + (1 - inverse_mask_frame) * temp_frame[:, :, 2] + return paste_frame + + +@lru_cache(maxsize = None) +def create_static_mask_frame(mask_size : Size, face_mask_blur : float, face_mask_padding : Padding) -> Frame: + mask_frame = numpy.ones(mask_size, numpy.float32) + blur_amount = int(mask_size[0] * 0.5 * face_mask_blur) + blur_area = max(blur_amount // 2, 1) + mask_frame[:max(blur_area, int(mask_size[1] * face_mask_padding[0] / 100)), :] = 0 + mask_frame[-max(blur_area, int(mask_size[1] * face_mask_padding[2] / 100)):, :] = 0 + mask_frame[:, :max(blur_area, int(mask_size[0] * face_mask_padding[3] / 100))] = 0 + mask_frame[:, -max(blur_area, int(mask_size[0] * face_mask_padding[1] / 100)):] = 0 + if blur_amount > 0: + mask_frame = cv2.GaussianBlur(mask_frame, (0, 0), blur_amount * 0.25) + return mask_frame + + +@lru_cache(maxsize=None) +def create_ellipse_mask_frame(mask_size: Size, face_mask_blur: float, face_mask_padding: Padding) -> Frame: + mask_frame = numpy.zeros(mask_size, numpy.float32) + center = (mask_size[1] // 2, mask_size[0] // 2) + axes = (max(1, mask_size[1] // 2 - int(mask_size[1] * face_mask_padding[1] / 100)), + max(1, mask_size[0] // 2 - int(mask_size[0] * face_mask_padding[0] / 100))) + cv2.ellipse(mask_frame, center, axes, 0, 0, 360, 1, -1) + + if face_mask_blur > 0: + blur_amount = int(mask_size[0] * 0.5 * face_mask_blur) + mask_frame = cv2.GaussianBlur(mask_frame, (0, 0), blur_amount * 0.25) + + return mask_frame + + + +@lru_cache(maxsize = None) +def create_static_anchors(feature_stride : int, anchor_total : int, stride_height : int, stride_width : int) -> numpy.ndarray[Any, Any]: + y, x = numpy.mgrid[:stride_height, :stride_width][::-1] + anchors = numpy.stack((y, x), axis = -1) + anchors = (anchors * feature_stride).reshape((-1, 2)) + anchors = numpy.stack([ anchors ] * anchor_total, axis = 1).reshape((-1, 2)) + return anchors + + +def distance_to_bbox(points : numpy.ndarray[Any, Any], distance : numpy.ndarray[Any, Any]) -> Bbox: + x1 = points[:, 0] - distance[:, 0] + y1 = points[:, 1] - distance[:, 1] + x2 = points[:, 0] + distance[:, 2] + y2 = points[:, 1] + distance[:, 3] + bbox = numpy.column_stack([ x1, y1, x2, y2 ]) + return bbox + + +def distance_to_kps(points : numpy.ndarray[Any, Any], distance : numpy.ndarray[Any, Any]) -> Kps: + x = points[:, 0::2] + distance[:, 0::2] + y = points[:, 1::2] + distance[:, 1::2] + kps = numpy.stack((x, y), axis = -1) + return kps + + +def apply_nms(bbox_list : List[Bbox], iou_threshold : float) -> List[int]: + keep_indices = [] + dimension_list = numpy.reshape(bbox_list, (-1, 4)) + x1 = dimension_list[:, 0] + y1 = dimension_list[:, 1] + x2 = dimension_list[:, 2] + y2 = dimension_list[:, 3] + areas = (x2 - x1 + 1) * (y2 - y1 + 1) + indices = numpy.arange(len(bbox_list)) + while indices.size > 0: + index = indices[0] + remain_indices = indices[1:] + keep_indices.append(index) + xx1 = numpy.maximum(x1[index], x1[remain_indices]) + yy1 = numpy.maximum(y1[index], y1[remain_indices]) + xx2 = numpy.minimum(x2[index], x2[remain_indices]) + yy2 = numpy.minimum(y2[index], y2[remain_indices]) + width = numpy.maximum(0, xx2 - xx1 + 1) + height = numpy.maximum(0, yy2 - yy1 + 1) + iou = width * height / (areas[index] + areas[remain_indices] - width * height) + indices = indices[numpy.where(iou <= iou_threshold)[0] + 1] + return keep_indices diff --git a/facefusion/face_reference.py b/facefusion/face_reference.py new file mode 100644 index 0000000000000000000000000000000000000000..72281fe6ad8dfaa7d35382011686761752077a94 --- /dev/null +++ b/facefusion/face_reference.py @@ -0,0 +1,21 @@ +from typing import Optional + +from facefusion.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/facefusion/globals.py b/facefusion/globals.py new file mode 100644 index 0000000000000000000000000000000000000000..5762fc8cc6b4352abafe04b2aa5c9e18fa040564 --- /dev/null +++ b/facefusion/globals.py @@ -0,0 +1,50 @@ +from typing import List, Optional + +from facefusion.typing import FaceSelectorMode, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, OutputVideoEncoder, FaceDetectorModel, FaceRecognizerModel, TempFrameFormat, Padding + +# api +api_mode : Optional[bool] = None +# general +source_path : Optional[str] = None +target_path : Optional[str] = None +output_path : Optional[str] = None +# misc +skip_download : Optional[bool] = None +headless : Optional[bool] = None +# execution +execution_providers : List[str] = [] +execution_thread_count : Optional[int] = None +execution_queue_count : Optional[int] = None +max_memory : Optional[int] = None +# face analyser +face_analyser_order : Optional[FaceAnalyserOrder] = None +face_analyser_age : Optional[FaceAnalyserAge] = None +face_analyser_gender : Optional[FaceAnalyserGender] = None +face_detector_model : Optional[FaceDetectorModel] = None +face_detector_size : Optional[str] = None +face_detector_score : Optional[float] = None +face_recognizer_model : Optional[FaceRecognizerModel] = None +# face selector +face_selector_mode : Optional[FaceSelectorMode] = None +reference_face_position : Optional[int] = None +reference_face_distance : Optional[float] = None +reference_frame_number : Optional[int] = None +# face mask +face_mask_blur : Optional[float] = None +face_mask_padding : Optional[Padding] = None +# frame extraction +trim_frame_start : Optional[int] = None +trim_frame_end : Optional[int] = None +temp_frame_format : Optional[TempFrameFormat] = None +temp_frame_quality : Optional[int] = None +keep_temp : Optional[bool] = None +# output creation +output_image_quality : Optional[int] = None +output_video_encoder : Optional[OutputVideoEncoder] = None +output_video_quality : Optional[int] = None +keep_fps : Optional[bool] = None +skip_audio : Optional[bool] = None +# frame processors +frame_processors : List[str] = [] +# uis +ui_layouts : List[str] = [] diff --git a/facefusion/installer.py b/facefusion/installer.py new file mode 100644 index 0000000000000000000000000000000000000000..dfd17dab4245901ea8c965bb9224e7899897beb4 --- /dev/null +++ b/facefusion/installer.py @@ -0,0 +1,63 @@ +from typing import Dict, Tuple +import subprocess +from argparse import ArgumentParser, HelpFormatter + +subprocess.call([ 'pip', 'install' , 'inquirer', '-q' ]) + +import inquirer + +from facefusion import metadata, wording + +TORCH : Dict[str, str] =\ +{ + 'default': 'default', + 'cpu': 'cpu', + 'cuda': 'cu118', + 'rocm': 'rocm5.6' +} +ONNXRUNTIMES : Dict[str, Tuple[str, str]] =\ +{ + 'default': ('onnxruntime', '1.16.3'), + 'cuda': ('onnxruntime-gpu', '1.16.3'), + 'coreml-legacy': ('onnxruntime-coreml', '1.13.1'), + 'coreml-silicon': ('onnxruntime-silicon', '1.16.0'), + 'directml': ('onnxruntime-directml', '1.16.3'), + 'openvino': ('onnxruntime-openvino', '1.16.0') +} + + +def cli() -> None: + program = ArgumentParser(formatter_class = lambda prog: HelpFormatter(prog, max_help_position = 120)) + program.add_argument('--torch', help = wording.get('install_dependency_help').format(dependency = 'torch'), dest = 'torch', choices = TORCH.keys()) + program.add_argument('--onnxruntime', help = wording.get('install_dependency_help').format(dependency = 'onnxruntime'), dest = 'onnxruntime', choices = ONNXRUNTIMES.keys()) + program.add_argument('-v', '--version', version = metadata.get('name') + ' ' + metadata.get('version'), action = 'version') + run(program) + + +def run(program : ArgumentParser) -> None: + args = program.parse_args() + + if args.torch and args.onnxruntime: + answers =\ + { + 'torch': args.torch, + 'onnxruntime': args.onnxruntime + } + else: + answers = inquirer.prompt( + [ + inquirer.List('torch', message = wording.get('install_dependency_help').format(dependency = 'torch'), choices = list(TORCH.keys())), + inquirer.List('onnxruntime', message = wording.get('install_dependency_help').format(dependency = 'onnxruntime'), choices = list(ONNXRUNTIMES.keys())) + ]) + if answers: + torch = answers['torch'] + torch_wheel = TORCH[torch] + onnxruntime = answers['onnxruntime'] + onnxruntime_name, onnxruntime_version = ONNXRUNTIMES[onnxruntime] + subprocess.call([ 'pip', 'uninstall', 'torch', '-y' ]) + if torch_wheel == 'default': + subprocess.call([ 'pip', 'install', '-r', 'requirements.txt' ]) + else: + subprocess.call([ 'pip', 'install', '-r', 'requirements.txt', '--extra-index-url', 'https://download.pytorch.org/whl/' + torch_wheel ]) + subprocess.call([ 'pip', 'uninstall', 'onnxruntime', onnxruntime_name, '-y' ]) + subprocess.call([ 'pip', 'install', onnxruntime_name + '==' + onnxruntime_version ]) diff --git a/facefusion/metadata.py b/facefusion/metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..6e167342916f90f299ca8ac4eb3c9ad2fa0dc722 --- /dev/null +++ b/facefusion/metadata.py @@ -0,0 +1,13 @@ +METADATA =\ +{ + 'name': 'FaceFusion', + 'description': 'Next generation face swapper and enhancer', + 'version': '2.0.0', + 'license': 'MIT', + 'author': 'Henry Ruhs', + 'url': 'https://facefusion.io' +} + + +def get(key : str) -> str: + return METADATA[key] diff --git a/facefusion/processors/__init__.py b/facefusion/processors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/processors/frame/__init__.py b/facefusion/processors/frame/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/processors/frame/choices.py b/facefusion/processors/frame/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..c8119d4623d3e489b9a84abfa885bea71883b86a --- /dev/null +++ b/facefusion/processors/frame/choices.py @@ -0,0 +1,13 @@ +from typing import List +import numpy + +from facefusion.processors.frame.typings import FaceSwapperModel, FaceEnhancerModel, FrameEnhancerModel, FaceDebuggerItem + +face_swapper_models : List[FaceSwapperModel] = [ 'blendface_256', 'inswapper_128', 'inswapper_128_fp16', 'simswap_256', 'simswap_512_unofficial' ] +face_enhancer_models : List[FaceEnhancerModel] = [ 'codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_256', 'gpen_bfr_512', 'restoreformer' ] +frame_enhancer_models : List[FrameEnhancerModel] = [ 'real_esrgan_x2plus', 'real_esrgan_x4plus', 'real_esrnet_x4plus' ] + +face_enhancer_blend_range : List[int] = numpy.arange(0, 101, 1).tolist() +frame_enhancer_blend_range : List[int] = numpy.arange(0, 101, 1).tolist() + +face_debugger_items : List[FaceDebuggerItem] = [ 'bbox', 'kps', 'face-mask', 'score' ] diff --git a/facefusion/processors/frame/core.py b/facefusion/processors/frame/core.py new file mode 100644 index 0000000000000000000000000000000000000000..ca6d84700cbc3d1c3190dfcb857d8cf373da2715 --- /dev/null +++ b/facefusion/processors/frame/core.py @@ -0,0 +1,105 @@ +import sys +import importlib +from concurrent.futures import ThreadPoolExecutor, as_completed +from queue import Queue +from types import ModuleType +from typing import Any, List +from tqdm import tqdm + +import facefusion.globals +from facefusion.typing import Process_Frames +from facefusion import wording +from facefusion.utilities import encode_execution_providers + +FRAME_PROCESSORS_MODULES : List[ModuleType] = [] +FRAME_PROCESSORS_METHODS =\ +[ + 'get_frame_processor', + 'clear_frame_processor', + 'get_options', + 'set_options', + 'register_args', + 'apply_args', + 'pre_check', + 'pre_process', + 'process_frame', + 'process_frames', + 'process_image', + 'process_video', + 'post_process' +] + + +def load_frame_processor_module(frame_processor : str) -> Any: + try: + frame_processor_module = importlib.import_module('facefusion.processors.frame.modules.' + frame_processor) + for method_name in FRAME_PROCESSORS_METHODS: + if not hasattr(frame_processor_module, method_name): + raise NotImplementedError + except ModuleNotFoundError: + sys.exit(wording.get('frame_processor_not_loaded').format(frame_processor = frame_processor)) + except NotImplementedError: + sys.exit(wording.get('frame_processor_not_implemented').format(frame_processor = frame_processor)) + return frame_processor_module + + +def get_frame_processors_modules(frame_processors : List[str]) -> List[ModuleType]: + global FRAME_PROCESSORS_MODULES + + if not FRAME_PROCESSORS_MODULES: + for frame_processor in frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + FRAME_PROCESSORS_MODULES.append(frame_processor_module) + return FRAME_PROCESSORS_MODULES + + +def api_get_frame_processors_modules(frame_processors: List[str]) -> List[ModuleType]: + frame_processors_modules = [] # ローカル変数を使用 + for frame_processor in frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + frame_processors_modules.append(frame_processor_module) + return frame_processors_modules + + + +def clear_frame_processors_modules() -> None: + global FRAME_PROCESSORS_MODULES + + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + frame_processor_module.clear_frame_processor() + FRAME_PROCESSORS_MODULES = [] + + +def multi_process_frames(source_path : str, temp_frame_paths : List[str], process_frames : Process_Frames) -> None: + with tqdm(total = len(temp_frame_paths), desc = wording.get('processing'), unit = 'frame', ascii = ' =') as progress: + progress.set_postfix( + { + 'execution_providers': encode_execution_providers(facefusion.globals.execution_providers), + 'execution_thread_count': facefusion.globals.execution_thread_count, + 'execution_queue_count': facefusion.globals.execution_queue_count + }) + with ThreadPoolExecutor(max_workers = facefusion.globals.execution_thread_count) as executor: + futures = [] + queue_temp_frame_paths : Queue[str] = create_queue(temp_frame_paths) + queue_per_future = max(len(temp_frame_paths) // facefusion.globals.execution_thread_count * facefusion.globals.execution_queue_count, 1) + while not queue_temp_frame_paths.empty(): + payload_temp_frame_paths = pick_queue(queue_temp_frame_paths, queue_per_future) + future = executor.submit(process_frames, source_path, payload_temp_frame_paths, progress.update) + futures.append(future) + for future_done in as_completed(futures): + future_done.result() + + +def create_queue(temp_frame_paths : List[str]) -> Queue[str]: + queue : Queue[str] = Queue() + for frame_path in temp_frame_paths: + queue.put(frame_path) + return queue + + +def pick_queue(queue : Queue[str], queue_per_future : int) -> List[str]: + queues = [] + for _ in range(queue_per_future): + if not queue.empty(): + queues.append(queue.get()) + return queues diff --git a/facefusion/processors/frame/globals.py b/facefusion/processors/frame/globals.py new file mode 100644 index 0000000000000000000000000000000000000000..526b85732dadd0b8b8eb1ad04454b75968854fc7 --- /dev/null +++ b/facefusion/processors/frame/globals.py @@ -0,0 +1,10 @@ +from typing import List, Optional + +from facefusion.processors.frame.typings import FaceSwapperModel, FaceEnhancerModel, FrameEnhancerModel, FaceDebuggerItem + +face_swapper_model : Optional[FaceSwapperModel] = None +face_enhancer_model : Optional[FaceEnhancerModel] = None +face_enhancer_blend : Optional[int] = None +frame_enhancer_model : Optional[FrameEnhancerModel] = None +frame_enhancer_blend : Optional[int] = None +face_debugger_items : Optional[List[FaceDebuggerItem]] = None diff --git a/facefusion/processors/frame/modules/__init__.py b/facefusion/processors/frame/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/processors/frame/modules/face_blur.py b/facefusion/processors/frame/modules/face_blur.py new file mode 100644 index 0000000000000000000000000000000000000000..c2a2b298a23799b5fadc0df35e7cfca3cee0bf13 --- /dev/null +++ b/facefusion/processors/frame/modules/face_blur.py @@ -0,0 +1,277 @@ +from typing import Any, List, Dict, Literal, Optional +from argparse import ArgumentParser +import threading +import numpy +import onnx +import onnxruntime +from onnx import numpy_helper +import cv2 + +import facefusion.globals +import facefusion.processors.frame.core as frame_processors +from facefusion import wording +from facefusion.face_analyser import get_one_face, get_many_faces, find_similar_faces, clear_face_analyser +from facefusion.face_helper import warp_face, paste_back_ellipse +from facefusion.face_reference import get_face_reference +from facefusion.content_analyser import clear_content_analyser +from facefusion.typing import Face, Frame, Update_Process, ProcessMode, ModelValue, OptionsWithModel, Embedding +from facefusion.utilities import conditional_download, resolve_relative_path, is_image, is_video, is_file, is_download_done, update_status +from facefusion.vision import read_image, read_static_image, write_image +from facefusion.processors.frame import globals as frame_processors_globals +from facefusion.processors.frame import choices as frame_processors_choices + +FRAME_PROCESSOR = None +MODEL_MATRIX = None +THREAD_LOCK : threading.Lock = threading.Lock() +NAME = 'FACEFUSION.FRAME_PROCESSOR.FACE_BLUR' +MODELS : Dict[str, ModelValue] =\ +{ + 'blendface_256': + { + 'type': 'blendface', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/blendface_256.onnx', + 'path': resolve_relative_path('../.assets/models/blendface_256.onnx'), + 'template': 'ffhq', + 'size': (512, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128': + { + 'type': 'inswapper', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128.onnx'), + 'template': 'arcface_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128_fp16': + { + 'type': 'inswapper', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128_fp16.onnx'), + 'template': 'arcface_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'simswap_256': + { + 'type': 'simswap', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/simswap_256.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_256.onnx'), + 'template': 'arcface_v1', + 'size': (112, 256), + 'mean': [ 0.485, 0.456, 0.406 ], + 'standard_deviation': [ 0.229, 0.224, 0.225 ] + }, + 'simswap_512_unofficial': + { + 'type': 'simswap', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/simswap_512_unofficial.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_512_unofficial.onnx'), + 'template': 'arcface_v1', + 'size': (112, 512), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + } +} +OPTIONS : Optional[OptionsWithModel] = None + + +def get_frame_processor() -> Any: + global FRAME_PROCESSOR + + with THREAD_LOCK: + if FRAME_PROCESSOR is None: + model_path = get_options('model').get('path') + FRAME_PROCESSOR = onnxruntime.InferenceSession(model_path, providers = facefusion.globals.execution_providers) + return FRAME_PROCESSOR + + +def clear_frame_processor() -> None: + global FRAME_PROCESSOR + + FRAME_PROCESSOR = None + + +def get_model_matrix() -> Any: + global MODEL_MATRIX + + with THREAD_LOCK: + if MODEL_MATRIX is None: + model_path = get_options('model').get('path') + model = onnx.load(model_path) + MODEL_MATRIX = numpy_helper.to_array(model.graph.initializer[-1]) + return MODEL_MATRIX + + +def clear_model_matrix() -> None: + global MODEL_MATRIX + + MODEL_MATRIX = None + + +def get_options(key : Literal['model']) -> Any: + global OPTIONS + + if OPTIONS is None: + OPTIONS =\ + { + 'model': MODELS[frame_processors_globals.face_swapper_model] + } + return OPTIONS.get(key) + + +def set_options(key : Literal['model'], value : Any) -> None: + global OPTIONS + + OPTIONS[key] = value + + +def register_args(program : ArgumentParser) -> None: + pass + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + frame_processors_globals.face_swapper_model = args.face_swapper_model + if args.face_swapper_model == 'blendface_256': + facefusion.globals.face_recognizer_model = 'arcface_blendface' + if args.face_swapper_model == 'inswapper_128' or args.face_swapper_model == 'inswapper_128_fp16': + facefusion.globals.face_recognizer_model = 'arcface_inswapper' + if args.face_swapper_model == 'simswap_256' or args.face_swapper_model == 'simswap_512_unofficial': + facefusion.globals.face_recognizer_model = 'arcface_simswap' + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_url = get_options('model').get('url') + conditional_download(download_directory_path, [ model_url ]) + return True + + +def pre_process(mode : ProcessMode) -> bool: + model_url = get_options('model').get('url') + model_path = get_options('model').get('path') + if not facefusion.globals.skip_download and not is_download_done(model_url, model_path): + update_status(wording.get('model_download_not_done') + wording.get('exclamation_mark'), NAME) + return False + elif not is_file(model_path): + update_status(wording.get('model_file_not_present') + wording.get('exclamation_mark'), NAME) + return False + if not is_image(facefusion.globals.source_path): + update_status(wording.get('select_image_source') + wording.get('exclamation_mark'), NAME) + return False + elif not get_one_face(read_static_image(facefusion.globals.source_path)): + update_status(wording.get('no_source_face_detected') + wording.get('exclamation_mark'), NAME) + return False + if mode in [ 'output', 'preview' ] and not is_image(facefusion.globals.target_path) and not is_video(facefusion.globals.target_path): + update_status(wording.get('select_image_or_video_target') + wording.get('exclamation_mark'), NAME) + return False + if mode == 'output' and not facefusion.globals.output_path: + update_status(wording.get('select_file_or_directory_output') + wording.get('exclamation_mark'), NAME) + return False + return True + + +def post_process() -> None: + clear_frame_processor() + clear_model_matrix() + clear_face_analyser() + clear_content_analyser() + read_static_image.cache_clear() + + +def apply_blur_to_face(target_face: Face, temp_frame: Frame) -> Frame: + print('apply_blur_to_face') + model_template = get_options('model').get('template') + model_size = get_options('model').get('size') + crop_frame, affine_matrix = warp_face(temp_frame, target_face.kps, model_template, model_size) + blurred_face = apply_blur(crop_frame) + temp_frame = paste_back_ellipse(temp_frame, blurred_face, affine_matrix, facefusion.globals.face_mask_blur, facefusion.globals.face_mask_padding) + return temp_frame + + +def apply_blur(crop_frame: Frame) -> Frame: + blurred_frame = cv2.GaussianBlur(crop_frame, (45, 45), 0) + return blurred_frame + + +def prepare_source_frame(source_face : Face) -> numpy.ndarray[Any, Any]: + source_frame = read_static_image(facefusion.globals.source_path) + source_frame, _ = warp_face(source_frame, source_face.kps, 'arcface_v2', (112, 112)) + source_frame = source_frame[:, :, ::-1] / 255.0 + source_frame = source_frame.transpose(2, 0, 1) + source_frame = numpy.expand_dims(source_frame, axis = 0).astype(numpy.float32) + return source_frame + + +def prepare_source_embedding(source_face : Face) -> Embedding: + model_type = get_options('model').get('type') + if model_type == 'inswapper': + model_matrix = get_model_matrix() + source_embedding = source_face.embedding.reshape((1, -1)) + source_embedding = numpy.dot(source_embedding, model_matrix) / numpy.linalg.norm(source_embedding) + else: + source_embedding = source_face.normed_embedding.reshape(1, -1) + return source_embedding + + +def prepare_crop_frame(crop_frame : Frame) -> Frame: + model_mean = get_options('model').get('mean') + model_standard_deviation = get_options('model').get('standard_deviation') + crop_frame = crop_frame[:, :, ::-1] / 255.0 + crop_frame = (crop_frame - model_mean) / model_standard_deviation + crop_frame = crop_frame.transpose(2, 0, 1) + crop_frame = numpy.expand_dims(crop_frame, axis = 0).astype(numpy.float32) + return crop_frame + + +def normalize_crop_frame(crop_frame : Frame) -> Frame: + crop_frame = crop_frame.transpose(1, 2, 0) + crop_frame = (crop_frame * 255.0).round() + crop_frame = crop_frame[:, :, ::-1].astype(numpy.uint8) + return crop_frame + + +def process_frame(source_face: Face, reference_face: Face, temp_frame: Frame) -> Frame: + if 'reference' in facefusion.globals.face_selector_mode: + similar_faces = find_similar_faces(temp_frame, reference_face, facefusion.globals.reference_face_distance) + if similar_faces: + for similar_face in similar_faces: + temp_frame = apply_blur_to_face(similar_face, temp_frame) + if 'one' in facefusion.globals.face_selector_mode: + target_face = get_one_face(temp_frame) + if target_face: + temp_frame = apply_blur_to_face(target_face, temp_frame) + if 'many' in facefusion.globals.face_selector_mode: + many_faces = get_many_faces(temp_frame) + if many_faces: + for target_face in many_faces: + temp_frame = apply_blur_to_face(target_face, temp_frame) + return temp_frame + + +def process_frames(source_path : str, temp_frame_paths : List[str], update_progress : Update_Process) -> None: + source_face = get_one_face(read_static_image(source_path)) + reference_face = get_face_reference() if 'reference' in facefusion.globals.face_selector_mode else None + for temp_frame_path in temp_frame_paths: + temp_frame = read_image(temp_frame_path) + result_frame = process_frame(source_face, reference_face, temp_frame) + write_image(temp_frame_path, result_frame) + update_progress() + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + source_face = get_one_face(read_static_image(source_path)) + target_frame = read_static_image(target_path) + reference_face = get_one_face(target_frame, facefusion.globals.reference_face_position) if 'reference' in facefusion.globals.face_selector_mode else None + result_frame = process_frame(source_face, reference_face, target_frame) + write_image(output_path, result_frame) + + +def process_video(source_path : str, temp_frame_paths : List[str]) -> None: + frame_processors.multi_process_frames(source_path, temp_frame_paths, process_frames) diff --git a/facefusion/processors/frame/modules/face_debugger.py b/facefusion/processors/frame/modules/face_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..75477e5da8796125d24878bb066c5ca76d834623 --- /dev/null +++ b/facefusion/processors/frame/modules/face_debugger.py @@ -0,0 +1,123 @@ +from typing import Any, List, Literal +from argparse import ArgumentParser +import cv2 +import numpy + +import facefusion.globals +import facefusion.processors.frame.core as frame_processors +from facefusion import wording +from facefusion.face_analyser import get_one_face, get_many_faces, find_similar_faces, clear_face_analyser +from facefusion.face_reference import get_face_reference +from facefusion.content_analyser import clear_content_analyser +from facefusion.typing import Face, Frame, Update_Process, ProcessMode +from facefusion.vision import read_image, read_static_image, write_image +from facefusion.face_helper import warp_face, create_static_mask_frame +from facefusion.processors.frame import globals as frame_processors_globals, choices as frame_processors_choices + +NAME = 'FACEFUSION.FRAME_PROCESSOR.FACE_DEBUGGER' + + +def get_frame_processor() -> None: + pass + + +def clear_frame_processor() -> None: + pass + + +def get_options(key : Literal['model']) -> None: + pass + + +def set_options(key : Literal['model'], value : Any) -> None: + pass + + +def register_args(program : ArgumentParser) -> None: + program.add_argument('--face-debugger-items', help = wording.get('face_debugger_items_help'), dest = 'face_debugger_items', default = [ 'kps', 'face-mask' ], choices = frame_processors_choices.face_debugger_items, nargs = '+') + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + frame_processors_globals.face_debugger_items = args.face_debugger_items + + +def pre_check() -> bool: + return True + + +def pre_process(mode : ProcessMode) -> bool: + return True + + +def post_process() -> None: + clear_frame_processor() + clear_face_analyser() + clear_content_analyser() + + +def debug_face(source_face : Face, target_face : Face, temp_frame : Frame) -> Frame: + primary_color = (0, 0, 255) + secondary_color = (0, 255, 0) + bounding_box = target_face.bbox.astype(numpy.int32) + if 'bbox' in frame_processors_globals.face_debugger_items: + cv2.rectangle(temp_frame, (bounding_box[0], bounding_box[1]), (bounding_box[2], bounding_box[3]), secondary_color, 2) + if 'face-mask' in frame_processors_globals.face_debugger_items: + crop_frame, affine_matrix = warp_face(temp_frame, target_face.kps, 'arcface_v2', (128, 128)) + inverse_matrix = cv2.invertAffineTransform(affine_matrix) + temp_frame_size = temp_frame.shape[:2][::-1] + mask_frame = create_static_mask_frame(crop_frame.shape[:2], 0, facefusion.globals.face_mask_padding) + mask_frame[mask_frame > 0] = 255 + inverse_mask_frame = cv2.warpAffine(mask_frame.astype(numpy.uint8), inverse_matrix, temp_frame_size) + inverse_mask_contours = cv2.findContours(inverse_mask_frame, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] + cv2.drawContours(temp_frame, inverse_mask_contours, 0, primary_color, 2) + if bounding_box[3] - bounding_box[1] > 60 and bounding_box[2] - bounding_box[0] > 60: + if 'kps' in frame_processors_globals.face_debugger_items: + kps = target_face.kps.astype(numpy.int32) + for index in range(kps.shape[0]): + cv2.circle(temp_frame, (kps[index][0], kps[index][1]), 3, primary_color, -1) + if 'score' in frame_processors_globals.face_debugger_items: + score_text = str(round(target_face.score, 2)) + score_position = (bounding_box[0] + 10, bounding_box[1] + 20) + cv2.putText(temp_frame, score_text, score_position, cv2.FONT_HERSHEY_SIMPLEX, 0.5, secondary_color, 2) + return temp_frame + + +def process_frame(source_face : Face, reference_face : Face, temp_frame : Frame) -> Frame: + if 'reference' in facefusion.globals.face_selector_mode: + similar_faces = find_similar_faces(temp_frame, reference_face, facefusion.globals.reference_face_distance) + if similar_faces: + for similar_face in similar_faces: + temp_frame = debug_face(source_face, similar_face, temp_frame) + if 'one' in facefusion.globals.face_selector_mode: + target_face = get_one_face(temp_frame) + if target_face: + temp_frame = debug_face(source_face, target_face, temp_frame) + if 'many' in facefusion.globals.face_selector_mode: + many_faces = get_many_faces(temp_frame) + if many_faces: + for target_face in many_faces: + temp_frame = debug_face(source_face, target_face, temp_frame) + return temp_frame + + +def process_frames(source_path : str, temp_frame_paths : List[str], update_progress : Update_Process) -> None: + source_face = get_one_face(read_static_image(source_path)) + reference_face = get_face_reference() if 'reference' in facefusion.globals.face_selector_mode else None + for temp_frame_path in temp_frame_paths: + temp_frame = read_image(temp_frame_path) + result_frame = process_frame(source_face, reference_face, temp_frame) + write_image(temp_frame_path, result_frame) + update_progress() + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + source_face = get_one_face(read_static_image(source_path)) + target_frame = read_static_image(target_path) + reference_face = get_one_face(target_frame, facefusion.globals.reference_face_position) if 'reference' in facefusion.globals.face_selector_mode else None + result_frame = process_frame(source_face, reference_face, target_frame) + write_image(output_path, result_frame) + + +def process_video(source_path : str, temp_frame_paths : List[str]) -> None: + frame_processors.multi_process_frames(source_path, temp_frame_paths, process_frames) diff --git a/facefusion/processors/frame/modules/face_enhancer.py b/facefusion/processors/frame/modules/face_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..b60b3f2dfa098bfc8a3d082bac7f4a1bb5b88315 --- /dev/null +++ b/facefusion/processors/frame/modules/face_enhancer.py @@ -0,0 +1,222 @@ +from typing import Any, List, Dict, Literal, Optional +from argparse import ArgumentParser +import cv2 +import threading +import numpy +import onnxruntime + +import facefusion.globals +import facefusion.processors.frame.core as frame_processors +from facefusion import wording +from facefusion.face_analyser import get_many_faces, clear_face_analyser +from facefusion.face_helper import warp_face, paste_back +from facefusion.content_analyser import clear_content_analyser +from facefusion.typing import Face, Frame, Update_Process, ProcessMode, ModelValue, OptionsWithModel +from facefusion.utilities import conditional_download, resolve_relative_path, is_image, is_video, is_file, is_download_done, create_metavar, update_status +from facefusion.vision import read_image, read_static_image, write_image +from facefusion.processors.frame import globals as frame_processors_globals +from facefusion.processors.frame import choices as frame_processors_choices + +FRAME_PROCESSOR = None +THREAD_SEMAPHORE : threading.Semaphore = threading.Semaphore() +THREAD_LOCK : threading.Lock = threading.Lock() +NAME = 'FACEFUSION.FRAME_PROCESSOR.FACE_ENHANCER' +MODELS : Dict[str, ModelValue] =\ +{ + 'codeformer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/codeformer.onnx', + 'path': resolve_relative_path('../.assets/models/codeformer.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + }, + 'gfpgan_1.2': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gfpgan_1.2.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.2.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + }, + 'gfpgan_1.3': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gfpgan_1.3.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.3.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + }, + 'gfpgan_1.4': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gfpgan_1.4.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.4.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + }, + 'gpen_bfr_256': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gpen_bfr_256.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_256.onnx'), + 'template': 'arcface_v2', + 'size': (128, 256) + }, + 'gpen_bfr_512': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/gpen_bfr_512.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_512.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + }, + 'restoreformer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/restoreformer.onnx', + 'path': resolve_relative_path('../.assets/models/restoreformer.onnx'), + 'template': 'ffhq', + 'size': (512, 512) + } +} +OPTIONS : Optional[OptionsWithModel] = None + + +def get_frame_processor() -> Any: + global FRAME_PROCESSOR + + with THREAD_LOCK: + if FRAME_PROCESSOR is None: + model_path = get_options('model').get('path') + FRAME_PROCESSOR = onnxruntime.InferenceSession(model_path, providers = facefusion.globals.execution_providers) + return FRAME_PROCESSOR + + +def clear_frame_processor() -> None: + global FRAME_PROCESSOR + + FRAME_PROCESSOR = None + + +def get_options(key : Literal['model']) -> Any: + global OPTIONS + + if OPTIONS is None: + OPTIONS =\ + { + 'model': MODELS[frame_processors_globals.face_enhancer_model] + } + return OPTIONS.get(key) + + +def set_options(key : Literal['model'], value : Any) -> None: + global OPTIONS + + OPTIONS[key] = value + + +def register_args(program : ArgumentParser) -> None: + program.add_argument('--face-enhancer-model', help = wording.get('frame_processor_model_help'), dest = 'face_enhancer_model', default = 'gfpgan_1.4', choices = frame_processors_choices.face_enhancer_models) + program.add_argument('--face-enhancer-blend', help = wording.get('frame_processor_blend_help'), dest = 'face_enhancer_blend', type = int, default = 80, choices = frame_processors_choices.face_enhancer_blend_range, metavar = create_metavar(frame_processors_choices.face_enhancer_blend_range)) + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + frame_processors_globals.face_enhancer_model = args.face_enhancer_model + frame_processors_globals.face_enhancer_blend = args.face_enhancer_blend + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_url = get_options('model').get('url') + print("下载文件",download_directory_path,model_url) + conditional_download(download_directory_path, [ model_url ]) + return True + + +def pre_process(mode : ProcessMode) -> bool: + model_url = get_options('model').get('url') + model_path = get_options('model').get('path') + if not facefusion.globals.skip_download and not is_download_done(model_url, model_path): + update_status(wording.get('model_download_not_done') + wording.get('exclamation_mark'), NAME) + return False + elif not is_file(model_path): + update_status(wording.get('model_file_not_present') + wording.get('exclamation_mark'), NAME) + return False + if mode in [ 'output', 'preview' ] and not is_image(facefusion.globals.target_path) and not is_video(facefusion.globals.target_path): + update_status(wording.get('select_image_or_video_target') + wording.get('exclamation_mark'), NAME) + return False + if mode == 'output' and not facefusion.globals.output_path: + update_status(wording.get('select_file_or_directory_output') + wording.get('exclamation_mark'), NAME) + return False + return True + + +def post_process() -> None: + clear_frame_processor() + clear_face_analyser() + clear_content_analyser() + read_static_image.cache_clear() + + +def enhance_face(target_face: Face, temp_frame: Frame) -> Frame: + frame_processor = get_frame_processor() + model_template = get_options('model').get('template') + model_size = get_options('model').get('size') + crop_frame, affine_matrix = warp_face(temp_frame, target_face.kps, model_template, model_size) + crop_frame = prepare_crop_frame(crop_frame) + frame_processor_inputs = {} + for frame_processor_input in frame_processor.get_inputs(): + if frame_processor_input.name == 'input': + frame_processor_inputs[frame_processor_input.name] = crop_frame + if frame_processor_input.name == 'weight': + frame_processor_inputs[frame_processor_input.name] = numpy.array([ 1 ], dtype = numpy.double) + with THREAD_SEMAPHORE: + crop_frame = frame_processor.run(None, frame_processor_inputs)[0][0] + crop_frame = normalize_crop_frame(crop_frame) + paste_frame = paste_back(temp_frame, crop_frame, affine_matrix, facefusion.globals.face_mask_blur, (0, 0, 0, 0)) + temp_frame = blend_frame(temp_frame, paste_frame) + return temp_frame + + +def prepare_crop_frame(crop_frame : Frame) -> Frame: + crop_frame = crop_frame[:, :, ::-1] / 255.0 + crop_frame = (crop_frame - 0.5) / 0.5 + crop_frame = numpy.expand_dims(crop_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return crop_frame + + +def normalize_crop_frame(crop_frame : Frame) -> Frame: + crop_frame = numpy.clip(crop_frame, -1, 1) + crop_frame = (crop_frame + 1) / 2 + crop_frame = crop_frame.transpose(1, 2, 0) + crop_frame = (crop_frame * 255.0).round() + crop_frame = crop_frame.astype(numpy.uint8)[:, :, ::-1] + return crop_frame + + +def blend_frame(temp_frame : Frame, paste_frame : Frame) -> Frame: + face_enhancer_blend = 1 - (frame_processors_globals.face_enhancer_blend / 100) + temp_frame = cv2.addWeighted(temp_frame, face_enhancer_blend, paste_frame, 1 - face_enhancer_blend, 0) + return 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_progress : Update_Process) -> None: + for temp_frame_path in temp_frame_paths: + temp_frame = read_image(temp_frame_path) + result_frame = process_frame(None, None, temp_frame) + write_image(temp_frame_path, result_frame) + update_progress() + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + target_frame = read_static_image(target_path) + result_frame = process_frame(None, None, target_frame) + write_image(output_path, result_frame) + + +def process_video(source_path : str, temp_frame_paths : List[str]) -> None: + frame_processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/frame/modules/face_swapper.py b/facefusion/processors/frame/modules/face_swapper.py new file mode 100644 index 0000000000000000000000000000000000000000..5453348a5325554a7319b71734c32b3049ed4edf --- /dev/null +++ b/facefusion/processors/frame/modules/face_swapper.py @@ -0,0 +1,283 @@ +from typing import Any, List, Dict, Literal, Optional +from argparse import ArgumentParser +import threading +import numpy +import onnx +import onnxruntime +from onnx import numpy_helper + +import facefusion.globals +import facefusion.processors.frame.core as frame_processors +from facefusion import wording +from facefusion.face_analyser import get_one_face, get_many_faces, find_similar_faces, clear_face_analyser +from facefusion.face_helper import warp_face, paste_back +from facefusion.face_reference import get_face_reference +from facefusion.content_analyser import clear_content_analyser +from facefusion.typing import Face, Frame, Update_Process, ProcessMode, ModelValue, OptionsWithModel, Embedding +from facefusion.utilities import conditional_download, resolve_relative_path, is_image, is_video, is_file, is_download_done, update_status +from facefusion.vision import read_image, read_static_image, write_image +from facefusion.processors.frame import globals as frame_processors_globals +from facefusion.processors.frame import choices as frame_processors_choices + +FRAME_PROCESSOR = None +MODEL_MATRIX = None +THREAD_LOCK : threading.Lock = threading.Lock() +NAME = 'FACEFUSION.FRAME_PROCESSOR.FACE_SWAPPER' +MODELS : Dict[str, ModelValue] =\ +{ + 'blendface_256': + { + 'type': 'blendface', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/blendface_256.onnx', + 'path': resolve_relative_path('../.assets/models/blendface_256.onnx'), + 'template': 'ffhq', + 'size': (512, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128': + { + 'type': 'inswapper', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128.onnx'), + 'template': 'arcface_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128_fp16': + { + 'type': 'inswapper', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128_fp16.onnx'), + 'template': 'arcface_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'simswap_256': + { + 'type': 'simswap', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/simswap_256.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_256.onnx'), + 'template': 'arcface_v1', + 'size': (112, 256), + 'mean': [ 0.485, 0.456, 0.406 ], + 'standard_deviation': [ 0.229, 0.224, 0.225 ] + }, + 'simswap_512_unofficial': + { + 'type': 'simswap', + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/simswap_512_unofficial.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_512_unofficial.onnx'), + 'template': 'arcface_v1', + 'size': (112, 512), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + } +} +OPTIONS : Optional[OptionsWithModel] = None + + +def get_frame_processor() -> Any: + global FRAME_PROCESSOR + + with THREAD_LOCK: + if FRAME_PROCESSOR is None: + model_path = get_options('model').get('path') + FRAME_PROCESSOR = onnxruntime.InferenceSession(model_path, providers = facefusion.globals.execution_providers) + return FRAME_PROCESSOR + + +def clear_frame_processor() -> None: + global FRAME_PROCESSOR + + FRAME_PROCESSOR = None + + +def get_model_matrix() -> Any: + global MODEL_MATRIX + + with THREAD_LOCK: + if MODEL_MATRIX is None: + model_path = get_options('model').get('path') + model = onnx.load(model_path) + MODEL_MATRIX = numpy_helper.to_array(model.graph.initializer[-1]) + return MODEL_MATRIX + + +def clear_model_matrix() -> None: + global MODEL_MATRIX + + MODEL_MATRIX = None + + +def get_options(key : Literal['model']) -> Any: + global OPTIONS + + if OPTIONS is None: + OPTIONS =\ + { + 'model': MODELS[frame_processors_globals.face_swapper_model] + } + return OPTIONS.get(key) + + +def set_options(key : Literal['model'], value : Any) -> None: + global OPTIONS + + OPTIONS[key] = value + + +def register_args(program : ArgumentParser) -> None: + program.add_argument('--face-swapper-model', help = wording.get('frame_processor_model_help'), dest = 'face_swapper_model', default = 'inswapper_128', choices = frame_processors_choices.face_swapper_models) + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + frame_processors_globals.face_swapper_model = args.face_swapper_model + if args.face_swapper_model == 'blendface_256': + facefusion.globals.face_recognizer_model = 'arcface_blendface' + if args.face_swapper_model == 'inswapper_128' or args.face_swapper_model == 'inswapper_128_fp16': + facefusion.globals.face_recognizer_model = 'arcface_inswapper' + if args.face_swapper_model == 'simswap_256' or args.face_swapper_model == 'simswap_512_unofficial': + facefusion.globals.face_recognizer_model = 'arcface_simswap' + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_url = get_options('model').get('url') + conditional_download(download_directory_path, [ model_url ]) + return True + + +def pre_process(mode : ProcessMode) -> bool: + model_url = get_options('model').get('url') + model_path = get_options('model').get('path') + if not facefusion.globals.skip_download and not is_download_done(model_url, model_path): + update_status(wording.get('model_download_not_done') + wording.get('exclamation_mark'), NAME) + return False + elif not is_file(model_path): + update_status(wording.get('model_file_not_present') + wording.get('exclamation_mark'), NAME) + return False + if not is_image(facefusion.globals.source_path): + update_status(wording.get('select_image_source') + wording.get('exclamation_mark'), NAME) + return False + elif not get_one_face(read_static_image(facefusion.globals.source_path)): + update_status(wording.get('no_source_face_detected') + wording.get('exclamation_mark'), NAME) + return False + if mode in [ 'output', 'preview' ] and not is_image(facefusion.globals.target_path) and not is_video(facefusion.globals.target_path): + update_status(wording.get('select_image_or_video_target') + wording.get('exclamation_mark'), NAME) + return False + if mode == 'output' and not facefusion.globals.output_path: + update_status(wording.get('select_file_or_directory_output') + wording.get('exclamation_mark'), NAME) + return False + return True + + +def post_process() -> None: + clear_frame_processor() + clear_model_matrix() + clear_face_analyser() + clear_content_analyser() + read_static_image.cache_clear() + + +def swap_face(source_face : Face, target_face : Face, temp_frame : Frame) -> Frame: + frame_processor = get_frame_processor() + model_template = get_options('model').get('template') + model_size = get_options('model').get('size') + model_type = get_options('model').get('type') + crop_frame, affine_matrix = warp_face(temp_frame, target_face.kps, model_template, model_size) + crop_frame = prepare_crop_frame(crop_frame) + frame_processor_inputs = {} + for frame_processor_input in frame_processor.get_inputs(): + if frame_processor_input.name == 'source': + if model_type == 'blendface': + frame_processor_inputs[frame_processor_input.name] = prepare_source_frame(source_face) + else: + frame_processor_inputs[frame_processor_input.name] = prepare_source_embedding(source_face) + if frame_processor_input.name == 'target': + frame_processor_inputs[frame_processor_input.name] = crop_frame + crop_frame = frame_processor.run(None, frame_processor_inputs)[0][0] + crop_frame = normalize_crop_frame(crop_frame) + temp_frame = paste_back(temp_frame, crop_frame, affine_matrix, facefusion.globals.face_mask_blur, facefusion.globals.face_mask_padding) + return temp_frame + + +def prepare_source_frame(source_face : Face) -> numpy.ndarray[Any, Any]: + source_frame = read_static_image(facefusion.globals.source_path) + source_frame, _ = warp_face(source_frame, source_face.kps, 'arcface_v2', (112, 112)) + source_frame = source_frame[:, :, ::-1] / 255.0 + source_frame = source_frame.transpose(2, 0, 1) + source_frame = numpy.expand_dims(source_frame, axis = 0).astype(numpy.float32) + return source_frame + + +def prepare_source_embedding(source_face : Face) -> Embedding: + model_type = get_options('model').get('type') + if model_type == 'inswapper': + model_matrix = get_model_matrix() + source_embedding = source_face.embedding.reshape((1, -1)) + source_embedding = numpy.dot(source_embedding, model_matrix) / numpy.linalg.norm(source_embedding) + else: + source_embedding = source_face.normed_embedding.reshape(1, -1) + return source_embedding + + +def prepare_crop_frame(crop_frame : Frame) -> Frame: + model_mean = get_options('model').get('mean') + model_standard_deviation = get_options('model').get('standard_deviation') + crop_frame = crop_frame[:, :, ::-1] / 255.0 + crop_frame = (crop_frame - model_mean) / model_standard_deviation + crop_frame = crop_frame.transpose(2, 0, 1) + crop_frame = numpy.expand_dims(crop_frame, axis = 0).astype(numpy.float32) + return crop_frame + + +def normalize_crop_frame(crop_frame : Frame) -> Frame: + crop_frame = crop_frame.transpose(1, 2, 0) + crop_frame = (crop_frame * 255.0).round() + crop_frame = crop_frame[:, :, ::-1].astype(numpy.uint8) + return crop_frame + + +def process_frame(source_face : Face, reference_face : Face, temp_frame : Frame) -> Frame: + if 'reference' in facefusion.globals.face_selector_mode: + similar_faces = find_similar_faces(temp_frame, reference_face, facefusion.globals.reference_face_distance) + if similar_faces: + for similar_face in similar_faces: + temp_frame = swap_face(source_face, similar_face, temp_frame) + if 'one' in facefusion.globals.face_selector_mode: + target_face = get_one_face(temp_frame) + if target_face: + temp_frame = swap_face(source_face, target_face, temp_frame) + if 'many' in facefusion.globals.face_selector_mode: + 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) + return temp_frame + + +def process_frames(source_path : str, temp_frame_paths : List[str], update_progress : Update_Process) -> None: + source_face = get_one_face(read_static_image(source_path)) + reference_face = get_face_reference() if 'reference' in facefusion.globals.face_selector_mode else None + for temp_frame_path in temp_frame_paths: + temp_frame = read_image(temp_frame_path) + result_frame = process_frame(source_face, reference_face, temp_frame) + write_image(temp_frame_path, result_frame) + update_progress() + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + source_face = get_one_face(read_static_image(source_path)) + target_frame = read_static_image(target_path) + reference_face = get_one_face(target_frame, facefusion.globals.reference_face_position) if 'reference' in facefusion.globals.face_selector_mode else None + result_frame = process_frame(source_face, reference_face, target_frame) + write_image(output_path, result_frame) + + +def process_video(source_path : str, temp_frame_paths : List[str]) -> None: + frame_processors.multi_process_frames(source_path, temp_frame_paths, process_frames) diff --git a/facefusion/processors/frame/modules/frame_enhancer.py b/facefusion/processors/frame/modules/frame_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..c2194e6d2cababa4ba7e3719f0bd295d7290f9ed --- /dev/null +++ b/facefusion/processors/frame/modules/frame_enhancer.py @@ -0,0 +1,165 @@ +from typing import Any, List, Dict, Literal, Optional +from argparse import ArgumentParser +import threading +import cv2 +from basicsr.archs.rrdbnet_arch import RRDBNet +from realesrgan import RealESRGANer + +import facefusion.globals +import facefusion.processors.frame.core as frame_processors +from facefusion import wording +from facefusion.face_analyser import clear_face_analyser +from facefusion.content_analyser import clear_content_analyser +from facefusion.typing import Frame, Face, Update_Process, ProcessMode, ModelValue, OptionsWithModel +from facefusion.utilities import conditional_download, resolve_relative_path, is_file, is_download_done, map_device, create_metavar, update_status +from facefusion.vision import read_image, read_static_image, write_image +from facefusion.processors.frame import globals as frame_processors_globals +from facefusion.processors.frame import choices as frame_processors_choices + +FRAME_PROCESSOR = None +THREAD_SEMAPHORE : threading.Semaphore = threading.Semaphore() +THREAD_LOCK : threading.Lock = threading.Lock() +NAME = 'FACEFUSION.FRAME_PROCESSOR.FRAME_ENHANCER' +MODELS: Dict[str, ModelValue] =\ +{ + 'real_esrgan_x2plus': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/real_esrgan_x2plus.pth', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x2plus.pth'), + 'scale': 2 + }, + 'real_esrgan_x4plus': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/real_esrgan_x4plus.pth', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x4plus.pth'), + 'scale': 4 + }, + 'real_esrnet_x4plus': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/real_esrnet_x4plus.pth', + 'path': resolve_relative_path('../.assets/models/real_esrnet_x4plus.pth'), + 'scale': 4 + } +} +OPTIONS : Optional[OptionsWithModel] = None + + +def get_frame_processor() -> Any: + global FRAME_PROCESSOR + + with THREAD_LOCK: + if FRAME_PROCESSOR is None: + model_path = get_options('model').get('path') + model_scale = get_options('model').get('scale') + FRAME_PROCESSOR = RealESRGANer( + model_path = model_path, + model = RRDBNet( + num_in_ch = 3, + num_out_ch = 3, + scale = model_scale + ), + device = map_device(facefusion.globals.execution_providers), + scale = model_scale + ) + return FRAME_PROCESSOR + + +def clear_frame_processor() -> None: + global FRAME_PROCESSOR + + FRAME_PROCESSOR = None + + +def get_options(key : Literal['model']) -> Any: + global OPTIONS + + if OPTIONS is None: + OPTIONS =\ + { + 'model': MODELS[frame_processors_globals.frame_enhancer_model] + } + return OPTIONS.get(key) + + +def set_options(key : Literal['model'], value : Any) -> None: + global OPTIONS + + OPTIONS[key] = value + + +def register_args(program : ArgumentParser) -> None: + program.add_argument('--frame-enhancer-model', help = wording.get('frame_processor_model_help'), dest = 'frame_enhancer_model', default = 'real_esrgan_x2plus', choices = frame_processors_choices.frame_enhancer_models) + program.add_argument('--frame-enhancer-blend', help = wording.get('frame_processor_blend_help'), dest = 'frame_enhancer_blend', type = int, default = 80, choices = frame_processors_choices.frame_enhancer_blend_range, metavar = create_metavar(frame_processors_choices.frame_enhancer_blend_range)) + + +def apply_args(program : ArgumentParser) -> None: + args = program.parse_args() + frame_processors_globals.frame_enhancer_model = args.frame_enhancer_model + frame_processors_globals.frame_enhancer_blend = args.frame_enhancer_blend + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + download_directory_path = resolve_relative_path('../.assets/models') + model_url = get_options('model').get('url') + conditional_download(download_directory_path, [ model_url ]) + return True + + +def pre_process(mode : ProcessMode) -> bool: + model_url = get_options('model').get('url') + model_path = get_options('model').get('path') + if not facefusion.globals.skip_download and not is_download_done(model_url, model_path): + update_status(wording.get('model_download_not_done') + wording.get('exclamation_mark'), NAME) + return False + elif not is_file(model_path): + update_status(wording.get('model_file_not_present') + wording.get('exclamation_mark'), NAME) + return False + if mode == 'output' and not facefusion.globals.output_path: + update_status(wording.get('select_file_or_directory_output') + wording.get('exclamation_mark'), NAME) + return False + return True + + +def post_process() -> None: + clear_frame_processor() + clear_face_analyser() + clear_content_analyser() + read_static_image.cache_clear() + + +def enhance_frame(temp_frame : Frame) -> Frame: + with THREAD_SEMAPHORE: + paste_frame, _ = get_frame_processor().enhance(temp_frame) + temp_frame = blend_frame(temp_frame, paste_frame) + return temp_frame + + +def blend_frame(temp_frame : Frame, paste_frame : Frame) -> Frame: + frame_enhancer_blend = 1 - (frame_processors_globals.frame_enhancer_blend / 100) + paste_frame_height, paste_frame_width = paste_frame.shape[0:2] + temp_frame = cv2.resize(temp_frame, (paste_frame_width, paste_frame_height)) + temp_frame = cv2.addWeighted(temp_frame, frame_enhancer_blend, paste_frame, 1 - frame_enhancer_blend, 0) + return temp_frame + + +def process_frame(source_face : Face, reference_face : Face, temp_frame : Frame) -> Frame: + return enhance_frame(temp_frame) + + +def process_frames(source_path : str, temp_frame_paths : List[str], update_progress : Update_Process) -> None: + for temp_frame_path in temp_frame_paths: + temp_frame = read_image(temp_frame_path) + result_frame = process_frame(None, None, temp_frame) + write_image(temp_frame_path, result_frame) + update_progress() + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + target_frame = read_static_image(target_path) + result = process_frame(None, None, target_frame) + write_image(output_path, result) + + +def process_video(source_path : str, temp_frame_paths : List[str]) -> None: + frame_processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/frame/typings.py b/facefusion/processors/frame/typings.py new file mode 100644 index 0000000000000000000000000000000000000000..7323188a25445bad8bf21c0aceb5273166cb110e --- /dev/null +++ b/facefusion/processors/frame/typings.py @@ -0,0 +1,7 @@ +from typing import Literal + +FaceSwapperModel = Literal['blendface_256', 'inswapper_128', 'inswapper_128_fp16', 'simswap_256', 'simswap_512_unofficial'] +FaceEnhancerModel = Literal['codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_256', 'gpen_bfr_512', 'restoreformer'] +FrameEnhancerModel = Literal['real_esrgan_x2plus', 'real_esrgan_x4plus', 'real_esrnet_x4plus'] + +FaceDebuggerItem = Literal['bbox', 'kps', 'face-mask', 'score'] diff --git a/facefusion/typing.py b/facefusion/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..64a24e8ede703ce7d71b708e6331426b17b5fe3b --- /dev/null +++ b/facefusion/typing.py @@ -0,0 +1,41 @@ +from collections import namedtuple +from typing import Any, Literal, Callable, List, Tuple, Dict, TypedDict +import numpy + +Bbox = numpy.ndarray[Any, Any] +Kps = numpy.ndarray[Any, Any] +Score = float +Embedding = numpy.ndarray[Any, Any] +Face = namedtuple('Face', +[ + 'bbox', + 'kps', + 'score', + 'embedding', + 'normed_embedding', + 'gender', + 'age' +]) +Frame = numpy.ndarray[Any, Any] +Matrix = numpy.ndarray[Any, Any] +Padding = Tuple[int, int, int, int] + +Update_Process = Callable[[], None] +Process_Frames = Callable[[str, List[str], Update_Process], None] + +Template = Literal['arcface_v1', 'arcface_v2', 'ffhq'] +ProcessMode = Literal['output', 'preview', 'stream'] +FaceSelectorMode = Literal['reference', 'one', 'many'] +FaceAnalyserOrder = Literal['left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best'] +FaceAnalyserAge = Literal['child', 'teen', 'adult', 'senior'] +FaceAnalyserGender = Literal['male', 'female'] +FaceDetectorModel = Literal['retinaface', 'yunet'] +FaceRecognizerModel = Literal['arcface_blendface', 'arcface_inswapper', 'arcface_simswap'] +TempFrameFormat = Literal['jpg', 'png'] +OutputVideoEncoder = Literal['libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc'] + +ModelValue = Dict[str, Any] +OptionsWithModel = TypedDict('OptionsWithModel', +{ + 'model' : ModelValue +}) diff --git a/facefusion/uis/__init__.py b/facefusion/uis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/uis/assets/fixes.css b/facefusion/uis/assets/fixes.css new file mode 100644 index 0000000000000000000000000000000000000000..f65a7cfd3e3e34111a09a9100c6714ff49558615 --- /dev/null +++ b/facefusion/uis/assets/fixes.css @@ -0,0 +1,7 @@ +:root:root:root button:not([class]) +{ + border-radius: 0.375rem; + float: left; + overflow: hidden; + width: 100%; +} diff --git a/facefusion/uis/assets/overrides.css b/facefusion/uis/assets/overrides.css new file mode 100644 index 0000000000000000000000000000000000000000..86ca371d5a146c1a28e5bc188f3771b6ebc1d263 --- /dev/null +++ b/facefusion/uis/assets/overrides.css @@ -0,0 +1,44 @@ +:root:root:root input[type="number"] +{ + max-width: 6rem; +} + +:root:root:root [type="checkbox"], +:root:root:root [type="radio"] +{ + border-radius: 50%; + height: 1.125rem; + width: 1.125rem; +} + +:root:root:root input[type="range"] +{ + height: 0.5rem; +} + +:root:root:root input[type="range"]::-moz-range-thumb, +:root:root:root input[type="range"]::-webkit-slider-thumb +{ + background: var(--neutral-300); + border: unset; + border-radius: 50%; + height: 1.125rem; + width: 1.125rem; +} + +:root:root:root input[type="range"]::-webkit-slider-thumb +{ + margin-top: 0.375rem; +} + +:root:root:root .grid-wrap.fixed-height +{ + min-height: unset; +} + +:root:root:root .grid-container +{ + grid-auto-rows: minmax(5em, 1fr); + grid-template-columns: repeat(var(--grid-cols), minmax(5em, 1fr)); + grid-template-rows: repeat(var(--grid-rows), minmax(5em, 1fr)); +} diff --git a/facefusion/uis/choices.py b/facefusion/uis/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..92ae5491260c816d7bd86e2c4ee8b6fd5d43b4bc --- /dev/null +++ b/facefusion/uis/choices.py @@ -0,0 +1,7 @@ +from typing import List + +from facefusion.uis.typing import WebcamMode + +common_options : List[str] = [ 'keep-fps', 'keep-temp', 'skip-audio', 'skip-download' ] +webcam_modes : List[WebcamMode] = [ 'inline', 'udp', 'v4l2' ] +webcam_resolutions : List[str] = [ '320x240', '640x480', '800x600', '1024x768', '1280x720', '1280x960', '1920x1080', '2560x1440', '3840x2160' ] diff --git a/facefusion/uis/components/__init__.py b/facefusion/uis/components/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/uis/components/about.py b/facefusion/uis/components/about.py new file mode 100644 index 0000000000000000000000000000000000000000..e2c52caa1bbb88262ea2137e71ccd940dcf1acbf --- /dev/null +++ b/facefusion/uis/components/about.py @@ -0,0 +1,23 @@ +from typing import Optional +import gradio + +from facefusion import metadata, wording + +ABOUT_BUTTON : Optional[gradio.HTML] = None +DONATE_BUTTON : Optional[gradio.HTML] = None + + +def render() -> None: + global ABOUT_BUTTON + global DONATE_BUTTON + + ABOUT_BUTTON = gradio.Button( + value = metadata.get('name') + ' ' + metadata.get('version'), + variant = 'primary', + link = metadata.get('url') + ) + DONATE_BUTTON = gradio.Button( + value = wording.get('donate_button_label'), + link = 'https://donate.facefusion.io', + size = 'sm' + ) diff --git a/facefusion/uis/components/benchmark.py b/facefusion/uis/components/benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..09afe15243069d074eab039d29174eb3f655c27b --- /dev/null +++ b/facefusion/uis/components/benchmark.py @@ -0,0 +1,131 @@ +from typing import Any, Optional, List, Dict, Generator +import time +import tempfile +import statistics +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.face_analyser import get_face_analyser +from facefusion.face_cache import clear_faces_cache +from facefusion.processors.frame.core import get_frame_processors_modules +from facefusion.vision import count_video_frame_total +from facefusion.core import limit_resources, conditional_process +from facefusion.utilities import normalize_output_path, clear_temp +from facefusion.uis.core import get_ui_component + +BENCHMARK_RESULTS_DATAFRAME : Optional[gradio.Dataframe] = None +BENCHMARK_START_BUTTON : Optional[gradio.Button] = None +BENCHMARK_CLEAR_BUTTON : Optional[gradio.Button] = None +BENCHMARKS : Dict[str, str] =\ +{ + '240p': '.assets/examples/target-240p.mp4', + '360p': '.assets/examples/target-360p.mp4', + '540p': '.assets/examples/target-540p.mp4', + '720p': '.assets/examples/target-720p.mp4', + '1080p': '.assets/examples/target-1080p.mp4', + '1440p': '.assets/examples/target-1440p.mp4', + '2160p': '.assets/examples/target-2160p.mp4' +} + + +def render() -> None: + global BENCHMARK_RESULTS_DATAFRAME + global BENCHMARK_START_BUTTON + global BENCHMARK_CLEAR_BUTTON + + BENCHMARK_RESULTS_DATAFRAME = gradio.Dataframe( + label = wording.get('benchmark_results_dataframe_label'), + headers = + [ + 'target_path', + 'benchmark_cycles', + 'average_run', + 'fastest_run', + 'slowest_run', + 'relative_fps' + ], + datatype = + [ + 'str', + 'number', + 'number', + 'number', + 'number', + 'number' + ] + ) + BENCHMARK_START_BUTTON = gradio.Button( + value = wording.get('start_button_label'), + variant = 'primary', + size = 'sm' + ) + BENCHMARK_CLEAR_BUTTON = gradio.Button( + value = wording.get('clear_button_label'), + size = 'sm' + ) + + +def listen() -> None: + benchmark_runs_checkbox_group = get_ui_component('benchmark_runs_checkbox_group') + benchmark_cycles_slider = get_ui_component('benchmark_cycles_slider') + if benchmark_runs_checkbox_group and benchmark_cycles_slider: + BENCHMARK_START_BUTTON.click(start, inputs = [ benchmark_runs_checkbox_group, benchmark_cycles_slider ], outputs = BENCHMARK_RESULTS_DATAFRAME) + BENCHMARK_CLEAR_BUTTON.click(clear, outputs = BENCHMARK_RESULTS_DATAFRAME) + + +def start(benchmark_runs : List[str], benchmark_cycles : int) -> Generator[List[Any], None, None]: + facefusion.globals.source_path = '.assets/examples/source.jpg' + target_paths = [ BENCHMARKS[benchmark_run] for benchmark_run in benchmark_runs if benchmark_run in BENCHMARKS ] + benchmark_results = [] + if target_paths: + pre_process() + for target_path in target_paths: + benchmark_results.append(benchmark(target_path, benchmark_cycles)) + yield benchmark_results + post_process() + + +def pre_process() -> None: + limit_resources() + get_face_analyser() + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + frame_processor_module.get_frame_processor() + + +def post_process() -> None: + clear_faces_cache() + + +def benchmark(target_path : str, benchmark_cycles : int) -> List[Any]: + process_times = [] + total_fps = 0.0 + for i in range(benchmark_cycles): + facefusion.globals.target_path = target_path + facefusion.globals.output_path = normalize_output_path(facefusion.globals.source_path, facefusion.globals.target_path, tempfile.gettempdir()) + video_frame_total = count_video_frame_total(facefusion.globals.target_path) + start_time = time.perf_counter() + conditional_process() + end_time = time.perf_counter() + process_time = end_time - start_time + total_fps += video_frame_total / process_time + process_times.append(process_time) + average_run = round(statistics.mean(process_times), 2) + fastest_run = round(min(process_times), 2) + slowest_run = round(max(process_times), 2) + relative_fps = round(total_fps / benchmark_cycles, 2) + return\ + [ + facefusion.globals.target_path, + benchmark_cycles, + average_run, + fastest_run, + slowest_run, + relative_fps + ] + + +def clear() -> gradio.Dataframe: + if facefusion.globals.target_path: + clear_temp(facefusion.globals.target_path) + return gradio.Dataframe(value = None) diff --git a/facefusion/uis/components/benchmark_options.py b/facefusion/uis/components/benchmark_options.py new file mode 100644 index 0000000000000000000000000000000000000000..75767a88817c1b709f0177d74d65976038fd8746 --- /dev/null +++ b/facefusion/uis/components/benchmark_options.py @@ -0,0 +1,29 @@ +from typing import Optional +import gradio + +from facefusion import wording +from facefusion.uis.core import register_ui_component +from facefusion.uis.components.benchmark import BENCHMARKS + +BENCHMARK_RUNS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None +BENCHMARK_CYCLES_SLIDER : Optional[gradio.Button] = None + + +def render() -> None: + global BENCHMARK_RUNS_CHECKBOX_GROUP + global BENCHMARK_CYCLES_SLIDER + + BENCHMARK_RUNS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('benchmark_runs_checkbox_group_label'), + value = list(BENCHMARKS.keys()), + choices = list(BENCHMARKS.keys()) + ) + BENCHMARK_CYCLES_SLIDER = gradio.Slider( + label = wording.get('benchmark_cycles_slider_label'), + value = 3, + step = 1, + minimum = 1, + maximum = 10 + ) + register_ui_component('benchmark_runs_checkbox_group', BENCHMARK_RUNS_CHECKBOX_GROUP) + register_ui_component('benchmark_cycles_slider', BENCHMARK_CYCLES_SLIDER) diff --git a/facefusion/uis/components/common_options.py b/facefusion/uis/components/common_options.py new file mode 100644 index 0000000000000000000000000000000000000000..0b3e2d3971a9c0c865ea88b7e4125524fb2cbe5c --- /dev/null +++ b/facefusion/uis/components/common_options.py @@ -0,0 +1,38 @@ +from typing import Optional, List +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.uis import choices as uis_choices + +COMMON_OPTIONS_CHECKBOX_GROUP : Optional[gradio.Checkboxgroup] = None + + +def render() -> None: + global COMMON_OPTIONS_CHECKBOX_GROUP + + value = [] + if facefusion.globals.keep_fps: + value.append('keep-fps') + if facefusion.globals.keep_temp: + value.append('keep-temp') + if facefusion.globals.skip_audio: + value.append('skip-audio') + if facefusion.globals.skip_download: + value.append('skip-download') + COMMON_OPTIONS_CHECKBOX_GROUP = gradio.Checkboxgroup( + label = wording.get('common_options_checkbox_group_label'), + choices = uis_choices.common_options, + value = value + ) + + +def listen() -> None: + COMMON_OPTIONS_CHECKBOX_GROUP.change(update, inputs = COMMON_OPTIONS_CHECKBOX_GROUP) + + +def update(common_options : List[str]) -> None: + facefusion.globals.keep_fps = 'keep-fps' in common_options + facefusion.globals.keep_temp = 'keep-temp' in common_options + facefusion.globals.skip_audio = 'skip-audio' in common_options + facefusion.globals.skip_download = 'skip-download' in common_options diff --git a/facefusion/uis/components/execution.py b/facefusion/uis/components/execution.py new file mode 100644 index 0000000000000000000000000000000000000000..632d38cf00a10cc90cc631c7c6fda7dfe92ee703 --- /dev/null +++ b/facefusion/uis/components/execution.py @@ -0,0 +1,34 @@ +from typing import List, Optional +import gradio +import onnxruntime + +import facefusion.globals +from facefusion import wording +from facefusion.face_analyser import clear_face_analyser +from facefusion.processors.frame.core import clear_frame_processors_modules +from facefusion.utilities import encode_execution_providers, decode_execution_providers + +EXECUTION_PROVIDERS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global EXECUTION_PROVIDERS_CHECKBOX_GROUP + + EXECUTION_PROVIDERS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('execution_providers_checkbox_group_label'), + choices = encode_execution_providers(onnxruntime.get_available_providers()), + value = encode_execution_providers(facefusion.globals.execution_providers) + ) + + +def listen() -> None: + EXECUTION_PROVIDERS_CHECKBOX_GROUP.change(update_execution_providers, inputs = EXECUTION_PROVIDERS_CHECKBOX_GROUP, outputs = EXECUTION_PROVIDERS_CHECKBOX_GROUP) + + +def update_execution_providers(execution_providers : List[str]) -> gradio.CheckboxGroup: + clear_face_analyser() + clear_frame_processors_modules() + if not execution_providers: + execution_providers = encode_execution_providers(onnxruntime.get_available_providers()) + facefusion.globals.execution_providers = decode_execution_providers(execution_providers) + return gradio.CheckboxGroup(value = execution_providers) diff --git a/facefusion/uis/components/execution_queue_count.py b/facefusion/uis/components/execution_queue_count.py new file mode 100644 index 0000000000000000000000000000000000000000..fc8a3c87d3b86490b8e290a939cc1fc029ac7fb0 --- /dev/null +++ b/facefusion/uis/components/execution_queue_count.py @@ -0,0 +1,28 @@ +from typing import Optional +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording + +EXECUTION_QUEUE_COUNT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global EXECUTION_QUEUE_COUNT_SLIDER + + EXECUTION_QUEUE_COUNT_SLIDER = gradio.Slider( + label = wording.get('execution_queue_count_slider_label'), + value = facefusion.globals.execution_queue_count, + step = facefusion.choices.execution_queue_count_range[1] - facefusion.choices.execution_queue_count_range[0], + minimum = facefusion.choices.execution_queue_count_range[0], + maximum = facefusion.choices.execution_queue_count_range[-1] + ) + + +def listen() -> None: + EXECUTION_QUEUE_COUNT_SLIDER.change(update_execution_queue_count, inputs = EXECUTION_QUEUE_COUNT_SLIDER) + + +def update_execution_queue_count(execution_queue_count : int = 1) -> None: + facefusion.globals.execution_queue_count = execution_queue_count diff --git a/facefusion/uis/components/execution_thread_count.py b/facefusion/uis/components/execution_thread_count.py new file mode 100644 index 0000000000000000000000000000000000000000..615d164215f663a1e49cc122c270e32731a6f3dc --- /dev/null +++ b/facefusion/uis/components/execution_thread_count.py @@ -0,0 +1,29 @@ +from typing import Optional +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording + +EXECUTION_THREAD_COUNT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global EXECUTION_THREAD_COUNT_SLIDER + + EXECUTION_THREAD_COUNT_SLIDER = gradio.Slider( + label = wording.get('execution_thread_count_slider_label'), + value = facefusion.globals.execution_thread_count, + step = facefusion.choices.execution_thread_count_range[1] - facefusion.choices.execution_thread_count_range[0], + minimum = facefusion.choices.execution_thread_count_range[0], + maximum = facefusion.choices.execution_thread_count_range[-1] + ) + + +def listen() -> None: + EXECUTION_THREAD_COUNT_SLIDER.change(update_execution_thread_count, inputs = EXECUTION_THREAD_COUNT_SLIDER) + + +def update_execution_thread_count(execution_thread_count : int = 1) -> None: + facefusion.globals.execution_thread_count = execution_thread_count + diff --git a/facefusion/uis/components/face_analyser.py b/facefusion/uis/components/face_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..3c701182b7423945b0e3eb7d08d3293db077fc0f --- /dev/null +++ b/facefusion/uis/components/face_analyser.py @@ -0,0 +1,98 @@ +from typing import Optional + +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording +from facefusion.typing import FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, FaceDetectorModel +from facefusion.uis.core import register_ui_component + +FACE_ANALYSER_ORDER_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_ANALYSER_AGE_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_ANALYSER_GENDER_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_DETECTOR_SIZE_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_DETECTOR_SCORE_SLIDER : Optional[gradio.Slider] = None +FACE_DETECTOR_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global FACE_ANALYSER_ORDER_DROPDOWN + global FACE_ANALYSER_AGE_DROPDOWN + global FACE_ANALYSER_GENDER_DROPDOWN + global FACE_DETECTOR_SIZE_DROPDOWN + global FACE_DETECTOR_SCORE_SLIDER + global FACE_DETECTOR_MODEL_DROPDOWN + + with gradio.Row(): + FACE_ANALYSER_ORDER_DROPDOWN = gradio.Dropdown( + label = wording.get('face_analyser_order_dropdown_label'), + choices = facefusion.choices.face_analyser_orders, + value = facefusion.globals.face_analyser_order + ) + FACE_ANALYSER_AGE_DROPDOWN = gradio.Dropdown( + label = wording.get('face_analyser_age_dropdown_label'), + choices = [ 'none' ] + facefusion.choices.face_analyser_ages, + value = facefusion.globals.face_analyser_age or 'none' + ) + FACE_ANALYSER_GENDER_DROPDOWN = gradio.Dropdown( + label = wording.get('face_analyser_gender_dropdown_label'), + choices = [ 'none' ] + facefusion.choices.face_analyser_genders, + value = facefusion.globals.face_analyser_gender or 'none' + ) + FACE_DETECTOR_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('face_detector_model_dropdown_label'), + choices = facefusion.choices.face_detector_models, + value = facefusion.globals.face_detector_model + ) + FACE_DETECTOR_SIZE_DROPDOWN = gradio.Dropdown( + label = wording.get('face_detector_size_dropdown_label'), + choices = facefusion.choices.face_detector_sizes, + value = facefusion.globals.face_detector_size + ) + FACE_DETECTOR_SCORE_SLIDER = gradio.Slider( + label = wording.get('face_detector_score_slider_label'), + value = facefusion.globals.face_detector_score, + step =facefusion.choices.face_detector_score_range[1] - facefusion.choices.face_detector_score_range[0], + minimum = facefusion.choices.face_detector_score_range[0], + maximum = facefusion.choices.face_detector_score_range[-1] + ) + register_ui_component('face_analyser_order_dropdown', FACE_ANALYSER_ORDER_DROPDOWN) + register_ui_component('face_analyser_age_dropdown', FACE_ANALYSER_AGE_DROPDOWN) + register_ui_component('face_analyser_gender_dropdown', FACE_ANALYSER_GENDER_DROPDOWN) + register_ui_component('face_detector_model_dropdown', FACE_DETECTOR_MODEL_DROPDOWN) + register_ui_component('face_detector_size_dropdown', FACE_DETECTOR_SIZE_DROPDOWN) + register_ui_component('face_detector_score_slider', FACE_DETECTOR_SCORE_SLIDER) + + +def listen() -> None: + FACE_ANALYSER_ORDER_DROPDOWN.select(update_face_analyser_order, inputs = FACE_ANALYSER_ORDER_DROPDOWN) + FACE_ANALYSER_AGE_DROPDOWN.select(update_face_analyser_age, inputs = FACE_ANALYSER_AGE_DROPDOWN) + FACE_ANALYSER_GENDER_DROPDOWN.select(update_face_analyser_gender, inputs = FACE_ANALYSER_GENDER_DROPDOWN) + FACE_DETECTOR_MODEL_DROPDOWN.change(update_face_detector_model, inputs = FACE_DETECTOR_MODEL_DROPDOWN) + FACE_DETECTOR_SIZE_DROPDOWN.select(update_face_detector_size, inputs = FACE_DETECTOR_SIZE_DROPDOWN) + FACE_DETECTOR_SCORE_SLIDER.change(update_face_detector_score, inputs = FACE_DETECTOR_SCORE_SLIDER) + + +def update_face_analyser_order(face_analyser_order : FaceAnalyserOrder) -> None: + facefusion.globals.face_analyser_order = face_analyser_order if face_analyser_order != 'none' else None + + +def update_face_analyser_age(face_analyser_age : FaceAnalyserAge) -> None: + facefusion.globals.face_analyser_age = face_analyser_age if face_analyser_age != 'none' else None + + +def update_face_analyser_gender(face_analyser_gender : FaceAnalyserGender) -> None: + facefusion.globals.face_analyser_gender = face_analyser_gender if face_analyser_gender != 'none' else None + + +def update_face_detector_model(face_detector_model : FaceDetectorModel) -> None: + facefusion.globals.face_detector_model = face_detector_model + + +def update_face_detector_size(face_detector_size : str) -> None: + facefusion.globals.face_detector_size = face_detector_size + + +def update_face_detector_score(face_detector_score : float) -> None: + facefusion.globals.face_detector_score = face_detector_score diff --git a/facefusion/uis/components/face_mask.py b/facefusion/uis/components/face_mask.py new file mode 100644 index 0000000000000000000000000000000000000000..08780fba834b0040bd369ac15ab3e29204296a80 --- /dev/null +++ b/facefusion/uis/components/face_mask.py @@ -0,0 +1,80 @@ +from typing import Optional +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording +from facefusion.uis.core import register_ui_component + +FACE_MASK_BLUR_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_TOP_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_RIGHT_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_BOTTOM_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_LEFT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_MASK_BLUR_SLIDER + global FACE_MASK_PADDING_TOP_SLIDER + global FACE_MASK_PADDING_RIGHT_SLIDER + global FACE_MASK_PADDING_BOTTOM_SLIDER + global FACE_MASK_PADDING_LEFT_SLIDER + + FACE_MASK_BLUR_SLIDER = gradio.Slider( + label = wording.get('face_mask_blur_slider_label'), + step = facefusion.choices.face_mask_blur_range[1] - facefusion.choices.face_mask_blur_range[0], + minimum = facefusion.choices.face_mask_blur_range[0], + maximum = facefusion.choices.face_mask_blur_range[-1], + value = facefusion.globals.face_mask_blur + ) + with gradio.Group(): + with gradio.Row(): + FACE_MASK_PADDING_TOP_SLIDER = gradio.Slider( + label = wording.get('face_mask_padding_top_slider_label'), + step = facefusion.choices.face_mask_padding_range[1] - facefusion.choices.face_mask_padding_range[0], + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = facefusion.globals.face_mask_padding[0] + ) + FACE_MASK_PADDING_RIGHT_SLIDER = gradio.Slider( + label = wording.get('face_mask_padding_right_slider_label'), + step = facefusion.choices.face_mask_padding_range[1] - facefusion.choices.face_mask_padding_range[0], + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = facefusion.globals.face_mask_padding[1] + ) + with gradio.Row(): + FACE_MASK_PADDING_BOTTOM_SLIDER = gradio.Slider( + label = wording.get('face_mask_padding_bottom_slider_label'), + step = facefusion.choices.face_mask_padding_range[1] - facefusion.choices.face_mask_padding_range[0], + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = facefusion.globals.face_mask_padding[2] + ) + FACE_MASK_PADDING_LEFT_SLIDER = gradio.Slider( + label = wording.get('face_mask_padding_left_slider_label'), + step = facefusion.choices.face_mask_padding_range[1] - facefusion.choices.face_mask_padding_range[0], + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = facefusion.globals.face_mask_padding[3] + ) + register_ui_component('face_mask_blur_slider', FACE_MASK_BLUR_SLIDER) + register_ui_component('face_mask_padding_top_slider', FACE_MASK_PADDING_TOP_SLIDER) + register_ui_component('face_mask_padding_right_slider', FACE_MASK_PADDING_RIGHT_SLIDER) + register_ui_component('face_mask_padding_bottom_slider', FACE_MASK_PADDING_BOTTOM_SLIDER) + register_ui_component('face_mask_padding_left_slider', FACE_MASK_PADDING_LEFT_SLIDER) + + +def listen() -> None: + FACE_MASK_BLUR_SLIDER.change(update_face_mask_blur, inputs = FACE_MASK_BLUR_SLIDER) + face_mask_padding_sliders = [ FACE_MASK_PADDING_TOP_SLIDER, FACE_MASK_PADDING_RIGHT_SLIDER, FACE_MASK_PADDING_BOTTOM_SLIDER, FACE_MASK_PADDING_LEFT_SLIDER ] + for face_mask_padding_slider in face_mask_padding_sliders: + face_mask_padding_slider.change(update_face_mask_padding, inputs = face_mask_padding_sliders) + + +def update_face_mask_blur(face_mask_blur : float) -> None: + facefusion.globals.face_mask_blur = face_mask_blur + + +def update_face_mask_padding(face_mask_padding_top : int, face_mask_padding_right : int, face_mask_padding_bottom : int, face_mask_padding_left : int) -> None: + facefusion.globals.face_mask_padding = (face_mask_padding_top, face_mask_padding_right, face_mask_padding_bottom, face_mask_padding_left) diff --git a/facefusion/uis/components/face_selector.py b/facefusion/uis/components/face_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..5ac5f5ed9eeec0326cec1989e8cd8185d8cf7418 --- /dev/null +++ b/facefusion/uis/components/face_selector.py @@ -0,0 +1,165 @@ +from typing import List, Optional, Tuple, Any, Dict + +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording +from facefusion.face_cache import clear_faces_cache +from facefusion.vision import get_video_frame, read_static_image, normalize_frame_color +from facefusion.face_analyser import get_many_faces +from facefusion.face_reference import clear_face_reference +from facefusion.typing import Frame, FaceSelectorMode +from facefusion.utilities import is_image, is_video +from facefusion.uis.core import get_ui_component, register_ui_component +from facefusion.uis.typing import ComponentName + +FACE_SELECTOR_MODE_DROPDOWN : Optional[gradio.Dropdown] = None +REFERENCE_FACE_POSITION_GALLERY : Optional[gradio.Gallery] = None +REFERENCE_FACE_DISTANCE_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_SELECTOR_MODE_DROPDOWN + global REFERENCE_FACE_POSITION_GALLERY + global REFERENCE_FACE_DISTANCE_SLIDER + + reference_face_gallery_args: Dict[str, Any] =\ + { + 'label': wording.get('reference_face_gallery_label'), + 'object_fit': 'cover', + 'columns': 8, + 'allow_preview': False, + 'visible': 'reference' in facefusion.globals.face_selector_mode + } + if is_image(facefusion.globals.target_path): + reference_frame = read_static_image(facefusion.globals.target_path) + reference_face_gallery_args['value'] = extract_gallery_frames(reference_frame) + if is_video(facefusion.globals.target_path): + reference_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) + reference_face_gallery_args['value'] = extract_gallery_frames(reference_frame) + FACE_SELECTOR_MODE_DROPDOWN = gradio.Dropdown( + label = wording.get('face_selector_mode_dropdown_label'), + choices = facefusion.choices.face_selector_modes, + value = facefusion.globals.face_selector_mode + ) + REFERENCE_FACE_POSITION_GALLERY = gradio.Gallery(**reference_face_gallery_args) + REFERENCE_FACE_DISTANCE_SLIDER = gradio.Slider( + label = wording.get('reference_face_distance_slider_label'), + value = facefusion.globals.reference_face_distance, + step = facefusion.choices.reference_face_distance_range[1] - facefusion.choices.reference_face_distance_range[0], + minimum = facefusion.choices.reference_face_distance_range[0], + maximum = facefusion.choices.reference_face_distance_range[-1], + visible = 'reference' in facefusion.globals.face_selector_mode + ) + register_ui_component('face_selector_mode_dropdown', FACE_SELECTOR_MODE_DROPDOWN) + register_ui_component('reference_face_position_gallery', REFERENCE_FACE_POSITION_GALLERY) + register_ui_component('reference_face_distance_slider', REFERENCE_FACE_DISTANCE_SLIDER) + + +def listen() -> None: + FACE_SELECTOR_MODE_DROPDOWN.select(update_face_selector_mode, inputs = FACE_SELECTOR_MODE_DROPDOWN, outputs = [ REFERENCE_FACE_POSITION_GALLERY, REFERENCE_FACE_DISTANCE_SLIDER ]) + REFERENCE_FACE_POSITION_GALLERY.select(clear_and_update_reference_face_position) + REFERENCE_FACE_DISTANCE_SLIDER.change(update_reference_face_distance, inputs = REFERENCE_FACE_DISTANCE_SLIDER) + multi_component_names : List[ComponentName] =\ + [ + 'target_image', + 'target_video' + ] + for component_name in multi_component_names: + component = get_ui_component(component_name) + if component: + for method in [ 'upload', 'change', 'clear' ]: + getattr(component, method)(update_reference_face_position) + getattr(component, method)(update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + change_one_component_names : List[ComponentName] =\ + [ + 'face_analyser_order_dropdown', + 'face_analyser_age_dropdown', + 'face_analyser_gender_dropdown' + ] + for component_name in change_one_component_names: + component = get_ui_component(component_name) + if component: + component.change(update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + change_two_component_names : List[ComponentName] =\ + [ + 'face_detector_model_dropdown', + 'face_detector_size_dropdown', + 'face_detector_score_slider' + ] + for component_name in change_two_component_names: + component = get_ui_component(component_name) + if component: + component.change(clear_and_update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + preview_frame_slider = get_ui_component('preview_frame_slider') + if preview_frame_slider: + preview_frame_slider.change(update_reference_frame_number, inputs = preview_frame_slider) + preview_frame_slider.release(update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + + +def update_face_selector_mode(face_selector_mode : FaceSelectorMode) -> Tuple[gradio.Gallery, gradio.Slider]: + if face_selector_mode == 'reference': + facefusion.globals.face_selector_mode = face_selector_mode + return gradio.Gallery(visible = True), gradio.Slider(visible = True) + if face_selector_mode == 'one': + facefusion.globals.face_selector_mode = face_selector_mode + return gradio.Gallery(visible = False), gradio.Slider(visible = False) + if face_selector_mode == 'many': + facefusion.globals.face_selector_mode = face_selector_mode + return gradio.Gallery(visible = False), gradio.Slider(visible = False) + + +def clear_and_update_reference_face_position(event : gradio.SelectData) -> gradio.Gallery: + clear_face_reference() + clear_faces_cache() + update_reference_face_position(event.index) + return update_reference_position_gallery() + + +def update_reference_face_position(reference_face_position : int = 0) -> None: + facefusion.globals.reference_face_position = reference_face_position + + +def update_reference_face_distance(reference_face_distance : float) -> None: + facefusion.globals.reference_face_distance = reference_face_distance + + +def update_reference_frame_number(reference_frame_number : int) -> None: + facefusion.globals.reference_frame_number = reference_frame_number + + +def clear_and_update_reference_position_gallery() -> gradio.Gallery: + clear_face_reference() + clear_faces_cache() + return update_reference_position_gallery() + + +def update_reference_position_gallery() -> gradio.Gallery: + gallery_frames = [] + if is_image(facefusion.globals.target_path): + reference_frame = read_static_image(facefusion.globals.target_path) + gallery_frames = extract_gallery_frames(reference_frame) + if is_video(facefusion.globals.target_path): + reference_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) + gallery_frames = extract_gallery_frames(reference_frame) + if gallery_frames: + return gradio.Gallery(value = gallery_frames) + return gradio.Gallery(value = None) + + +def extract_gallery_frames(reference_frame : Frame) -> List[Frame]: + crop_frames = [] + faces = get_many_faces(reference_frame) + for face in faces: + start_x, start_y, end_x, end_y = map(int, face.bbox) + padding_x = int((end_x - start_x) * 0.25) + padding_y = int((end_y - start_y) * 0.25) + start_x = max(0, start_x - padding_x) + start_y = max(0, start_y - padding_y) + end_x = max(0, end_x + padding_x) + end_y = max(0, end_y + padding_y) + crop_frame = reference_frame[start_y:end_y, start_x:end_x] + crop_frame = normalize_frame_color(crop_frame) + crop_frames.append(crop_frame) + return crop_frames diff --git a/facefusion/uis/components/frame_processors.py b/facefusion/uis/components/frame_processors.py new file mode 100644 index 0000000000000000000000000000000000000000..861e5771d1b5f817d7bd77fbcdd457628b778887 --- /dev/null +++ b/facefusion/uis/components/frame_processors.py @@ -0,0 +1,40 @@ +from typing import List, Optional +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.processors.frame.core import load_frame_processor_module, clear_frame_processors_modules +from facefusion.utilities import list_module_names +from facefusion.uis.core import register_ui_component + +FRAME_PROCESSORS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global FRAME_PROCESSORS_CHECKBOX_GROUP + + FRAME_PROCESSORS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('frame_processors_checkbox_group_label'), + choices = sort_frame_processors(facefusion.globals.frame_processors), + value = facefusion.globals.frame_processors + ) + register_ui_component('frame_processors_checkbox_group', FRAME_PROCESSORS_CHECKBOX_GROUP) + + +def listen() -> None: + FRAME_PROCESSORS_CHECKBOX_GROUP.change(update_frame_processors, inputs = FRAME_PROCESSORS_CHECKBOX_GROUP, outputs = FRAME_PROCESSORS_CHECKBOX_GROUP) + + +def update_frame_processors(frame_processors : List[str]) -> gradio.CheckboxGroup: + facefusion.globals.frame_processors = frame_processors + clear_frame_processors_modules() + for frame_processor in frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + if not frame_processor_module.pre_check(): + return gradio.CheckboxGroup() + return gradio.CheckboxGroup(value = frame_processors, choices = sort_frame_processors(frame_processors)) + + +def sort_frame_processors(frame_processors : List[str]) -> list[str]: + available_frame_processors = list_module_names('facefusion/processors/frame/modules') + return sorted(available_frame_processors, key = lambda frame_processor : frame_processors.index(frame_processor) if frame_processor in frame_processors else len(frame_processors)) diff --git a/facefusion/uis/components/frame_processors_options.py b/facefusion/uis/components/frame_processors_options.py new file mode 100644 index 0000000000000000000000000000000000000000..0b4aa547f92941220756c80e5a13a6e75432ed57 --- /dev/null +++ b/facefusion/uis/components/frame_processors_options.py @@ -0,0 +1,141 @@ +from typing import List, Optional, Tuple +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.processors.frame.core import load_frame_processor_module +from facefusion.processors.frame import globals as frame_processors_globals, choices as frame_processors_choices +from facefusion.processors.frame.typings import FaceSwapperModel, FaceEnhancerModel, FrameEnhancerModel, FaceDebuggerItem +from facefusion.uis.core import get_ui_component, register_ui_component + +FACE_SWAPPER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_ENHANCER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_ENHANCER_BLEND_SLIDER : Optional[gradio.Slider] = None +FRAME_ENHANCER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FRAME_ENHANCER_BLEND_SLIDER : Optional[gradio.Slider] = None +FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global FACE_SWAPPER_MODEL_DROPDOWN + global FACE_ENHANCER_MODEL_DROPDOWN + global FACE_ENHANCER_BLEND_SLIDER + global FRAME_ENHANCER_MODEL_DROPDOWN + global FRAME_ENHANCER_BLEND_SLIDER + global FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP + + FACE_SWAPPER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('face_swapper_model_dropdown_label'), + choices = frame_processors_choices.face_swapper_models, + value = frame_processors_globals.face_swapper_model, + visible = 'face_swapper' in facefusion.globals.frame_processors + ) + FACE_ENHANCER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('face_enhancer_model_dropdown_label'), + choices = frame_processors_choices.face_enhancer_models, + value = frame_processors_globals.face_enhancer_model, + visible = 'face_enhancer' in facefusion.globals.frame_processors + ) + FACE_ENHANCER_BLEND_SLIDER = gradio.Slider( + label = wording.get('face_enhancer_blend_slider_label'), + value = frame_processors_globals.face_enhancer_blend, + step = frame_processors_choices.face_enhancer_blend_range[1] - frame_processors_choices.face_enhancer_blend_range[0], + minimum = frame_processors_choices.face_enhancer_blend_range[0], + maximum = frame_processors_choices.face_enhancer_blend_range[-1], + visible = 'face_enhancer' in facefusion.globals.frame_processors + ) + FRAME_ENHANCER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('frame_enhancer_model_dropdown_label'), + choices = frame_processors_choices.frame_enhancer_models, + value = frame_processors_globals.frame_enhancer_model, + visible = 'frame_enhancer' in facefusion.globals.frame_processors + ) + FRAME_ENHANCER_BLEND_SLIDER = gradio.Slider( + label = wording.get('frame_enhancer_blend_slider_label'), + value = frame_processors_globals.frame_enhancer_blend, + step = frame_processors_choices.frame_enhancer_blend_range[1] - frame_processors_choices.frame_enhancer_blend_range[0], + minimum = frame_processors_choices.frame_enhancer_blend_range[0], + maximum = frame_processors_choices.frame_enhancer_blend_range[-1], + visible = 'face_enhancer' in facefusion.globals.frame_processors + ) + FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('face_debugger_items_checkbox_group_label'), + choices = frame_processors_choices.face_debugger_items, + value = frame_processors_globals.face_debugger_items, + visible = 'face_debugger' in facefusion.globals.frame_processors + ) + + register_ui_component('face_swapper_model_dropdown', FACE_SWAPPER_MODEL_DROPDOWN) + register_ui_component('face_enhancer_model_dropdown', FACE_ENHANCER_MODEL_DROPDOWN) + register_ui_component('face_enhancer_blend_slider', FACE_ENHANCER_BLEND_SLIDER) + register_ui_component('frame_enhancer_model_dropdown', FRAME_ENHANCER_MODEL_DROPDOWN) + register_ui_component('frame_enhancer_blend_slider', FRAME_ENHANCER_BLEND_SLIDER) + register_ui_component('face_debugger_items_checkbox_group', FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP) + + +def listen() -> None: + FACE_SWAPPER_MODEL_DROPDOWN.change(update_face_swapper_model, inputs = FACE_SWAPPER_MODEL_DROPDOWN, outputs = FACE_SWAPPER_MODEL_DROPDOWN) + FACE_ENHANCER_MODEL_DROPDOWN.change(update_face_enhancer_model, inputs = FACE_ENHANCER_MODEL_DROPDOWN, outputs = FACE_ENHANCER_MODEL_DROPDOWN) + FACE_ENHANCER_BLEND_SLIDER.change(update_face_enhancer_blend, inputs = FACE_ENHANCER_BLEND_SLIDER) + FRAME_ENHANCER_MODEL_DROPDOWN.change(update_frame_enhancer_model, inputs = FRAME_ENHANCER_MODEL_DROPDOWN, outputs = FRAME_ENHANCER_MODEL_DROPDOWN) + FRAME_ENHANCER_BLEND_SLIDER.change(update_frame_enhancer_blend, inputs = FRAME_ENHANCER_BLEND_SLIDER) + FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP.change(update_face_debugger_items, inputs = FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP) + frame_processors_checkbox_group = get_ui_component('frame_processors_checkbox_group') + if frame_processors_checkbox_group: + frame_processors_checkbox_group.change(toggle_face_swapper_model, inputs = frame_processors_checkbox_group, outputs = [ FACE_SWAPPER_MODEL_DROPDOWN, FACE_ENHANCER_MODEL_DROPDOWN, FACE_ENHANCER_BLEND_SLIDER, FRAME_ENHANCER_MODEL_DROPDOWN, FRAME_ENHANCER_BLEND_SLIDER, FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP ]) + + +def update_face_swapper_model(face_swapper_model : FaceSwapperModel) -> gradio.Dropdown: + frame_processors_globals.face_swapper_model = face_swapper_model + if face_swapper_model == 'blendface_256': + facefusion.globals.face_recognizer_model = 'arcface_blendface' + if face_swapper_model == 'inswapper_128' or face_swapper_model == 'inswapper_128_fp16': + facefusion.globals.face_recognizer_model = 'arcface_inswapper' + if face_swapper_model == 'simswap_256' or face_swapper_model == 'simswap_512_unofficial': + facefusion.globals.face_recognizer_model = 'arcface_simswap' + face_swapper_module = load_frame_processor_module('face_swapper') + face_swapper_module.clear_frame_processor() + face_swapper_module.set_options('model', face_swapper_module.MODELS[face_swapper_model]) + if not face_swapper_module.pre_check(): + return gradio.Dropdown() + return gradio.Dropdown(value = face_swapper_model) + + +def update_face_enhancer_model(face_enhancer_model : FaceEnhancerModel) -> gradio.Dropdown: + frame_processors_globals.face_enhancer_model = face_enhancer_model + face_enhancer_module = load_frame_processor_module('face_enhancer') + face_enhancer_module.clear_frame_processor() + face_enhancer_module.set_options('model', face_enhancer_module.MODELS[face_enhancer_model]) + if not face_enhancer_module.pre_check(): + return gradio.Dropdown() + return gradio.Dropdown(value = face_enhancer_model) + + +def update_face_enhancer_blend(face_enhancer_blend : int) -> None: + frame_processors_globals.face_enhancer_blend = face_enhancer_blend + + +def update_frame_enhancer_model(frame_enhancer_model : FrameEnhancerModel) -> gradio.Dropdown: + frame_processors_globals.frame_enhancer_model = frame_enhancer_model + frame_enhancer_module = load_frame_processor_module('frame_enhancer') + frame_enhancer_module.clear_frame_processor() + frame_enhancer_module.set_options('model', frame_enhancer_module.MODELS[frame_enhancer_model]) + if not frame_enhancer_module.pre_check(): + return gradio.Dropdown() + return gradio.Dropdown(value = frame_enhancer_model) + + +def update_frame_enhancer_blend(frame_enhancer_blend : int) -> None: + frame_processors_globals.frame_enhancer_blend = frame_enhancer_blend + + +def update_face_debugger_items(face_debugger_items : List[FaceDebuggerItem]) -> None: + frame_processors_globals.face_debugger_items = face_debugger_items + + +def toggle_face_swapper_model(frame_processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Dropdown, gradio.Slider, gradio.Dropdown, gradio.Slider, gradio.CheckboxGroup]: + has_face_swapper = 'face_swapper' in frame_processors + has_face_enhancer = 'face_enhancer' in frame_processors + has_frame_enhancer = 'frame_enhancer' in frame_processors + has_face_debugger = 'face_debugger' in frame_processors + return gradio.Dropdown(visible = has_face_swapper), gradio.Dropdown(visible = has_face_enhancer), gradio.Slider(visible = has_face_enhancer), gradio.Dropdown(visible = has_frame_enhancer), gradio.Slider(visible = has_frame_enhancer), gradio.CheckboxGroup(visible = has_face_debugger) diff --git a/facefusion/uis/components/limit_resources.py b/facefusion/uis/components/limit_resources.py new file mode 100644 index 0000000000000000000000000000000000000000..6703cf1a0b45a16cb0b7fde03e193662ef2d7210 --- /dev/null +++ b/facefusion/uis/components/limit_resources.py @@ -0,0 +1,27 @@ +from typing import Optional +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording + +MAX_MEMORY_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global MAX_MEMORY_SLIDER + + MAX_MEMORY_SLIDER = gradio.Slider( + label = wording.get('max_memory_slider_label'), + step = facefusion.choices.max_memory_range[1] - facefusion.choices.max_memory_range[0], + minimum = facefusion.choices.max_memory_range[0], + maximum = facefusion.choices.max_memory_range[-1] + ) + + +def listen() -> None: + MAX_MEMORY_SLIDER.change(update_max_memory, inputs = MAX_MEMORY_SLIDER) + + +def update_max_memory(max_memory : int) -> None: + facefusion.globals.max_memory = max_memory if max_memory > 0 else None diff --git a/facefusion/uis/components/output.py b/facefusion/uis/components/output.py new file mode 100644 index 0000000000000000000000000000000000000000..81156e69b0d3cb6436cb0691bdc0908dbd2646f0 --- /dev/null +++ b/facefusion/uis/components/output.py @@ -0,0 +1,61 @@ +from typing import Tuple, Optional +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.core import limit_resources, conditional_process +from facefusion.uis.core import get_ui_component +from facefusion.utilities import is_image, is_video, normalize_output_path, clear_temp + +OUTPUT_IMAGE : Optional[gradio.Image] = None +OUTPUT_VIDEO : Optional[gradio.Video] = None +OUTPUT_START_BUTTON : Optional[gradio.Button] = None +OUTPUT_CLEAR_BUTTON : Optional[gradio.Button] = None + + +def render() -> None: + global OUTPUT_IMAGE + global OUTPUT_VIDEO + global OUTPUT_START_BUTTON + global OUTPUT_CLEAR_BUTTON + + OUTPUT_IMAGE = gradio.Image( + label = wording.get('output_image_or_video_label'), + visible = False + ) + OUTPUT_VIDEO = gradio.Video( + label = wording.get('output_image_or_video_label') + ) + OUTPUT_START_BUTTON = gradio.Button( + value = wording.get('start_button_label'), + variant = 'primary', + size = 'sm' + ) + OUTPUT_CLEAR_BUTTON = gradio.Button( + value = wording.get('clear_button_label'), + size = 'sm' + ) + + +def listen() -> None: + output_path_textbox = get_ui_component('output_path_textbox') + if output_path_textbox: + OUTPUT_START_BUTTON.click(start, inputs = output_path_textbox, outputs = [ OUTPUT_IMAGE, OUTPUT_VIDEO ]) + OUTPUT_CLEAR_BUTTON.click(clear, outputs = [ OUTPUT_IMAGE, OUTPUT_VIDEO ]) + + +def start(output_path : str) -> Tuple[gradio.Image, gradio.Video]: + facefusion.globals.output_path = normalize_output_path(facefusion.globals.source_path, facefusion.globals.target_path, output_path) + limit_resources() + conditional_process() + if is_image(facefusion.globals.output_path): + return gradio.Image(value = facefusion.globals.output_path, visible = True), gradio.Video(value = None, visible = False) + if is_video(facefusion.globals.output_path): + return gradio.Image(value = None, visible = False), gradio.Video(value = facefusion.globals.output_path, visible = True) + return gradio.Image(), gradio.Video() + + +def clear() -> Tuple[gradio.Image, gradio.Video]: + if facefusion.globals.target_path: + clear_temp(facefusion.globals.target_path) + return gradio.Image(value = None), gradio.Video(value = None) diff --git a/facefusion/uis/components/output_options.py b/facefusion/uis/components/output_options.py new file mode 100644 index 0000000000000000000000000000000000000000..900a92cc7a55e08d39570e02dc87962c918917e9 --- /dev/null +++ b/facefusion/uis/components/output_options.py @@ -0,0 +1,94 @@ +from typing import Optional, Tuple, List +import tempfile +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording +from facefusion.typing import OutputVideoEncoder +from facefusion.utilities import is_image, is_video +from facefusion.uis.typing import ComponentName +from facefusion.uis.core import get_ui_component, register_ui_component + +OUTPUT_PATH_TEXTBOX : Optional[gradio.Textbox] = None +OUTPUT_IMAGE_QUALITY_SLIDER : Optional[gradio.Slider] = None +OUTPUT_VIDEO_ENCODER_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_VIDEO_QUALITY_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global OUTPUT_PATH_TEXTBOX + global OUTPUT_IMAGE_QUALITY_SLIDER + global OUTPUT_VIDEO_ENCODER_DROPDOWN + global OUTPUT_VIDEO_QUALITY_SLIDER + + OUTPUT_PATH_TEXTBOX = gradio.Textbox( + label = wording.get('output_path_textbox_label'), + value = facefusion.globals.output_path or tempfile.gettempdir(), + max_lines = 1 + ) + OUTPUT_IMAGE_QUALITY_SLIDER = gradio.Slider( + label = wording.get('output_image_quality_slider_label'), + value = facefusion.globals.output_image_quality, + step = facefusion.choices.output_image_quality_range[1] - facefusion.choices.output_image_quality_range[0], + minimum = facefusion.choices.output_image_quality_range[0], + maximum = facefusion.choices.output_image_quality_range[-1], + visible = is_image(facefusion.globals.target_path) + ) + OUTPUT_VIDEO_ENCODER_DROPDOWN = gradio.Dropdown( + label = wording.get('output_video_encoder_dropdown_label'), + choices = facefusion.choices.output_video_encoders, + value = facefusion.globals.output_video_encoder, + visible = is_video(facefusion.globals.target_path) + ) + OUTPUT_VIDEO_QUALITY_SLIDER = gradio.Slider( + label = wording.get('output_video_quality_slider_label'), + value = facefusion.globals.output_video_quality, + step = facefusion.choices.output_video_quality_range[1] - facefusion.choices.output_video_quality_range[0], + minimum = facefusion.choices.output_video_quality_range[0], + maximum = facefusion.choices.output_video_quality_range[-1], + visible = is_video(facefusion.globals.target_path) + ) + register_ui_component('output_path_textbox', OUTPUT_PATH_TEXTBOX) + + +def listen() -> None: + OUTPUT_PATH_TEXTBOX.change(update_output_path, inputs = OUTPUT_PATH_TEXTBOX) + OUTPUT_IMAGE_QUALITY_SLIDER.change(update_output_image_quality, inputs = OUTPUT_IMAGE_QUALITY_SLIDER) + OUTPUT_VIDEO_ENCODER_DROPDOWN.select(update_output_video_encoder, inputs = OUTPUT_VIDEO_ENCODER_DROPDOWN) + OUTPUT_VIDEO_QUALITY_SLIDER.change(update_output_video_quality, inputs = OUTPUT_VIDEO_QUALITY_SLIDER) + multi_component_names : List[ComponentName] =\ + [ + 'source_image', + 'target_image', + 'target_video' + ] + for component_name in multi_component_names: + component = get_ui_component(component_name) + if component: + for method in [ 'upload', 'change', 'clear' ]: + getattr(component, method)(remote_update, outputs = [ OUTPUT_IMAGE_QUALITY_SLIDER, OUTPUT_VIDEO_ENCODER_DROPDOWN, OUTPUT_VIDEO_QUALITY_SLIDER ]) + + +def remote_update() -> Tuple[gradio.Slider, gradio.Dropdown, gradio.Slider]: + if is_image(facefusion.globals.target_path): + return gradio.Slider(visible = True), gradio.Dropdown(visible = False), gradio.Slider(visible = False) + if is_video(facefusion.globals.target_path): + return gradio.Slider(visible = False), gradio.Dropdown(visible = True), gradio.Slider(visible = True) + return gradio.Slider(visible = False), gradio.Dropdown(visible = False), gradio.Slider(visible = False) + + +def update_output_path(output_path : str) -> None: + facefusion.globals.output_path = output_path + + +def update_output_image_quality(output_image_quality : int) -> None: + facefusion.globals.output_image_quality = output_image_quality + + +def update_output_video_encoder(output_video_encoder: OutputVideoEncoder) -> None: + facefusion.globals.output_video_encoder = output_video_encoder + + +def update_output_video_quality(output_video_quality : int) -> None: + facefusion.globals.output_video_quality = output_video_quality diff --git a/facefusion/uis/components/preview.py b/facefusion/uis/components/preview.py new file mode 100644 index 0000000000000000000000000000000000000000..7b2061ddafdf7eff4cf37c9dc0e4e07da645603d --- /dev/null +++ b/facefusion/uis/components/preview.py @@ -0,0 +1,172 @@ +from typing import Any, Dict, List, Optional +import cv2 +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.core import conditional_set_face_reference +from facefusion.face_cache import clear_faces_cache +from facefusion.typing import Frame, Face +from facefusion.vision import get_video_frame, count_video_frame_total, normalize_frame_color, resize_frame_dimension, read_static_image +from facefusion.face_analyser import get_one_face, clear_face_analyser +from facefusion.face_reference import get_face_reference, clear_face_reference +from facefusion.content_analyser import analyse_frame +from facefusion.processors.frame.core import load_frame_processor_module +from facefusion.utilities import is_video, is_image +from facefusion.uis.typing import ComponentName +from facefusion.uis.core import get_ui_component, register_ui_component + +PREVIEW_IMAGE : Optional[gradio.Image] = None +PREVIEW_FRAME_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global PREVIEW_IMAGE + global PREVIEW_FRAME_SLIDER + + preview_image_args: Dict[str, Any] =\ + { + 'label': wording.get('preview_image_label'), + 'interactive': False + } + preview_frame_slider_args: Dict[str, Any] =\ + { + 'label': wording.get('preview_frame_slider_label'), + 'step': 1, + 'minimum': 0, + 'maximum': 100, + 'visible': False + } + conditional_set_face_reference() + source_face = get_one_face(read_static_image(facefusion.globals.source_path)) + reference_face = get_face_reference() if 'reference' in facefusion.globals.face_selector_mode else None + if is_image(facefusion.globals.target_path): + target_frame = read_static_image(facefusion.globals.target_path) + preview_frame = process_preview_frame(source_face, reference_face, target_frame) + preview_image_args['value'] = normalize_frame_color(preview_frame) + if is_video(facefusion.globals.target_path): + temp_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) + preview_frame = process_preview_frame(source_face, reference_face, temp_frame) + preview_image_args['value'] = normalize_frame_color(preview_frame) + preview_image_args['visible'] = True + preview_frame_slider_args['value'] = facefusion.globals.reference_frame_number + preview_frame_slider_args['maximum'] = count_video_frame_total(facefusion.globals.target_path) + preview_frame_slider_args['visible'] = True + PREVIEW_IMAGE = gradio.Image(**preview_image_args) + PREVIEW_FRAME_SLIDER = gradio.Slider(**preview_frame_slider_args) + register_ui_component('preview_frame_slider', PREVIEW_FRAME_SLIDER) + + +def listen() -> None: + PREVIEW_FRAME_SLIDER.change(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + multi_one_component_names : List[ComponentName] =\ + [ + 'source_image', + 'target_image', + 'target_video' + ] + for component_name in multi_one_component_names: + component = get_ui_component(component_name) + if component: + for method in [ 'upload', 'change', 'clear' ]: + getattr(component, method)(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + multi_two_component_names : List[ComponentName] =\ + [ + 'target_image', + 'target_video' + ] + for component_name in multi_two_component_names: + component = get_ui_component(component_name) + if component: + for method in [ 'upload', 'change', 'clear' ]: + getattr(component, method)(update_preview_frame_slider, outputs = PREVIEW_FRAME_SLIDER) + select_component_names : List[ComponentName] =\ + [ + 'reference_face_position_gallery', + 'face_analyser_order_dropdown', + 'face_analyser_age_dropdown', + 'face_analyser_gender_dropdown' + ] + for component_name in select_component_names: + component = get_ui_component(component_name) + if component: + component.select(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + change_one_component_names : List[ComponentName] =\ + [ + 'frame_processors_checkbox_group', + 'face_debugger_items_checkbox_group', + 'face_enhancer_model_dropdown', + 'face_enhancer_blend_slider', + 'frame_enhancer_model_dropdown', + 'frame_enhancer_blend_slider', + 'face_selector_mode_dropdown', + 'reference_face_distance_slider', + 'face_mask_blur_slider', + 'face_mask_padding_top_slider', + 'face_mask_padding_bottom_slider', + 'face_mask_padding_left_slider', + 'face_mask_padding_right_slider' + ] + for component_name in change_one_component_names: + component = get_ui_component(component_name) + if component: + component.change(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + change_two_component_names : List[ComponentName] =\ + [ + 'face_swapper_model_dropdown', + 'face_detector_model_dropdown', + 'face_detector_size_dropdown', + 'face_detector_score_slider' + ] + for component_name in change_two_component_names: + component = get_ui_component(component_name) + if component: + component.change(clear_and_update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + +def clear_and_update_preview_image(frame_number : int = 0) -> gradio.Image: + clear_face_analyser() + clear_face_reference() + clear_faces_cache() + return update_preview_image(frame_number) + + +def update_preview_image(frame_number : int = 0) -> gradio.Image: + conditional_set_face_reference() + source_face = get_one_face(read_static_image(facefusion.globals.source_path)) + reference_face = get_face_reference() if 'reference' in facefusion.globals.face_selector_mode else None + if is_image(facefusion.globals.target_path): + target_frame = read_static_image(facefusion.globals.target_path) + preview_frame = process_preview_frame(source_face, reference_face, target_frame) + preview_frame = normalize_frame_color(preview_frame) + return gradio.Image(value = preview_frame) + if is_video(facefusion.globals.target_path): + temp_frame = get_video_frame(facefusion.globals.target_path, frame_number) + preview_frame = process_preview_frame(source_face, reference_face, temp_frame) + preview_frame = normalize_frame_color(preview_frame) + return gradio.Image(value = preview_frame) + return gradio.Image(value = None) + + +def update_preview_frame_slider() -> gradio.Slider: + if is_video(facefusion.globals.target_path): + video_frame_total = count_video_frame_total(facefusion.globals.target_path) + return gradio.Slider(maximum = video_frame_total, visible = True) + return gradio.Slider(value = None, maximum = None, visible = False) + + +def process_preview_frame(source_face : Face, reference_face : Face, temp_frame : Frame) -> Frame: + temp_frame = resize_frame_dimension(temp_frame, 640, 640) + # if analyse_frame(temp_frame): + # return cv2.GaussianBlur(temp_frame, (99, 99), 0) + from facefusion.api import model + model.print_globals() + for frame_processor in facefusion.globals.frame_processors: + frame_processor_module = load_frame_processor_module(frame_processor) + if frame_processor_module.pre_process('preview'): + temp_frame = frame_processor_module.process_frame( + source_face, + reference_face, + temp_frame + ) + return temp_frame diff --git a/facefusion/uis/components/source.py b/facefusion/uis/components/source.py new file mode 100644 index 0000000000000000000000000000000000000000..37777ea45ed7f620b214ba0c60f2d5679b6feaf2 --- /dev/null +++ b/facefusion/uis/components/source.py @@ -0,0 +1,46 @@ +from typing import Any, IO, Optional +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.utilities import is_image +from facefusion.uis.core import register_ui_component + +SOURCE_FILE : Optional[gradio.File] = None +SOURCE_IMAGE : Optional[gradio.Image] = None + + +def render() -> None: + global SOURCE_FILE + global SOURCE_IMAGE + + is_source_image = is_image(facefusion.globals.source_path) + SOURCE_FILE = gradio.File( + file_count = 'single', + file_types = + [ + '.png', + '.jpg', + '.webp' + ], + label = wording.get('source_file_label'), + value = facefusion.globals.source_path if is_source_image else None + ) + SOURCE_IMAGE = gradio.Image( + value = SOURCE_FILE.value['name'] if is_source_image else None, + visible = is_source_image, + show_label = False + ) + register_ui_component('source_image', SOURCE_IMAGE) + + +def listen() -> None: + SOURCE_FILE.change(update, inputs = SOURCE_FILE, outputs = SOURCE_IMAGE) + + +def update(file: IO[Any]) -> gradio.Image: + if file and is_image(file.name): + facefusion.globals.source_path = file.name + return gradio.Image(value = file.name, visible = True) + facefusion.globals.source_path = None + return gradio.Image(value = None, visible = False) diff --git a/facefusion/uis/components/target.py b/facefusion/uis/components/target.py new file mode 100644 index 0000000000000000000000000000000000000000..b89ac187112e3fa81c86f9877c6c410860af040b --- /dev/null +++ b/facefusion/uis/components/target.py @@ -0,0 +1,63 @@ +from typing import Any, IO, Tuple, Optional +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.face_cache import clear_faces_cache +from facefusion.face_reference import clear_face_reference +from facefusion.utilities import is_image, is_video +from facefusion.uis.core import register_ui_component + +TARGET_FILE : Optional[gradio.File] = None +TARGET_IMAGE : Optional[gradio.Image] = None +TARGET_VIDEO : Optional[gradio.Video] = None + + +def render() -> None: + global TARGET_FILE + global TARGET_IMAGE + global TARGET_VIDEO + + is_target_image = is_image(facefusion.globals.target_path) + is_target_video = is_video(facefusion.globals.target_path) + TARGET_FILE = gradio.File( + label = wording.get('target_file_label'), + file_count = 'single', + file_types = + [ + '.png', + '.jpg', + '.webp', + '.mp4' + ], + value = facefusion.globals.target_path if is_target_image or is_target_video else None + ) + TARGET_IMAGE = gradio.Image( + value = TARGET_FILE.value['name'] if is_target_image else None, + visible = is_target_image, + show_label = False + ) + TARGET_VIDEO = gradio.Video( + value = TARGET_FILE.value['name'] if is_target_video else None, + visible = is_target_video, + show_label = False + ) + register_ui_component('target_image', TARGET_IMAGE) + register_ui_component('target_video', TARGET_VIDEO) + + +def listen() -> None: + TARGET_FILE.change(update, inputs = TARGET_FILE, outputs = [ TARGET_IMAGE, TARGET_VIDEO ]) + + +def update(file : IO[Any]) -> Tuple[gradio.Image, gradio.Video]: + clear_face_reference() + clear_faces_cache() + if file and is_image(file.name): + facefusion.globals.target_path = file.name + return gradio.Image(value = file.name, visible = True), gradio.Video(value = None, visible = False) + if file and is_video(file.name): + facefusion.globals.target_path = file.name + return gradio.Image(value = None, visible = False), gradio.Video(value = file.name, visible = True) + facefusion.globals.target_path = None + return gradio.Image(value = None, visible = False), gradio.Video(value = None, visible = False) diff --git a/facefusion/uis/components/temp_frame.py b/facefusion/uis/components/temp_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..dfab64fe42970ba2c7f22caa2baaf9ec4bea1c84 --- /dev/null +++ b/facefusion/uis/components/temp_frame.py @@ -0,0 +1,55 @@ +from typing import Optional, Tuple +import gradio + +import facefusion.globals +import facefusion.choices +from facefusion import wording +from facefusion.typing import TempFrameFormat +from facefusion.utilities import is_video +from facefusion.uis.core import get_ui_component + +TEMP_FRAME_FORMAT_DROPDOWN : Optional[gradio.Dropdown] = None +TEMP_FRAME_QUALITY_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global TEMP_FRAME_FORMAT_DROPDOWN + global TEMP_FRAME_QUALITY_SLIDER + + TEMP_FRAME_FORMAT_DROPDOWN = gradio.Dropdown( + label = wording.get('temp_frame_format_dropdown_label'), + choices = facefusion.choices.temp_frame_formats, + value = facefusion.globals.temp_frame_format, + visible = is_video(facefusion.globals.target_path) + ) + TEMP_FRAME_QUALITY_SLIDER = gradio.Slider( + label = wording.get('temp_frame_quality_slider_label'), + value = facefusion.globals.temp_frame_quality, + step = facefusion.choices.temp_frame_quality_range[1] - facefusion.choices.temp_frame_quality_range[0], + minimum = facefusion.choices.temp_frame_quality_range[0], + maximum = facefusion.choices.temp_frame_quality_range[-1], + visible = is_video(facefusion.globals.target_path) + ) + + +def listen() -> None: + TEMP_FRAME_FORMAT_DROPDOWN.select(update_temp_frame_format, inputs = TEMP_FRAME_FORMAT_DROPDOWN) + TEMP_FRAME_QUALITY_SLIDER.change(update_temp_frame_quality, inputs = TEMP_FRAME_QUALITY_SLIDER) + target_video = get_ui_component('target_video') + if target_video: + for method in [ 'upload', 'change', 'clear' ]: + getattr(target_video, method)(remote_update, outputs = [ TEMP_FRAME_FORMAT_DROPDOWN, TEMP_FRAME_QUALITY_SLIDER ]) + + +def remote_update() -> Tuple[gradio.Dropdown, gradio.Slider]: + if is_video(facefusion.globals.target_path): + return gradio.Dropdown(visible = True), gradio.Slider(visible = True) + return gradio.Dropdown(visible = False), gradio.Slider(visible = False) + + +def update_temp_frame_format(temp_frame_format : TempFrameFormat) -> None: + facefusion.globals.temp_frame_format = temp_frame_format + + +def update_temp_frame_quality(temp_frame_quality : int) -> None: + facefusion.globals.temp_frame_quality = temp_frame_quality diff --git a/facefusion/uis/components/trim_frame.py b/facefusion/uis/components/trim_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..1e6048c3fa74c5bcd1510aff52f5d13a2ea53404 --- /dev/null +++ b/facefusion/uis/components/trim_frame.py @@ -0,0 +1,71 @@ +from typing import Any, Dict, Tuple, Optional +import gradio + +import facefusion.globals +from facefusion import wording +from facefusion.vision import count_video_frame_total +from facefusion.utilities import is_video +from facefusion.uis.core import get_ui_component + +TRIM_FRAME_START_SLIDER : Optional[gradio.Slider] = None +TRIM_FRAME_END_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global TRIM_FRAME_START_SLIDER + global TRIM_FRAME_END_SLIDER + + trim_frame_start_slider_args : Dict[str, Any] =\ + { + 'label': wording.get('trim_frame_start_slider_label'), + 'step': 1, + 'minimum': 0, + 'maximum': 100, + 'visible': False + } + trim_frame_end_slider_args : Dict[str, Any] =\ + { + 'label': wording.get('trim_frame_end_slider_label'), + 'step': 1, + 'minimum': 0, + 'maximum': 100, + 'visible': False + } + if is_video(facefusion.globals.target_path): + video_frame_total = count_video_frame_total(facefusion.globals.target_path) + trim_frame_start_slider_args['value'] = facefusion.globals.trim_frame_start or 0 + trim_frame_start_slider_args['maximum'] = video_frame_total + trim_frame_start_slider_args['visible'] = True + trim_frame_end_slider_args['value'] = facefusion.globals.trim_frame_end or video_frame_total + trim_frame_end_slider_args['maximum'] = video_frame_total + trim_frame_end_slider_args['visible'] = True + with gradio.Row(): + TRIM_FRAME_START_SLIDER = gradio.Slider(**trim_frame_start_slider_args) + TRIM_FRAME_END_SLIDER = gradio.Slider(**trim_frame_end_slider_args) + + +def listen() -> None: + TRIM_FRAME_START_SLIDER.change(update_trim_frame_start, inputs = TRIM_FRAME_START_SLIDER) + TRIM_FRAME_END_SLIDER.change(update_trim_frame_end, inputs = TRIM_FRAME_END_SLIDER) + target_video = get_ui_component('target_video') + if target_video: + for method in [ 'upload', 'change', 'clear' ]: + getattr(target_video, method)(remote_update, outputs = [ TRIM_FRAME_START_SLIDER, TRIM_FRAME_END_SLIDER ]) + + +def remote_update() -> Tuple[gradio.Slider, gradio.Slider]: + if is_video(facefusion.globals.target_path): + video_frame_total = count_video_frame_total(facefusion.globals.target_path) + facefusion.globals.trim_frame_start = None + facefusion.globals.trim_frame_end = None + return gradio.Slider(value = 0, maximum = video_frame_total, visible = True), gradio.Slider(value = video_frame_total, maximum = video_frame_total, visible = True) + return gradio.Slider(value = None, maximum = None, visible = False), gradio.Slider(value = None, maximum = None, visible = False) + + +def update_trim_frame_start(trim_frame_start : int) -> None: + facefusion.globals.trim_frame_start = trim_frame_start if trim_frame_start > 0 else None + + +def update_trim_frame_end(trim_frame_end : int) -> None: + video_frame_total = count_video_frame_total(facefusion.globals.target_path) + facefusion.globals.trim_frame_end = trim_frame_end if trim_frame_end < video_frame_total else None diff --git a/facefusion/uis/components/webcam.py b/facefusion/uis/components/webcam.py new file mode 100644 index 0000000000000000000000000000000000000000..0b7ba8d0aef04dd2700abf4b37e56b90db6e880f --- /dev/null +++ b/facefusion/uis/components/webcam.py @@ -0,0 +1,147 @@ +from typing import Optional, Generator, Deque +from concurrent.futures import ThreadPoolExecutor +from collections import deque +import os +import platform +import subprocess +import cv2 +import gradio +from tqdm import tqdm + +import facefusion.globals +from facefusion import wording +from facefusion.content_analyser import analyse_stream +from facefusion.typing import Frame, Face +from facefusion.face_analyser import get_one_face +from facefusion.processors.frame.core import get_frame_processors_modules +from facefusion.utilities import open_ffmpeg +from facefusion.vision import normalize_frame_color, read_static_image +from facefusion.uis.typing import StreamMode, WebcamMode +from facefusion.uis.core import get_ui_component + +WEBCAM_CAPTURE : Optional[cv2.VideoCapture] = None +WEBCAM_IMAGE : Optional[gradio.Image] = None +WEBCAM_START_BUTTON : Optional[gradio.Button] = None +WEBCAM_STOP_BUTTON : Optional[gradio.Button] = None + + +def get_webcam_capture() -> Optional[cv2.VideoCapture]: + global WEBCAM_CAPTURE + + if WEBCAM_CAPTURE is None: + if platform.system().lower() == 'windows': + webcam_capture = cv2.VideoCapture(0, cv2.CAP_DSHOW) + else: + webcam_capture = cv2.VideoCapture(0) + if webcam_capture and webcam_capture.isOpened(): + WEBCAM_CAPTURE = webcam_capture + return WEBCAM_CAPTURE + + +def clear_webcam_capture() -> None: + global WEBCAM_CAPTURE + + if WEBCAM_CAPTURE: + WEBCAM_CAPTURE.release() + WEBCAM_CAPTURE = None + + +def render() -> None: + global WEBCAM_IMAGE + global WEBCAM_START_BUTTON + global WEBCAM_STOP_BUTTON + + WEBCAM_IMAGE = gradio.Image( + label = wording.get('webcam_image_label') + ) + WEBCAM_START_BUTTON = gradio.Button( + value = wording.get('start_button_label'), + variant = 'primary', + size = 'sm' + ) + WEBCAM_STOP_BUTTON = gradio.Button( + value = wording.get('stop_button_label'), + size = 'sm' + ) + + +def listen() -> None: + start_event = None + webcam_mode_radio = get_ui_component('webcam_mode_radio') + webcam_resolution_dropdown = get_ui_component('webcam_resolution_dropdown') + webcam_fps_slider = get_ui_component('webcam_fps_slider') + if webcam_mode_radio and webcam_resolution_dropdown and webcam_fps_slider: + start_event = WEBCAM_START_BUTTON.click(start, inputs = [ webcam_mode_radio, webcam_resolution_dropdown, webcam_fps_slider ], outputs = WEBCAM_IMAGE) + WEBCAM_STOP_BUTTON.click(stop, cancels = start_event) + source_image = get_ui_component('source_image') + if source_image: + for method in [ 'upload', 'change', 'clear' ]: + getattr(source_image, method)(stop, cancels = start_event) + + +def start(mode : WebcamMode, resolution : str, fps : float) -> Generator[Frame, None, None]: + facefusion.globals.face_selector_mode = 'one' + facefusion.globals.face_analyser_order = 'large-small' + source_face = get_one_face(read_static_image(facefusion.globals.source_path)) + stream = None + if mode in [ 'udp', 'v4l2' ]: + stream = open_stream(mode, resolution, fps) # type: ignore[arg-type] + webcam_width, webcam_height = map(int, resolution.split('x')) + webcam_capture = get_webcam_capture() + if webcam_capture and webcam_capture.isOpened(): + webcam_capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) # type: ignore[attr-defined] + webcam_capture.set(cv2.CAP_PROP_FRAME_WIDTH, webcam_width) + webcam_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, webcam_height) + webcam_capture.set(cv2.CAP_PROP_FPS, fps) + for capture_frame in multi_process_capture(source_face, webcam_capture, fps): + if mode == 'inline': + yield normalize_frame_color(capture_frame) + else: + stream.stdin.write(capture_frame.tobytes()) + yield None + + +def multi_process_capture(source_face : Face, webcam_capture : cv2.VideoCapture, fps : float) -> Generator[Frame, None, None]: + with tqdm(desc = wording.get('processing'), unit = 'frame', ascii = ' =') as progress: + with ThreadPoolExecutor(max_workers = facefusion.globals.execution_thread_count) as executor: + futures = [] + deque_capture_frames : Deque[Frame] = deque() + while webcam_capture and webcam_capture.isOpened(): + _, capture_frame = webcam_capture.read() + if analyse_stream(capture_frame, fps): + return + future = executor.submit(process_stream_frame, source_face, capture_frame) + futures.append(future) + for future_done in [ future for future in futures if future.done() ]: + capture_frame = future_done.result() + deque_capture_frames.append(capture_frame) + futures.remove(future_done) + while deque_capture_frames: + progress.update() + yield deque_capture_frames.popleft() + + +def stop() -> gradio.Image: + clear_webcam_capture() + return gradio.Image(value = None) + + +def process_stream_frame(source_face : Face, temp_frame : Frame) -> Frame: + for frame_processor_module in get_frame_processors_modules(facefusion.globals.frame_processors): + if frame_processor_module.pre_process('stream'): + temp_frame = frame_processor_module.process_frame( + source_face, + None, + temp_frame + ) + return temp_frame + + +def open_stream(mode : StreamMode, resolution : str, fps : float) -> subprocess.Popen[bytes]: + commands = [ '-f', 'rawvideo', '-pix_fmt', 'bgr24', '-s', resolution, '-r', str(fps), '-i', '-' ] + if mode == 'udp': + commands.extend([ '-b:v', '2000k', '-f', 'mpegts', 'udp://localhost:27000?pkt_size=1316' ]) + if mode == 'v4l2': + device_name = os.listdir('/sys/devices/virtual/video4linux')[0] + commands.extend([ '-f', 'v4l2', '/dev/' + device_name ]) + return open_ffmpeg(commands) diff --git a/facefusion/uis/components/webcam_options.py b/facefusion/uis/components/webcam_options.py new file mode 100644 index 0000000000000000000000000000000000000000..edb245c811ff85c6ceffa58f35ecae692b357cb6 --- /dev/null +++ b/facefusion/uis/components/webcam_options.py @@ -0,0 +1,37 @@ +from typing import Optional +import gradio + +from facefusion import wording +from facefusion.uis import choices as uis_choices +from facefusion.uis.core import register_ui_component + +WEBCAM_MODE_RADIO : Optional[gradio.Radio] = None +WEBCAM_RESOLUTION_DROPDOWN : Optional[gradio.Dropdown] = None +WEBCAM_FPS_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global WEBCAM_MODE_RADIO + global WEBCAM_RESOLUTION_DROPDOWN + global WEBCAM_FPS_SLIDER + + WEBCAM_MODE_RADIO = gradio.Radio( + label = wording.get('webcam_mode_radio_label'), + choices = uis_choices.webcam_modes, + value = 'inline' + ) + WEBCAM_RESOLUTION_DROPDOWN = gradio.Dropdown( + label = wording.get('webcam_resolution_dropdown'), + choices = uis_choices.webcam_resolutions, + value = uis_choices.webcam_resolutions[0] + ) + WEBCAM_FPS_SLIDER = gradio.Slider( + label = wording.get('webcam_fps_slider'), + value = 25, + step = 1, + minimum = 1, + maximum = 60 + ) + register_ui_component('webcam_mode_radio', WEBCAM_MODE_RADIO) + register_ui_component('webcam_resolution_dropdown', WEBCAM_RESOLUTION_DROPDOWN) + register_ui_component('webcam_fps_slider', WEBCAM_FPS_SLIDER) diff --git a/facefusion/uis/core.py b/facefusion/uis/core.py new file mode 100644 index 0000000000000000000000000000000000000000..d8b565e0ff97cff62265396e86e7e8ca853b3c2d --- /dev/null +++ b/facefusion/uis/core.py @@ -0,0 +1,130 @@ +from typing import Dict, Optional, Any, List +from types import ModuleType +import importlib +import sys +import gradio + +import facefusion.globals +from facefusion import metadata, wording +from facefusion.uis.typing import Component, ComponentName +from facefusion.utilities import resolve_relative_path + +UI_COMPONENTS: Dict[ComponentName, Component] = {} +UI_LAYOUT_MODULES : List[ModuleType] = [] +UI_LAYOUT_METHODS =\ +[ + 'pre_check', + 'pre_render', + 'render', + 'listen', + 'run' +] + + +def load_ui_layout_module(ui_layout : str) -> Any: + try: + ui_layout_module = importlib.import_module('facefusion.uis.layouts.' + ui_layout) + for method_name in UI_LAYOUT_METHODS: + if not hasattr(ui_layout_module, method_name): + raise NotImplementedError + except ModuleNotFoundError: + sys.exit(wording.get('ui_layout_not_loaded').format(ui_layout = ui_layout)) + except NotImplementedError: + sys.exit(wording.get('ui_layout_not_implemented').format(ui_layout = ui_layout)) + return ui_layout_module + + +def get_ui_layouts_modules(ui_layouts : List[str]) -> List[ModuleType]: + global UI_LAYOUT_MODULES + + if not UI_LAYOUT_MODULES: + for ui_layout in ui_layouts: + ui_layout_module = load_ui_layout_module(ui_layout) + UI_LAYOUT_MODULES.append(ui_layout_module) + return UI_LAYOUT_MODULES + + +def get_ui_component(name : ComponentName) -> Optional[Component]: + if name in UI_COMPONENTS: + return UI_COMPONENTS[name] + return None + + +def register_ui_component(name : ComponentName, component: Component) -> None: + UI_COMPONENTS[name] = component + + +def launch() -> None: + with gradio.Blocks(theme = get_theme(), css = get_css(), title = metadata.get('name') + ' ' + metadata.get('version')) as ui: + for ui_layout in facefusion.globals.ui_layouts: + ui_layout_module = load_ui_layout_module(ui_layout) + if ui_layout_module.pre_render(): + ui_layout_module.render() + ui_layout_module.listen() + + for ui_layout in facefusion.globals.ui_layouts: + ui_layout_module = load_ui_layout_module(ui_layout) + ui_layout_module.run(ui) + + +def get_theme() -> gradio.Theme: + return gradio.themes.Base( + primary_hue = gradio.themes.colors.red, + secondary_hue = gradio.themes.colors.neutral, + font = gradio.themes.GoogleFont('Open Sans') + ).set( + background_fill_primary = '*neutral_100', + block_background_fill = 'white', + block_border_width = '0', + block_label_background_fill = '*primary_100', + block_label_background_fill_dark = '*primary_600', + block_label_border_width = 'none', + block_label_margin = '0.5rem', + block_label_radius = '*radius_md', + block_label_text_color = '*primary_500', + block_label_text_color_dark = 'white', + block_label_text_weight = '600', + block_title_background_fill = '*primary_100', + block_title_background_fill_dark = '*primary_600', + block_title_padding = '*block_label_padding', + block_title_radius = '*block_label_radius', + block_title_text_color = '*primary_500', + block_title_text_size = '*text_sm', + block_title_text_weight = '600', + block_padding = '0.5rem', + border_color_primary = 'transparent', + border_color_primary_dark = 'transparent', + button_large_padding = '2rem 0.5rem', + button_large_text_weight = 'normal', + button_primary_background_fill = '*primary_500', + button_primary_text_color = 'white', + button_secondary_background_fill = 'white', + button_secondary_border_color = 'transparent', + button_secondary_border_color_dark = 'transparent', + button_secondary_border_color_hover = 'transparent', + button_secondary_border_color_hover_dark = 'transparent', + button_secondary_text_color = '*neutral_800', + button_small_padding = '0.75rem', + checkbox_background_color = '*neutral_200', + checkbox_background_color_selected = '*primary_600', + checkbox_background_color_selected_dark = '*primary_700', + checkbox_border_color_focus = '*primary_500', + checkbox_border_color_focus_dark = '*primary_600', + checkbox_border_color_selected = '*primary_600', + checkbox_border_color_selected_dark = '*primary_700', + checkbox_label_background_fill = '*neutral_50', + checkbox_label_background_fill_hover = '*neutral_50', + checkbox_label_background_fill_selected = '*primary_500', + checkbox_label_background_fill_selected_dark = '*primary_600', + checkbox_label_text_color_selected = 'white', + input_background_fill = '*neutral_50', + shadow_drop = 'none', + slider_color = '*primary_500', + slider_color_dark = '*primary_600' + ) + + +def get_css() -> str: + fixes_css_path = resolve_relative_path('uis/assets/fixes.css') + overrides_css_path = resolve_relative_path('uis/assets/overrides.css') + return open(fixes_css_path, 'r').read() + open(overrides_css_path, 'r').read() diff --git a/facefusion/uis/layouts/benchmark.py b/facefusion/uis/layouts/benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..829db2fd41fcd0c7dcb3e4b1bddef209b2e12d3b --- /dev/null +++ b/facefusion/uis/layouts/benchmark.py @@ -0,0 +1,63 @@ +import gradio + +import facefusion.globals +from facefusion.utilities import conditional_download +from facefusion.uis.components import about, frame_processors, frame_processors_options, execution, execution_thread_count, execution_queue_count, limit_resources, benchmark_options, benchmark + + +def pre_check() -> bool: + if not facefusion.globals.skip_download: + conditional_download('.assets/examples', + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-360p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-540p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-720p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-1080p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-1440p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-2160p.mp4' + ]) + return True + return False + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + frame_processors.render() + frame_processors_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + execution_queue_count.render() + with gradio.Blocks(): + limit_resources.render() + with gradio.Blocks(): + benchmark_options.render() + with gradio.Column(scale = 5): + with gradio.Blocks(): + benchmark.render() + return layout + + +def listen() -> None: + frame_processors.listen() + frame_processors_options.listen() + execution.listen() + execution_thread_count.listen() + execution_queue_count.listen() + limit_resources.listen() + benchmark.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.queue(concurrency_count = 2, api_open = False).launch(show_api = False) diff --git a/facefusion/uis/layouts/default.py b/facefusion/uis/layouts/default.py new file mode 100644 index 0000000000000000000000000000000000000000..3e9da2e76a1de7b117642eb0709f1d0fca6684d8 --- /dev/null +++ b/facefusion/uis/layouts/default.py @@ -0,0 +1,77 @@ +import gradio + +from facefusion.uis.components import about, frame_processors, frame_processors_options, execution, execution_thread_count, execution_queue_count, limit_resources, temp_frame, output_options, common_options, source, target, output, preview, trim_frame, face_analyser, face_selector, face_mask + + +def pre_check() -> bool: + return True + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + frame_processors.render() + frame_processors_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + execution_queue_count.render() + with gradio.Blocks(): + limit_resources.render() + with gradio.Blocks(): + temp_frame.render() + with gradio.Blocks(): + output_options.render() + with gradio.Blocks(): + common_options.render() + with gradio.Column(scale = 2): + with gradio.Blocks(): + source.render() + with gradio.Blocks(): + target.render() + with gradio.Blocks(): + output.render() + with gradio.Column(scale = 3): + with gradio.Blocks(): + preview.render() + with gradio.Blocks(): + trim_frame.render() + with gradio.Blocks(): + face_selector.render() + with gradio.Blocks(): + face_mask.render() + with gradio.Blocks(): + face_analyser.render() + return layout + + +def listen() -> None: + frame_processors.listen() + frame_processors_options.listen() + execution.listen() + execution_thread_count.listen() + execution_queue_count.listen() + limit_resources.listen() + temp_frame.listen() + output_options.listen() + common_options.listen() + source.listen() + target.listen() + output.listen() + preview.listen() + trim_frame.listen() + face_selector.listen() + face_mask.listen() + face_analyser.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.launch(show_api = False) diff --git a/facefusion/uis/layouts/webcam.py b/facefusion/uis/layouts/webcam.py new file mode 100644 index 0000000000000000000000000000000000000000..a5b6e184fbbec96b58e1d05474bbeab473229356 --- /dev/null +++ b/facefusion/uis/layouts/webcam.py @@ -0,0 +1,46 @@ +import gradio + +from facefusion.uis.components import about, frame_processors, frame_processors_options, execution, execution_thread_count, webcam_options, source, webcam + + +def pre_check() -> bool: + return True + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + frame_processors.render() + frame_processors_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + with gradio.Blocks(): + webcam_options.render() + with gradio.Blocks(): + source.render() + with gradio.Column(scale = 5): + with gradio.Blocks(): + webcam.render() + return layout + + +def listen() -> None: + frame_processors.listen() + frame_processors_options.listen() + execution.listen() + execution_thread_count.listen() + source.listen() + webcam.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.queue(concurrency_count = 2, api_open = False).launch(show_api = False) diff --git a/facefusion/uis/typing.py b/facefusion/uis/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..e104d0fcf613c2c0e18449c2bb88b31dce5808ca --- /dev/null +++ b/facefusion/uis/typing.py @@ -0,0 +1,40 @@ +from typing import Literal +import gradio + +Component = gradio.File or gradio.Image or gradio.Video or gradio.Slider +ComponentName = Literal\ +[ + 'source_image', + 'target_image', + 'target_video', + 'preview_frame_slider', + 'face_selector_mode_dropdown', + 'reference_face_position_gallery', + 'reference_face_distance_slider', + 'face_analyser_order_dropdown', + 'face_analyser_age_dropdown', + 'face_analyser_gender_dropdown', + 'face_detector_model_dropdown', + 'face_detector_size_dropdown', + 'face_detector_score_slider', + 'face_mask_blur_slider', + 'face_mask_padding_top_slider', + 'face_mask_padding_bottom_slider', + 'face_mask_padding_left_slider', + 'face_mask_padding_right_slider', + 'frame_processors_checkbox_group', + 'face_swapper_model_dropdown', + 'face_enhancer_model_dropdown', + 'face_enhancer_blend_slider', + 'frame_enhancer_model_dropdown', + 'frame_enhancer_blend_slider', + 'face_debugger_items_checkbox_group', + 'output_path_textbox', + 'benchmark_runs_checkbox_group', + 'benchmark_cycles_slider', + 'webcam_mode_radio', + 'webcam_resolution_dropdown', + 'webcam_fps_slider' +] +WebcamMode = Literal['inline', 'udp', 'v4l2'] +StreamMode = Literal['udp', 'v4l2'] diff --git a/facefusion/utilities.py b/facefusion/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..dcdaae13a7a5201e28e6f6c44c3a2f4c05d736eb --- /dev/null +++ b/facefusion/utilities.py @@ -0,0 +1,268 @@ +from typing import Any, List, Optional +from concurrent.futures import ThreadPoolExecutor +from functools import lru_cache +from pathlib import Path +from tqdm import tqdm +import glob +import filetype +import os +import platform +import shutil +import ssl +import subprocess +import tempfile +import urllib.request +import onnxruntime + +import facefusion.globals +from facefusion import wording +from facefusion.typing import Padding +from facefusion.vision import detect_fps + +TEMP_DIRECTORY_PATH = os.path.join(tempfile.gettempdir(), 'facefusion') +TEMP_OUTPUT_VIDEO_NAME = 'temp.mp4' + +# monkey patch ssl +if platform.system().lower() == 'darwin': + ssl._create_default_https_context = ssl._create_unverified_context + + +def run_ffmpeg(args : List[str]) -> bool: + commands = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error' ] + commands.extend(args) + try: + subprocess.run(commands, stderr = subprocess.PIPE, check = True) + return True + except subprocess.CalledProcessError: + return False + + +def open_ffmpeg(args : List[str]) -> subprocess.Popen[bytes]: + commands = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error' ] + commands.extend(args) + return subprocess.Popen(commands, stdin = subprocess.PIPE) + + +def extract_frames(target_path : str, fps : float) -> bool: + temp_frame_compression = round(31 - (facefusion.globals.temp_frame_quality * 0.31)) + trim_frame_start = facefusion.globals.trim_frame_start + trim_frame_end = facefusion.globals.trim_frame_end + temp_frames_pattern = get_temp_frames_pattern(target_path, '%04d') + commands = [ '-hwaccel', 'auto', '-i', target_path, '-q:v', str(temp_frame_compression), '-pix_fmt', 'rgb24' ] + if trim_frame_start is not None and trim_frame_end is not None: + commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ':end_frame=' + str(trim_frame_end) + ',fps=' + str(fps) ]) + elif trim_frame_start is not None: + commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ',fps=' + str(fps) ]) + elif trim_frame_end is not None: + commands.extend([ '-vf', 'trim=end_frame=' + str(trim_frame_end) + ',fps=' + str(fps) ]) + else: + commands.extend([ '-vf', 'fps=' + str(fps) ]) + commands.extend([ '-vsync', '0', temp_frames_pattern ]) + return run_ffmpeg(commands) + + +def compress_image(output_path : str) -> bool: + output_image_compression = round(31 - (facefusion.globals.output_image_quality * 0.31)) + commands = [ '-hwaccel', 'auto', '-i', output_path, '-q:v', str(output_image_compression), '-y', output_path ] + return run_ffmpeg(commands) + + +def merge_video(target_path : str, fps : float) -> bool: + temp_output_video_path = get_temp_output_video_path(target_path) + temp_frames_pattern = get_temp_frames_pattern(target_path, '%04d') + commands = [ '-hwaccel', 'auto', '-r', str(fps), '-i', temp_frames_pattern, '-c:v', facefusion.globals.output_video_encoder ] + if facefusion.globals.output_video_encoder in [ 'libx264', 'libx265' ]: + output_video_compression = round(51 - (facefusion.globals.output_video_quality * 0.51)) + commands.extend([ '-crf', str(output_video_compression) ]) + if facefusion.globals.output_video_encoder in [ 'libvpx-vp9' ]: + output_video_compression = round(63 - (facefusion.globals.output_video_quality * 0.63)) + commands.extend([ '-crf', str(output_video_compression) ]) + if facefusion.globals.output_video_encoder in [ 'h264_nvenc', 'hevc_nvenc' ]: + output_video_compression = round(51 - (facefusion.globals.output_video_quality * 0.51)) + commands.extend([ '-cq', str(output_video_compression) ]) + commands.extend([ '-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-y', temp_output_video_path ]) + return run_ffmpeg(commands) + + +def restore_audio(target_path : str, output_path : str) -> bool: + fps = detect_fps(target_path) + trim_frame_start = facefusion.globals.trim_frame_start + trim_frame_end = facefusion.globals.trim_frame_end + temp_output_video_path = get_temp_output_video_path(target_path) + commands = [ '-hwaccel', 'auto', '-i', temp_output_video_path ] + if trim_frame_start is not None: + start_time = trim_frame_start / fps + commands.extend([ '-ss', str(start_time) ]) + if trim_frame_end is not None: + end_time = trim_frame_end / fps + commands.extend([ '-to', str(end_time) ]) + commands.extend([ '-i', target_path, '-c', 'copy', '-map', '0:v:0', '-map', '1:a:0', '-shortest', '-y', output_path ]) + return run_ffmpeg(commands) + + +def get_temp_frame_paths(target_path : str) -> List[str]: + temp_frames_pattern = get_temp_frames_pattern(target_path, '*') + return sorted(glob.glob(temp_frames_pattern)) + + +def get_temp_frames_pattern(target_path : str, temp_frame_prefix : str) -> str: + temp_directory_path = get_temp_directory_path(target_path) + return os.path.join(temp_directory_path, temp_frame_prefix + '.' + facefusion.globals.temp_frame_format) + + +def get_temp_directory_path(target_path : str) -> str: + target_name, _ = os.path.splitext(os.path.basename(target_path)) + return os.path.join(TEMP_DIRECTORY_PATH, target_name) + + +def get_temp_output_video_path(target_path : str) -> str: + temp_directory_path = get_temp_directory_path(target_path) + return os.path.join(temp_directory_path, TEMP_OUTPUT_VIDEO_NAME) + + +def create_temp(target_path : str) -> None: + temp_directory_path = get_temp_directory_path(target_path) + Path(temp_directory_path).mkdir(parents = True, exist_ok = True) + + +def move_temp(target_path : str, output_path : str) -> None: + temp_output_video_path = get_temp_output_video_path(target_path) + if is_file(temp_output_video_path): + if is_file(output_path): + os.remove(output_path) + shutil.move(temp_output_video_path, output_path) + + +def clear_temp(target_path : str) -> None: + temp_directory_path = get_temp_directory_path(target_path) + parent_directory_path = os.path.dirname(temp_directory_path) + if not facefusion.globals.keep_temp and is_directory(temp_directory_path): + shutil.rmtree(temp_directory_path) + if os.path.exists(parent_directory_path) and not os.listdir(parent_directory_path): + os.rmdir(parent_directory_path) + + +def normalize_output_path(source_path : Optional[str], target_path : Optional[str], output_path : Optional[str]) -> Optional[str]: + if is_file(target_path) and is_directory(output_path): + target_name, target_extension = os.path.splitext(os.path.basename(target_path)) + if is_file(source_path): + source_name, _ = os.path.splitext(os.path.basename(source_path)) + return os.path.join(output_path, source_name + '-' + target_name + target_extension) + return os.path.join(output_path, target_name + target_extension) + if is_file(target_path) and output_path: + _, target_extension = os.path.splitext(os.path.basename(target_path)) + output_name, output_extension = os.path.splitext(os.path.basename(output_path)) + output_directory_path = os.path.dirname(output_path) + if is_directory(output_directory_path) and output_extension: + return os.path.join(output_directory_path, output_name + target_extension) + return None + return output_path + + +def normalize_padding(padding : Optional[List[int]]) -> Optional[Padding]: + if padding and len(padding) == 1: + return tuple([ padding[0], padding[0], padding[0], padding[0] ]) # type: ignore[return-value] + if padding and len(padding) == 2: + return tuple([ padding[0], padding[1], padding[0], padding[1] ]) # type: ignore[return-value] + if padding and len(padding) == 3: + return tuple([ padding[0], padding[1], padding[2], padding[1] ]) # type: ignore[return-value] + if padding and len(padding) == 4: + return tuple(padding) # type: ignore[return-value] + return None + + +def is_file(file_path : str) -> bool: + return bool(file_path and os.path.isfile(file_path)) + + +def is_directory(directory_path : str) -> bool: + return bool(directory_path and os.path.isdir(directory_path)) + + +def is_image(image_path : str) -> bool: + if is_file(image_path): + mimetype = filetype.guess(image_path).mime + return bool(mimetype and mimetype.startswith('image/')) + return False + + +def is_video(video_path : str) -> bool: + if is_file(video_path): + mimetype = filetype.guess(video_path).mime + return bool(mimetype and mimetype.startswith('video/')) + return False + + +def conditional_download(download_directory_path : str, urls : List[str]) -> None: + with ThreadPoolExecutor() as executor: + for url in urls: + executor.submit(get_download_size, url) + for url in urls: + download_file_path = os.path.join(download_directory_path, os.path.basename(url)) + total = get_download_size(url) + if is_file(download_file_path): + initial = os.path.getsize(download_file_path) + else: + initial = 0 + if initial < total: + with tqdm(total = total, initial = initial, desc = wording.get('downloading'), unit = 'B', unit_scale = True, unit_divisor = 1024, ascii = ' =') as progress: + subprocess.Popen([ 'curl', '--create-dirs', '--silent', '--insecure', '--location', '--continue-at', '-', '--output', download_file_path, url ]) + current = initial + while current < total: + if is_file(download_file_path): + current = os.path.getsize(download_file_path) + progress.update(current - progress.n) + + +@lru_cache(maxsize = None) +def get_download_size(url : str) -> int: + try: + response = urllib.request.urlopen(url, timeout = 10) + return int(response.getheader('Content-Length')) + except (OSError, ValueError): + return 0 + + +def is_download_done(url : str, file_path : str) -> bool: + if is_file(file_path): + return get_download_size(url) == os.path.getsize(file_path) + return False + + +def resolve_relative_path(path : str) -> str: + return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) + + +def list_module_names(path : str) -> Optional[List[str]]: + if os.path.exists(path): + files = os.listdir(path) + return [ Path(file).stem for file in files if not Path(file).stem.startswith(('.', '__')) ] + return None + + +def encode_execution_providers(execution_providers : List[str]) -> List[str]: + return [ execution_provider.replace('ExecutionProvider', '').lower() for execution_provider in execution_providers ] + + +def decode_execution_providers(execution_providers: List[str]) -> List[str]: + available_execution_providers = onnxruntime.get_available_providers() + encoded_execution_providers = encode_execution_providers(available_execution_providers) + return [ execution_provider for execution_provider, encoded_execution_provider in zip(available_execution_providers, encoded_execution_providers) if any(execution_provider in encoded_execution_provider for execution_provider in execution_providers) ] + + +def map_device(execution_providers : List[str]) -> str: + if 'CoreMLExecutionProvider' in execution_providers: + return 'mps' + if 'CUDAExecutionProvider' in execution_providers or 'ROCMExecutionProvider' in execution_providers : + return 'cuda' + if 'OpenVINOExecutionProvider' in execution_providers: + return 'mkl' + return 'cpu' + + +def create_metavar(ranges : List[Any]) -> str: + return '[' + str(ranges[0]) + '-' + str(ranges[-1]) + ']' + + +def update_status(message : str, scope : str = 'FACEFUSION.CORE') -> None: + print('[' + scope + '] ' + message) diff --git a/facefusion/vision.py b/facefusion/vision.py new file mode 100644 index 0000000000000000000000000000000000000000..f5ee547dfef24fd3142202e36c1637a933b075a2 --- /dev/null +++ b/facefusion/vision.py @@ -0,0 +1,67 @@ +from typing import Optional +from functools import lru_cache +import cv2 + +from facefusion.typing import Frame + + +def get_video_frame(video_path : str, frame_number : int = 0) -> Optional[Frame]: + if video_path: + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + frame_total = video_capture.get(cv2.CAP_PROP_FRAME_COUNT) + video_capture.set(cv2.CAP_PROP_POS_FRAMES, min(frame_total, frame_number - 1)) + has_frame, frame = video_capture.read() + video_capture.release() + if has_frame: + return frame + return None + + +def detect_fps(video_path : str) -> Optional[float]: + if video_path: + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + return video_capture.get(cv2.CAP_PROP_FPS) + return None + + +def count_video_frame_total(video_path : str) -> int: + if video_path: + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + video_frame_total = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT)) + video_capture.release() + return video_frame_total + return 0 + + +def normalize_frame_color(frame : Frame) -> Frame: + return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + +def resize_frame_dimension(frame : Frame, max_width : int, max_height : int) -> Frame: + height, width = frame.shape[:2] + if height > max_height or width > max_width: + scale = min(max_height / height, max_width / width) + new_width = int(width * scale) + new_height = int(height * scale) + return cv2.resize(frame, (new_width, new_height)) + return frame + + +@lru_cache(maxsize = 128) +def read_static_image(image_path : str) -> Optional[Frame]: + return read_image(image_path) + + +def read_image(image_path : str) -> Optional[Frame]: + if image_path: + return cv2.imread(image_path) + return None + + +def write_image(image_path : str, frame : Frame) -> bool: + if image_path: + return cv2.imwrite(image_path, frame) + return False diff --git a/facefusion/wording.py b/facefusion/wording.py new file mode 100644 index 0000000000000000000000000000000000000000..c7c739bdfbd076ab76631bbf07cda155b77c84d7 --- /dev/null +++ b/facefusion/wording.py @@ -0,0 +1,129 @@ +WORDING =\ +{ + 'python_not_supported': 'Python version is not supported, upgrade to {version} or higher', + 'ffmpeg_not_installed': 'FFMpeg is not installed', + 'install_dependency_help': 'select the variant of {dependency} to install', + 'source_help': 'select a source image', + 'target_help': 'select a target image or video', + 'output_help': 'specify the output file or directory', + 'frame_processors_help': 'choose from the available frame processors (choices: {choices}, ...)', + 'frame_processor_model_help': 'choose the model for the frame processor', + 'frame_processor_blend_help': 'specify the blend factor for the frame processor', + 'face_debugger_items_help': 'specify the face debugger items', + 'ui_layouts_help': 'choose from the available ui layouts (choices: {choices}, ...)', + 'keep_fps_help': 'preserve the frames per second (fps) of the target', + 'keep_temp_help': 'retain temporary frames after processing', + 'skip_audio_help': 'omit audio from the target', + 'face_analyser_order_help': 'specify the order used for the face analyser', + 'face_analyser_age_help': 'specify the age used for the face analyser', + 'face_analyser_gender_help': 'specify the gender used for the face analyser', + 'face_detector_model_help': 'specify the model used for the face detector', + 'face_detector_size_help': 'specify the size threshold used for the face detector', + 'face_detector_score_help': 'specify the score threshold used for the face detector', + 'face_selector_mode_help': 'specify the mode for the face selector', + 'reference_face_position_help': 'specify the position of the reference face', + 'reference_face_distance_help': 'specify the distance between the reference face and the target face', + 'reference_frame_number_help': 'specify the number of the reference frame', + 'face_mask_blur_help': 'specify the blur amount for face mask', + 'face_mask_padding_help': 'specify the face mask padding (top, right, bottom, left) in percent', + 'trim_frame_start_help': 'specify the start frame for extraction', + 'trim_frame_end_help': 'specify the end frame for extraction', + 'temp_frame_format_help': 'specify the image format used for frame extraction', + 'temp_frame_quality_help': 'specify the image quality used for frame extraction', + 'output_image_quality_help': 'specify the quality used for the output image', + 'output_video_encoder_help': 'specify the encoder used for the output video', + 'output_video_quality_help': 'specify the quality used for the output video', + 'max_memory_help': 'specify the maximum amount of ram to be used (in gb)', + 'execution_providers_help': 'choose from the available execution providers', + 'execution_thread_count_help': 'specify the number of execution threads', + 'execution_queue_count_help': 'specify the number of execution queries', + 'skip_download_help': 'omit automate downloads and lookups', + 'headless_help': 'run the program in headless mode', + 'creating_temp': 'Creating temporary resources', + 'extracting_frames_fps': 'Extracting frames with {fps} FPS', + 'analysing': 'Analysing', + 'processing': 'Processing', + 'downloading': 'Downloading', + 'temp_frames_not_found': 'Temporary frames not found', + 'compressing_image': 'Compressing image', + 'compressing_image_failed': 'Compressing image failed', + 'merging_video_fps': 'Merging video with {fps} FPS', + 'merging_video_failed': 'Merging video failed', + 'skipping_audio': 'Skipping audio', + 'restoring_audio': 'Restoring audio', + 'restoring_audio_failed': 'Restoring audio failed', + 'clearing_temp': 'Clearing temporary resources', + 'processing_image_succeed': 'Processing to image succeed', + 'processing_image_failed': 'Processing to image failed', + 'processing_video_succeed': 'Processing to video succeed', + 'processing_video_failed': 'Processing to video failed', + 'model_download_not_done': 'Download of the model is not done', + 'model_file_not_present': 'File of the model is not present', + 'select_image_source': 'Select an image for source path', + 'select_image_or_video_target': 'Select an image or video for target path', + 'select_file_or_directory_output': 'Select an file or directory for output path', + 'no_source_face_detected': 'No source face detected', + 'frame_processor_not_loaded': 'Frame processor {frame_processor} could not be loaded', + 'frame_processor_not_implemented': 'Frame processor {frame_processor} not implemented correctly', + 'ui_layout_not_loaded': 'UI layout {ui_layout} could not be loaded', + 'ui_layout_not_implemented': 'UI layout {ui_layout} not implemented correctly', + 'donate_button_label': 'DONATE', + 'start_button_label': 'START', + 'stop_button_label': 'STOP', + 'clear_button_label': 'CLEAR', + 'benchmark_runs_checkbox_group_label': 'BENCHMARK RUNS', + 'benchmark_results_dataframe_label': 'BENCHMARK RESULTS', + 'benchmark_cycles_slider_label': 'BENCHMARK CYCLES', + 'execution_providers_checkbox_group_label': 'EXECUTION PROVIDERS', + 'execution_thread_count_slider_label': 'EXECUTION THREAD COUNT', + 'execution_queue_count_slider_label': 'EXECUTION QUEUE COUNT', + 'face_analyser_order_dropdown_label': 'FACE ANALYSER ORDER', + 'face_analyser_age_dropdown_label': 'FACE ANALYSER AGE', + 'face_analyser_gender_dropdown_label': 'FACE ANALYSER GENDER', + 'face_detector_model_dropdown_label': 'FACE DETECTOR MODEL', + 'face_detector_size_dropdown_label': 'FACE DETECTOR SIZE', + 'face_detector_score_slider_label': 'FACE DETECTOR SCORE', + 'face_selector_mode_dropdown_label': 'FACE SELECTOR MODE', + 'reference_face_gallery_label': 'REFERENCE FACE', + 'reference_face_distance_slider_label': 'REFERENCE FACE DISTANCE', + 'face_mask_blur_slider_label': 'FACE MASK BLUR', + 'face_mask_padding_top_slider_label': 'FACE MASK PADDING TOP', + 'face_mask_padding_bottom_slider_label': 'FACE MASK PADDING BOTTOM', + 'face_mask_padding_left_slider_label': 'FACE MASK PADDING LEFT', + 'face_mask_padding_right_slider_label': 'FACE MASK PADDING RIGHT', + 'max_memory_slider_label': 'MAX MEMORY', + 'output_image_or_video_label': 'OUTPUT', + 'output_path_textbox_label': 'OUTPUT PATH', + 'output_image_quality_slider_label': 'OUTPUT IMAGE QUALITY', + 'output_video_encoder_dropdown_label': 'OUTPUT VIDEO ENCODER', + 'output_video_quality_slider_label': 'OUTPUT VIDEO QUALITY', + 'preview_image_label': 'PREVIEW', + 'preview_frame_slider_label': 'PREVIEW FRAME', + 'frame_processors_checkbox_group_label': 'FRAME PROCESSORS', + 'face_swapper_model_dropdown_label': 'FACE SWAPPER MODEL', + 'face_enhancer_model_dropdown_label': 'FACE ENHANCER MODEL', + 'face_enhancer_blend_slider_label': 'FACE ENHANCER BLEND', + 'frame_enhancer_model_dropdown_label': 'FRAME ENHANCER MODEL', + 'frame_enhancer_blend_slider_label': 'FRAME ENHANCER BLEND', + 'face_debugger_items_checkbox_group_label': 'FACE DEBUGGER ITEMS', + 'common_options_checkbox_group_label': 'OPTIONS', + 'temp_frame_format_dropdown_label': 'TEMP FRAME FORMAT', + 'temp_frame_quality_slider_label': 'TEMP FRAME QUALITY', + 'trim_frame_start_slider_label': 'TRIM FRAME START', + 'trim_frame_end_slider_label': 'TRIM FRAME END', + 'source_file_label': 'SOURCE', + 'target_file_label': 'TARGET', + 'webcam_image_label': 'WEBCAM', + 'webcam_mode_radio_label': 'WEBCAM MODE', + 'webcam_resolution_dropdown': 'WEBCAM RESOLUTION', + 'webcam_fps_slider': 'WEBCAM FPS', + 'point': '.', + 'comma': ',', + 'colon': ':', + 'question_mark': '?', + 'exclamation_mark': '!' +} + + +def get(key : str) -> str: + return WORDING[key] diff --git a/install.py b/install.py new file mode 100644 index 0000000000000000000000000000000000000000..307f686fa5ed6409029975433c9c76a6a735c656 --- /dev/null +++ b/install.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from facefusion import installer + +if __name__ == '__main__': + installer.cli() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000000000000000000000000000000000..64218bc23688632a08c98ec4a0451ed46f8ed5e5 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +check_untyped_defs = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +ignore_missing_imports = True +strict_optional = False diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3751d5510beefd0a37e1c778193d10e94c2956b9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,106 @@ +absl-py==2.0.0 +addict==2.4.0 +aiofiles==23.2.1 +altair==5.2.0 +annotated-types==0.6.0 +anyio==3.7.1 +attrs==23.1.0 +basicsr==1.4.2 +cachetools==5.3.2 +certifi==2023.11.17 +charset-normalizer==3.3.2 +click==8.1.7 +coloredlogs==15.0.1 +contourpy==1.2.0 +cycler==0.12.1 +exceptiongroup==1.2.0 +facexlib==0.3.0 +fastapi==0.105.0 +ffmpy==0.3.1 +filelock==3.13.1 +filetype==1.2.0 +filterpy==1.4.5 +flatbuffers==23.5.26 +fonttools==4.46.0 +fsspec==2023.12.2 +future==0.18.3 +gfpgan==1.3.8 +google-auth==2.25.2 +google-auth-oauthlib==1.2.0 +gradio==3.50.2 +gradio_client==0.6.1 +grpcio==1.60.0 +h11==0.14.0 +httpcore==1.0.2 +httpx==0.25.2 +huggingface-hub==0.19.4 +humanfriendly==10.0 +idna==3.6 +imageio==2.33.1 +importlib-metadata==7.0.0 +importlib-resources==6.1.1 +Jinja2==3.1.2 +jsonschema==4.20.0 +jsonschema-specifications==2023.11.2 +kiwisolver==1.4.5 +lazy_loader==0.3 +llvmlite==0.41.1 +lmdb==1.4.1 +Markdown==3.5.1 +MarkupSafe==2.1.3 +matplotlib==3.8.2 +mpmath==1.3.0 +networkx==3.2.1 +numba==0.58.1 +numpy==1.26.1 +oauthlib==3.2.2 +onnx==1.15.0 +onnxruntime==1.16.0 +opencv-python==4.8.1.78 +orjson==3.9.10 +packaging==23.2 +pandas==2.1.4 +Pillow==10.1.0 +platformdirs==4.1.0 +protobuf==4.25.1 +psutil==5.9.6 +pyasn1==0.5.1 +pyasn1-modules==0.3.0 +pydantic==2.5.2 +pydantic_core==2.14.5 +pydub==0.25.1 +pyparsing==3.1.1 +python-dateutil==2.8.2 +python-multipart==0.0.6 +pytz==2023.3.post1 +PyYAML==6.0.1 +realesrgan==0.3.0 +referencing==0.32.0 +requests==2.31.0 +requests-oauthlib==1.3.1 +rpds-py==0.13.2 +rsa==4.9 +scikit-image==0.22.0 +scipy==1.11.4 +semantic-version==2.10.0 +six==1.16.0 +sniffio==1.3.0 +starlette==0.27.0 +sympy==1.12 +tb-nightly==2.16.0a20231213 +tensorboard-data-server==0.7.2 +tf-keras-nightly==2.16.0.dev2023121310 +tifffile==2023.12.9 +tomli==2.0.1 +toolz==0.12.0 +torch==2.1.0 +torchvision==0.16.0 +tqdm==4.66.1 +typing_extensions==4.9.0 +tzdata==2023.3 +urllib3==2.1.0 +uvicorn==0.24.0.post1 +websockets==11.0.3 +Werkzeug==3.0.1 +yapf==0.40.2 +zipp==3.17.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..3b796757e894649c07f2f23d42563319e0880eec --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from facefusion import core + +if __name__ == '__main__': + core.cli() diff --git a/temp/target/test-1703080901.png b/temp/target/test-1703080901.png new file mode 100644 index 0000000000000000000000000000000000000000..b93d72002d1fd8c691151d834306a6cffd195bf6 --- /dev/null +++ b/temp/target/test-1703080901.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e9e1eb1c9c5a2ade03fa10a7ebd66fc180fa856482209791f690f6f009f265 +size 1344720 diff --git a/temp/target/test-1703080957.png b/temp/target/test-1703080957.png new file mode 100644 index 0000000000000000000000000000000000000000..b93d72002d1fd8c691151d834306a6cffd195bf6 --- /dev/null +++ b/temp/target/test-1703080957.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e9e1eb1c9c5a2ade03fa10a7ebd66fc180fa856482209791f690f6f009f265 +size 1344720 diff --git a/temp/target/test-1703080999.png b/temp/target/test-1703080999.png new file mode 100644 index 0000000000000000000000000000000000000000..b93d72002d1fd8c691151d834306a6cffd195bf6 --- /dev/null +++ b/temp/target/test-1703080999.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e9e1eb1c9c5a2ade03fa10a7ebd66fc180fa856482209791f690f6f009f265 +size 1344720 diff --git a/temp/target/test-1703081020.png b/temp/target/test-1703081020.png new file mode 100644 index 0000000000000000000000000000000000000000..b93d72002d1fd8c691151d834306a6cffd195bf6 --- /dev/null +++ b/temp/target/test-1703081020.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e9e1eb1c9c5a2ade03fa10a7ebd66fc180fa856482209791f690f6f009f265 +size 1344720 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..65104eab523fafacbdbdedb6005dbbf00180fef1 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,32 @@ +import subprocess +import sys +import pytest + +from facefusion import wording +from facefusion.utilities import conditional_download + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download('.assets/examples', + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-1080p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-1080p.mp4', '-vframes', '1', '.assets/examples/target-1080p.jpg' ]) + + +def test_image_to_image() -> None: + commands = [ sys.executable, 'run.py', '-s', '.assets/examples/source.jpg', '-t', '.assets/examples/target-1080p.jpg', '-o', '.assets/examples', '--headless' ] + run = subprocess.run(commands, stdout = subprocess.PIPE) + + assert run.returncode == 0 + assert wording.get('processing_image_succeed') in run.stdout.decode() + + +def test_image_to_video() -> None: + commands = [ sys.executable, 'run.py', '-s', '.assets/examples/source.jpg', '-t', '.assets/examples/target-1080p.mp4', '-o', '.assets/examples', '--trim-frame-end', '10', '--headless' ] + run = subprocess.run(commands, stdout = subprocess.PIPE) + + assert run.returncode == 0 + assert wording.get('processing_video_succeed') in run.stdout.decode() diff --git a/tests/test_utilities.py b/tests/test_utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..934b340036c764d98a1c96961c100bfa0baeb831 --- /dev/null +++ b/tests/test_utilities.py @@ -0,0 +1,169 @@ +import glob +import platform +import subprocess +import pytest + +import facefusion.globals +from facefusion.utilities import conditional_download, extract_frames, create_temp, get_temp_directory_path, clear_temp, normalize_output_path, normalize_padding, is_file, is_directory, is_image, is_video, get_download_size, is_download_done, encode_execution_providers, decode_execution_providers + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + facefusion.globals.temp_frame_quality = 100 + facefusion.globals.trim_frame_start = None + facefusion.globals.trim_frame_end = None + facefusion.globals.temp_frame_format = 'png' + conditional_download('.assets/examples', + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=25', '.assets/examples/target-240p-25fps.mp4' ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=30', '.assets/examples/target-240p-30fps.mp4' ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=60', '.assets/examples/target-240p-60fps.mp4' ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + facefusion.globals.trim_frame_start = None + facefusion.globals.trim_frame_end = None + facefusion.globals.temp_frame_quality = 90 + facefusion.globals.temp_frame_format = 'jpg' + + +def test_extract_frames() -> None: + target_paths =\ + [ + '.assets/examples/target-240p-25fps.mp4', + '.assets/examples/target-240p-30fps.mp4', + '.assets/examples/target-240p-60fps.mp4' + ] + for target_path in target_paths: + temp_directory_path = get_temp_directory_path(target_path) + create_temp(target_path) + + assert extract_frames(target_path, 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == 324 + + clear_temp(target_path) + + +def test_extract_frames_with_trim_start() -> None: + facefusion.globals.trim_frame_start = 224 + data_provider =\ + [ + ('.assets/examples/target-240p-25fps.mp4', 55), + ('.assets/examples/target-240p-30fps.mp4', 100), + ('.assets/examples/target-240p-60fps.mp4', 212) + ] + for target_path, frame_total in data_provider: + temp_directory_path = get_temp_directory_path(target_path) + create_temp(target_path) + + assert extract_frames(target_path, 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp(target_path) + + +def test_extract_frames_with_trim_start_and_trim_end() -> None: + facefusion.globals.trim_frame_start = 124 + facefusion.globals.trim_frame_end = 224 + data_provider =\ + [ + ('.assets/examples/target-240p-25fps.mp4', 120), + ('.assets/examples/target-240p-30fps.mp4', 100), + ('.assets/examples/target-240p-60fps.mp4', 50) + ] + for target_path, frame_total in data_provider: + temp_directory_path = get_temp_directory_path(target_path) + create_temp(target_path) + + assert extract_frames(target_path, 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp(target_path) + + +def test_extract_frames_with_trim_end() -> None: + facefusion.globals.trim_frame_end = 100 + data_provider =\ + [ + ('.assets/examples/target-240p-25fps.mp4', 120), + ('.assets/examples/target-240p-30fps.mp4', 100), + ('.assets/examples/target-240p-60fps.mp4', 50) + ] + for target_path, frame_total in data_provider: + temp_directory_path = get_temp_directory_path(target_path) + create_temp(target_path) + + assert extract_frames(target_path, 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp(target_path) + + +def test_normalize_output_path() -> None: + if platform.system().lower() != 'windows': + assert normalize_output_path('.assets/examples/source.jpg', None, '.assets/examples/target-240p.mp4') == '.assets/examples/target-240p.mp4' + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/examples/target-240p.mp4') == '.assets/examples/target-240p.mp4' + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/examples') == '.assets/examples/target-240p.mp4' + assert normalize_output_path('.assets/examples/source.jpg', '.assets/examples/target-240p.mp4', '.assets/examples') == '.assets/examples/source-target-240p.mp4' + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/examples/output.mp4') == '.assets/examples/output.mp4' + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/output.mov') == '.assets/output.mp4' + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/examples/invalid') is None + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', '.assets/invalid/output.mp4') is None + assert normalize_output_path(None, '.assets/examples/target-240p.mp4', 'invalid') is None + assert normalize_output_path('.assets/examples/source.jpg', '.assets/examples/target-240p.mp4', None) is None + + +def test_normalize_padding() -> None: + assert normalize_padding([ 0, 0, 0, 0 ]) == (0, 0, 0, 0) + assert normalize_padding([ 1 ]) == (1, 1, 1, 1) + assert normalize_padding([ 1, 2 ]) == (1, 2, 1, 2) + assert normalize_padding([ 1, 2, 3 ]) == (1, 2, 3, 2) + assert normalize_padding(None) is None + + +def test_is_file() -> None: + assert is_file('.assets/examples/source.jpg') is True + assert is_file('.assets/examples') is False + assert is_file('invalid') is False + + +def test_is_directory() -> None: + assert is_directory('.assets/examples') is True + assert is_directory('.assets/examples/source.jpg') is False + assert is_directory('invalid') is False + + +def test_is_image() -> None: + assert is_image('.assets/examples/source.jpg') is True + assert is_image('.assets/examples/target-240p.mp4') is False + assert is_image('invalid') is False + + +def test_is_video() -> None: + assert is_video('.assets/examples/target-240p.mp4') is True + assert is_video('.assets/examples/source.jpg') is False + assert is_video('invalid') is False + + +def test_get_download_size() -> None: + assert get_download_size('https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4') == 191675 + assert get_download_size('https://github.com/facefusion/facefusion-assets/releases/download/examples/target-360p.mp4') == 370732 + assert get_download_size('invalid') == 0 + + +def test_is_download_done() -> None: + assert is_download_done('https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4', '.assets/examples/target-240p.mp4') is True + assert is_download_done('https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4','invalid') is False + assert is_download_done('invalid', 'invalid') is False + + +def test_encode_execution_providers() -> None: + assert encode_execution_providers([ 'CPUExecutionProvider' ]) == [ 'cpu' ] + + +def test_decode_execution_providers() -> None: + assert decode_execution_providers([ 'cpu' ]) == [ 'CPUExecutionProvider' ] diff --git a/tests/test_vision.py b/tests/test_vision.py new file mode 100644 index 0000000000000000000000000000000000000000..f77af0490195bd0965ccc7bf0338807d9c56ca38 --- /dev/null +++ b/tests/test_vision.py @@ -0,0 +1,49 @@ +import subprocess +import pytest + +import facefusion.globals +from facefusion.utilities import conditional_download +from facefusion.vision import get_video_frame, detect_fps, count_video_frame_total + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + facefusion.globals.temp_frame_quality = 100 + facefusion.globals.trim_frame_start = None + facefusion.globals.trim_frame_end = None + facefusion.globals.temp_frame_format = 'png' + conditional_download('.assets/examples', + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=25', '.assets/examples/target-240p-25fps.mp4' ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=30', '.assets/examples/target-240p-30fps.mp4' ]) + subprocess.run([ 'ffmpeg', '-i', '.assets/examples/target-240p.mp4', '-vf', 'fps=60', '.assets/examples/target-240p-60fps.mp4' ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + facefusion.globals.trim_frame_start = None + facefusion.globals.trim_frame_end = None + facefusion.globals.temp_frame_quality = 90 + facefusion.globals.temp_frame_format = 'jpg' + + +def test_get_video_frame() -> None: + assert get_video_frame('.assets/examples/target-240p-25fps.mp4') is not None + assert get_video_frame('invalid') is None + + +def test_detect_fps() -> None: + assert detect_fps('.assets/examples/target-240p-25fps.mp4') == 25.0 + assert detect_fps('.assets/examples/target-240p-30fps.mp4') == 30.0 + assert detect_fps('.assets/examples/target-240p-60fps.mp4') == 60.0 + assert detect_fps('invalid') is None + + +def test_count_video_frame_total() -> None: + assert count_video_frame_total('.assets/examples/target-240p-25fps.mp4') == 270 + assert count_video_frame_total('.assets/examples/target-240p-30fps.mp4') == 324 + assert count_video_frame_total('.assets/examples/target-240p-60fps.mp4') == 648 + assert count_video_frame_total('invalid') == 0 diff --git a/upload.py b/upload.py new file mode 100644 index 0000000000000000000000000000000000000000..4e176ba92a0c13797d968fe934a52941ba8e5353 --- /dev/null +++ b/upload.py @@ -0,0 +1,8 @@ +from huggingface_hub import HfApi +api = HfApi() + +api.upload_folder( + folder_path="/workspaces/facefusion-api", + repo_id="michaelj/facefusionapi", + repo_type="space", +) \ No newline at end of file