diff --git a/.github/preview.png b/.github/preview.png index ffcff320..960017c0 100644 Binary files a/.github/preview.png and b/.github/preview.png differ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..a93bed3f --- /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 c10322d8..ad8a0e69 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Preview Installation ------------ -Be aware, the installation needs technical skills and is not for beginners. Please do not open platform and installation related issues on GitHub. We have a very helpful [Discord](https://join.facefusion.io) community that will guide you to install FaceFusion. +Be aware, the installation needs technical skills and is not for beginners. Please do not open platform and installation related issues on GitHub. We have a very helpful [Discord](https://join.facefusion.io) community that will guide you to complete the installation. -Read the [installation](https://docs.facefusion.io/installation) now. +Get started with the [installation](https://docs.facefusion.io/installation) guide. Usage @@ -30,68 +30,68 @@ Run the command: python run.py [options] options: - -h, --help show this help message and exit - -s SOURCE_PATH, --source SOURCE_PATH select a source image - -t TARGET_PATH, --target TARGET_PATH select a target image or video - -o OUTPUT_PATH, --output OUTPUT_PATH specify the output file or directory - -v, --version show program's version number and exit + -h, --help show this help message and exit + -s SOURCE_PATH, --source SOURCE_PATH select a source image + -t TARGET_PATH, --target TARGET_PATH select a target image or video + -o OUTPUT_PATH, --output OUTPUT_PATH specify the output file or directory + -v, --version show program's version number and exit misc: - --skip-download omit automate downloads and lookups - --headless run the program in headless mode + --skip-download omit automate downloads and lookups + --headless run the program in headless mode execution: - --execution-providers {cpu} [{cpu} ...] choose from the available execution providers (choices: cpu, ...) - --execution-thread-count EXECUTION_THREAD_COUNT specify the number of execution threads - --execution-queue-count EXECUTION_QUEUE_COUNT specify the number of execution queries - --max-memory MAX_MEMORY specify the maximum amount of ram to be used (in gb) + --execution-providers {cpu} [{cpu} ...] choose from the available execution providers + --execution-thread-count [1-128] specify the number of execution threads + --execution-queue-count [1-32] specify the number of execution queries + --max-memory [0-128] specify the maximum amount of ram to be used (in gb) -face recognition: - --face-recognition {reference,many} specify the method for face recognition - --face-analyser-direction {left-right,right-left,top-bottom,bottom-top,small-large,large-small} specify the direction used for face analysis - --face-analyser-age {child,teen,adult,senior} specify the age used for face analysis - --face-analyser-gender {male,female} specify the gender used for face analysis - --reference-face-position REFERENCE_FACE_POSITION specify the position of the reference face - --reference-face-distance REFERENCE_FACE_DISTANCE specify the distance between the reference face and the target face - --reference-frame-number REFERENCE_FRAME_NUMBER specify the number of the reference frame +face analyser: + --face-analyser-order {left-right,right-left,top-bottom,bottom-top,small-large,large-small,best-worst,worst-best} specify the order used for the face analyser + --face-analyser-age {child,teen,adult,senior} specify the age used for the face analyser + --face-analyser-gender {male,female} specify the gender used for the face analyser + --face-detector-model {retinaface,yunet} specify the model used for the face detector + --face-detector-size {160x160,320x320,480x480,512x512,640x640,768x768,960x960,1024x1024} specify the size threshold used for the face detector + --face-detector-score [0.0-1.0] specify the score threshold used for the face detector + +face selector: + --face-selector-mode {reference,one,many} specify the mode for the face selector + --reference-face-position REFERENCE_FACE_POSITION specify the position of the reference face + --reference-face-distance [0.0-1.5] specify the distance between the reference face and the target face + --reference-frame-number REFERENCE_FRAME_NUMBER specify the number of the reference frame + +face mask: + --face-mask-blur [0.0-1.0] specify the blur amount for face mask + --face-mask-padding FACE_MASK_PADDING [FACE_MASK_PADDING ...] specify the face mask padding (top, right, bottom, left) in percent frame extraction: - --trim-frame-start TRIM_FRAME_START specify the start frame for extraction - --trim-frame-end TRIM_FRAME_END specify the end frame for extraction - --temp-frame-format {jpg,png} specify the image format used for frame extraction - --temp-frame-quality [0-100] specify the image quality used for frame extraction - --keep-temp retain temporary frames after processing + --trim-frame-start TRIM_FRAME_START specify the start frame for extraction + --trim-frame-end TRIM_FRAME_END specify the end frame for extraction + --temp-frame-format {jpg,png} specify the image format used for frame extraction + --temp-frame-quality [0-100] specify the image quality used for frame extraction + --keep-temp retain temporary frames after processing output creation: - --output-image-quality [0-100] specify the quality used for the output image - --output-video-encoder {libx264,libx265,libvpx-vp9,h264_nvenc,hevc_nvenc} specify the encoder used for the output video - --output-video-quality [0-100] specify the quality used for the output video - --keep-fps preserve the frames per second (fps) of the target - --skip-audio omit audio from the target + --output-image-quality [0-100] specify the quality used for the output image + --output-video-encoder {libx264,libx265,libvpx-vp9,h264_nvenc,hevc_nvenc} specify the encoder used for the output video + --output-video-quality [0-100] specify the quality used for the output video + --keep-fps preserve the frames per second (fps) of the target + --skip-audio omit audio from the target frame processors: - --frame-processors FRAME_PROCESSORS [FRAME_PROCESSORS ...] choose from the available frame processors (choices: face_enhancer, face_swapper, frame_enhancer, ...) - --face-enhancer-model {codeformer,gfpgan_1.2,gfpgan_1.3,gfpgan_1.4,gpen_bfr_512} choose from the mode for the frame processor - --face-enhancer-blend [0-100] specify the blend factor for the frame processor - --face-swapper-model {inswapper_128,inswapper_128_fp16} choose from the mode for the frame processor - --frame-enhancer-model {realesrgan_x2plus,realesrgan_x4plus,realesrnet_x4plus} choose from the mode for the frame processor - --frame-enhancer-blend [0-100] specify the blend factor for the frame processor + --frame-processors FRAME_PROCESSORS [FRAME_PROCESSORS ...] choose from the available frame processors (choices: face_debugger, face_enhancer, face_swapper, frame_enhancer, ...) + --face-debugger-items {bbox,kps,face-mask,score} [{bbox,kps,face-mask,score} ...] specify the face debugger items + --face-enhancer-model {codeformer,gfpgan_1.2,gfpgan_1.3,gfpgan_1.4,gpen_bfr_256,gpen_bfr_512,restoreformer} choose the model for the frame processor + --face-enhancer-blend [0-100] specify the blend factor for the frame processor + --face-swapper-model {blendface_256,inswapper_128,inswapper_128_fp16,simswap_256,simswap_512_unofficial} choose the model for the frame processor + --frame-enhancer-model {real_esrgan_x2plus,real_esrgan_x4plus,real_esrnet_x4plus} choose the model for the frame processor + --frame-enhancer-blend [0-100] specify the blend factor for the frame processor uis: - --ui-layouts UI_LAYOUTS [UI_LAYOUTS ...] choose from the available ui layouts (choices: benchmark, webcam, default, ...) + --ui-layouts UI_LAYOUTS [UI_LAYOUTS ...] choose from the available ui layouts (choices: benchmark, webcam, default, ...) ``` -Disclaimer ----------- - -We acknowledge the unethical potential of FaceFusion and are resolutely dedicated to establishing safeguards against such misuse. This program has been engineered to abstain from processing inappropriate content such as nudity, graphic content and sensitive material. - -It is important to note that we maintain a strong stance against any type of pornographic nature and do not collaborate with any websites promoting the unauthorized use of our software. - -Users who seek to engage in such activities will face consequences, including being banned from our community. We reserve the right to report developers on GitHub who distribute unlocked forks of our software at any time. - - Documentation ------------- diff --git a/facefusion/choices.py b/facefusion/choices.py index f758e7d1..cadeda67 100644 --- a/facefusion/choices.py +++ b/facefusion/choices.py @@ -1,10 +1,26 @@ from typing import List -from facefusion.typing import FaceRecognition, FaceAnalyserDirection, FaceAnalyserAge, FaceAnalyserGender, TempFrameFormat, OutputVideoEncoder +import numpy -face_recognitions : List[FaceRecognition] = [ 'reference', 'many' ] -face_analyser_directions : List[FaceAnalyserDirection] = [ 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small' ] +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 00000000..2111effa --- /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 index 2e2e48fb..c6c205cb 100755 --- a/facefusion/core.py +++ b/facefusion/core.py @@ -1,7 +1,6 @@ import os os.environ['OMP_NUM_THREADS'] = '1' -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' import signal import sys @@ -9,17 +8,20 @@ import warnings import platform import shutil import onnxruntime -import tensorflow from argparse import ArgumentParser, HelpFormatter import facefusion.choices import facefusion.globals -from facefusion import metadata, wording -from facefusion.predictor import predict_image, predict_video +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 -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 +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 -warnings.filterwarnings('ignore', category = FutureWarning, module = 'insightface') +onnxruntime.set_default_logger_severity(3) +warnings.filterwarnings('ignore', category = UserWarning, module = 'gradio') warnings.filterwarnings('ignore', category = UserWarning, module = 'torchvision') @@ -37,33 +39,42 @@ def cli() -> None: 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').format(choices = 'cpu'), 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 = 1) - group_execution.add_argument('--execution-queue-count', help = wording.get('execution_queue_count_help'), dest = 'execution_queue_count', type = int, default = 1) - group_execution.add_argument('--max-memory', help=wording.get('max_memory_help'), dest='max_memory', type = int) - # face recognition - group_face_recognition = program.add_argument_group('face recognition') - group_face_recognition.add_argument('--face-recognition', help = wording.get('face_recognition_help'), dest = 'face_recognition', default = 'reference', choices = facefusion.choices.face_recognitions) - group_face_recognition.add_argument('--face-analyser-direction', help = wording.get('face_analyser_direction_help'), dest = 'face_analyser_direction', default = 'left-right', choices = facefusion.choices.face_analyser_directions) - group_face_recognition.add_argument('--face-analyser-age', help = wording.get('face_analyser_age_help'), dest = 'face_analyser_age', choices = facefusion.choices.face_analyser_ages) - group_face_recognition.add_argument('--face-analyser-gender', help = wording.get('face_analyser_gender_help'), dest = 'face_analyser_gender', choices = facefusion.choices.face_analyser_genders) - group_face_recognition.add_argument('--reference-face-position', help = wording.get('reference_face_position_help'), dest = 'reference_face_position', type = int, default = 0) - group_face_recognition.add_argument('--reference-face-distance', help = wording.get('reference_face_distance_help'), dest = 'reference_face_distance', type = float, default = 1.5) - group_face_recognition.add_argument('--reference-frame-number', help = wording.get('reference_frame_number_help'), dest = 'reference_frame_number', type = int, default = 0) + 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_processing = program.add_argument_group('frame extraction') - group_processing.add_argument('--trim-frame-start', help = wording.get('trim_frame_start_help'), dest = 'trim_frame_start', type = int) - group_processing.add_argument('--trim-frame-end', help = wording.get('trim_frame_end_help'), dest = 'trim_frame_end', type = int) - group_processing.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_processing.add_argument('--temp-frame-quality', help = wording.get('temp_frame_quality_help'), dest = 'temp_frame_quality', type = int, default = 100, choices = range(101), metavar = '[0-100]') - group_processing.add_argument('--keep-temp', help = wording.get('keep_temp_help'), dest = 'keep_temp', action = 'store_true') + 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 = program.add_argument_group('output creation') - group_output.add_argument('--output-image-quality', help=wording.get('output_image_quality_help'), dest = 'output_image_quality', type = int, default = 80, choices = range(101), metavar = '[0-100]') - group_output.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.add_argument('--output-video-quality', help = wording.get('output_video_quality_help'), dest = 'output_video_quality', type = int, default = 80, choices = range(101), metavar = '[0-100]') - group_output.add_argument('--keep-fps', help = wording.get('keep_fps_help'), dest = 'keep_fps', action = 'store_true') - group_output.add_argument('--skip-audio', help = wording.get('skip_audio_help'), dest = 'skip_audio', action = 'store_true') + 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) @@ -92,14 +103,21 @@ def apply_args(program : ArgumentParser) -> None: 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 recognition - facefusion.globals.face_recognition = args.face_recognition - facefusion.globals.face_analyser_direction = args.face_analyser_direction + # 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 @@ -125,7 +143,7 @@ def apply_args(program : ArgumentParser) -> None: def run(program : ArgumentParser) -> None: apply_args(program) limit_resources() - if not pre_check(): + 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(): @@ -148,14 +166,6 @@ def destroy() -> None: def limit_resources() -> None: - # prevent tensorflow memory leak - gpus = tensorflow.config.experimental.list_physical_devices('GPU') - for gpu in gpus: - tensorflow.config.experimental.set_virtual_device_configuration(gpu, - [ - tensorflow.config.experimental.VirtualDeviceConfiguration(memory_limit = 512) - ]) - # limit memory usage if facefusion.globals.max_memory: memory = facefusion.globals.max_memory * 1024 ** 3 if platform.system().lower() == 'darwin': @@ -180,6 +190,7 @@ def pre_check() -> bool: 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 @@ -189,8 +200,18 @@ def conditional_process() -> None: 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 predict_image(facefusion.globals.target_path): + if analyse_image(facefusion.globals.target_path): return shutil.copy2(facefusion.globals.target_path, facefusion.globals.output_path) # process frame @@ -203,14 +224,14 @@ def process_image() -> None: if not compress_image(facefusion.globals.output_path): update_status(wording.get('compressing_image_failed')) # validate image - if is_image(facefusion.globals.target_path): + 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 predict_video(facefusion.globals.target_path): + 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 @@ -247,11 +268,7 @@ def process_video() -> None: update_status(wording.get('clearing_temp')) clear_temp(facefusion.globals.target_path) # validate video - if is_video(facefusion.globals.target_path): + if is_video(facefusion.globals.output_path): update_status(wording.get('processing_video_succeed')) else: update_status(wording.get('processing_video_failed')) - - -def update_status(message : str, scope : str = 'FACEFUSION.CORE') -> None: - print('[' + scope + '] ' + message) diff --git a/facefusion/face_analyser.py b/facefusion/face_analyser.py index 0a13bf3f..97ee5f58 100644 --- a/facefusion/face_analyser.py +++ b/facefusion/face_analyser.py @@ -1,14 +1,52 @@ -from typing import Any, Optional, List +from typing import Any, Optional, List, Dict, Tuple import threading -import insightface +import cv2 import numpy +import onnxruntime import facefusion.globals from facefusion.face_cache import get_faces_cache, set_faces_cache -from facefusion.typing import Frame, Face, FaceAnalyserDirection, FaceAnalyserAge, FaceAnalyserGender +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: @@ -16,8 +54,23 @@ def get_face_analyser() -> Any: with THREAD_LOCK: if FACE_ANALYSER is None: - FACE_ANALYSER = insightface.app.FaceAnalysis(name = 'buffalo_l', providers = facefusion.globals.execution_providers) - FACE_ANALYSER.prepare(ctx_id = 0) + 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 @@ -27,6 +80,150 @@ def clear_face_analyser() -> Any: 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: @@ -43,10 +240,10 @@ def get_many_faces(frame : Frame) -> List[Face]: if faces_cache: faces = faces_cache else: - faces = get_face_analyser().get(frame) + faces = extract_faces(frame) set_faces_cache(frame, faces) - if facefusion.globals.face_analyser_direction: - faces = sort_by_direction(faces, facefusion.globals.face_analyser_direction) + 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: @@ -62,38 +259,42 @@ def find_similar_faces(frame : Frame, reference_face : Face, face_distance : flo if many_faces: for face in many_faces: if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'): - current_face_distance = numpy.sum(numpy.square(face.normed_embedding - 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_direction(faces : List[Face], direction : FaceAnalyserDirection) -> List[Face]: - if direction == 'left-right': - return sorted(faces, key = lambda face: face['bbox'][0]) - if direction == 'right-left': - return sorted(faces, key = lambda face: face['bbox'][0], reverse = True) - if direction == 'top-bottom': - return sorted(faces, key = lambda face: face['bbox'][1]) - if direction == 'bottom-top': - return sorted(faces, key = lambda face: face['bbox'][1], reverse = True) - if direction == 'small-large': - return sorted(faces, key = lambda face: (face['bbox'][2] - face['bbox'][0]) * (face['bbox'][3] - face['bbox'][1])) - if direction == 'large-small': - return sorted(faces, key = lambda face: (face['bbox'][2] - face['bbox'][0]) * (face['bbox'][3] - face['bbox'][1]), reverse = True) +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': + if face.age < 13 and age == 'child': filter_faces.append(face) - elif face['age'] < 19 and age == 'teen': + elif face.age < 19 and age == 'teen': filter_faces.append(face) - elif face['age'] < 60 and age == 'adult': + elif face.age < 60 and age == 'adult': filter_faces.append(face) - elif face['age'] > 59 and age == 'senior': + elif face.age > 59 and age == 'senior': filter_faces.append(face) return filter_faces @@ -101,8 +302,8 @@ def filter_by_age(faces : List[Face], age : FaceAnalyserAge) -> List[Face]: def filter_by_gender(faces : List[Face], gender : FaceAnalyserGender) -> List[Face]: filter_faces = [] for face in faces: - if face['gender'] == 1 and gender == 'male': + if face.gender == 0 and gender == 'female': filter_faces.append(face) - if face['gender'] == 0 and gender == 'female': + 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 index d6328dd1..8730509a 100644 --- a/facefusion/face_cache.py +++ b/facefusion/face_cache.py @@ -26,4 +26,4 @@ def clear_faces_cache() -> None: def create_frame_hash(frame : Frame) -> Optional[str]: - return hashlib.sha256(frame.tobytes()).hexdigest() if frame is not None else None + 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 00000000..b635f944 --- /dev/null +++ b/facefusion/face_helper.py @@ -0,0 +1,119 @@ +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 + + +@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_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/globals.py b/facefusion/globals.py old mode 100644 new mode 100755 index c77dc5b4..7d3d7add --- a/facefusion/globals.py +++ b/facefusion/globals.py @@ -1,6 +1,6 @@ from typing import List, Optional -from facefusion.typing import FaceRecognition, FaceAnalyserDirection, FaceAnalyserAge, FaceAnalyserGender, TempFrameFormat, OutputVideoEncoder +from facefusion.typing import FaceSelectorMode, FaceAnalyserOrder, FaceAnalyserAge, FaceAnalyserGender, OutputVideoEncoder, FaceDetectorModel, FaceRecognizerModel, TempFrameFormat, Padding # general source_path : Optional[str] = None @@ -14,14 +14,22 @@ execution_providers : List[str] = [] execution_thread_count : Optional[int] = None execution_queue_count : Optional[int] = None max_memory : Optional[int] = None -# face recognition -face_recognition : Optional[FaceRecognition] = None -face_analyser_direction : Optional[FaceAnalyserDirection] = 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 diff --git a/facefusion/installer.py b/facefusion/installer.py index 37f788a0..dfd17dab 100644 --- a/facefusion/installer.py +++ b/facefusion/installer.py @@ -17,12 +17,12 @@ TORCH : Dict[str, str] =\ } ONNXRUNTIMES : Dict[str, Tuple[str, str]] =\ { - 'default': ('onnxruntime', '1.16.0'), - 'cuda': ('onnxruntime-gpu', '1.16.0'), + '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.0'), - 'openvino': ('onnxruntime-openvino', '1.15.0') + 'directml': ('onnxruntime-directml', '1.16.3'), + 'openvino': ('onnxruntime-openvino', '1.16.0') } @@ -46,18 +46,10 @@ def run(program : ArgumentParser) -> None: 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()) - ) + 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 is not None: + if answers: torch = answers['torch'] torch_wheel = TORCH[torch] onnxruntime = answers['onnxruntime'] diff --git a/facefusion/metadata.py b/facefusion/metadata.py index d58f8b23..6e167342 100644 --- a/facefusion/metadata.py +++ b/facefusion/metadata.py @@ -2,7 +2,7 @@ METADATA =\ { 'name': 'FaceFusion', 'description': 'Next generation face swapper and enhancer', - 'version': '1.3.1', + 'version': '2.0.0', 'license': 'MIT', 'author': 'Henry Ruhs', 'url': 'https://facefusion.io' diff --git a/facefusion/predictor.py b/facefusion/predictor.py deleted file mode 100644 index b173ba12..00000000 --- a/facefusion/predictor.py +++ /dev/null @@ -1,58 +0,0 @@ -import threading -from functools import lru_cache - -import numpy -import opennsfw2 -from PIL import Image -from keras import Model - -from facefusion.typing import Frame - -PREDICTOR = None -THREAD_LOCK : threading.Lock = threading.Lock() -MAX_PROBABILITY = 0.75 -FRAME_INTERVAL = 25 -STREAM_COUNTER = 0 - - -def get_predictor() -> Model: - global PREDICTOR - - with THREAD_LOCK: - if PREDICTOR is None: - PREDICTOR = opennsfw2.make_open_nsfw_model() - return PREDICTOR - - -def clear_predictor() -> None: - global PREDICTOR - - PREDICTOR = None - - -def predict_stream(frame : Frame) -> bool: - global STREAM_COUNTER - - STREAM_COUNTER = STREAM_COUNTER + 1 - if STREAM_COUNTER % FRAME_INTERVAL == 0: - return predict_frame(frame) - return False - - -def predict_frame(frame : Frame) -> bool: - image = Image.fromarray(frame) - image = opennsfw2.preprocess_image(image, opennsfw2.Preprocessing.YAHOO) - views = numpy.expand_dims(image, axis = 0) - _, probability = get_predictor().predict(views)[0] - return probability > MAX_PROBABILITY - - -@lru_cache(maxsize = None) -def predict_image(image_path : str) -> bool: - return opennsfw2.predict_image(image_path) > MAX_PROBABILITY - - -@lru_cache(maxsize = None) -def predict_video(video_path : str) -> bool: - _, probabilities = opennsfw2.predict_video_frames(video_path = video_path, frame_interval = FRAME_INTERVAL) - return any(probability > MAX_PROBABILITY for probability in probabilities) diff --git a/facefusion/processors/frame/choices.py b/facefusion/processors/frame/choices.py old mode 100644 new mode 100755 index ad224411..c8119d46 --- a/facefusion/processors/frame/choices.py +++ b/facefusion/processors/frame/choices.py @@ -1,5 +1,13 @@ from typing import List +import numpy -face_swapper_models : List[str] = [ 'inswapper_128', 'inswapper_128_fp16' ] -face_enhancer_models : List[str] = [ 'codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_512' ] -frame_enhancer_models : List[str] = [ 'realesrgan_x2plus', 'realesrgan_x4plus', 'realesrnet_x4plus' ] +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 index a0f767b8..294ffe7b 100644 --- a/facefusion/processors/frame/core.py +++ b/facefusion/processors/frame/core.py @@ -1,7 +1,5 @@ -import os import sys import importlib -import psutil from concurrent.futures import ThreadPoolExecutor, as_completed from queue import Queue from types import ModuleType @@ -9,8 +7,9 @@ from typing import Any, List from tqdm import tqdm import facefusion.globals -from facefusion import wording 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 =\ @@ -63,15 +62,20 @@ def clear_frame_processors_modules() -> None: def multi_process_frames(source_path : str, temp_frame_paths : List[str], process_frames : Process_Frames) -> None: - progress_bar_format = '{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]' - with tqdm(total = len(temp_frame_paths), desc = wording.get('processing'), unit = 'frame', dynamic_ncols = True, bar_format = progress_bar_format) as progress: + 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, lambda: update_progress(progress)) + 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() @@ -90,17 +94,3 @@ def pick_queue(queue : Queue[str], queue_per_future : int) -> List[str]: if not queue.empty(): queues.append(queue.get()) return queues - - -def update_progress(progress : Any = None) -> None: - process = psutil.Process(os.getpid()) - memory_usage = process.memory_info().rss / 1024 / 1024 / 1024 - progress.set_postfix( - { - 'memory_usage': '{:.2f}'.format(memory_usage).zfill(5) + 'GB', - 'execution_providers': facefusion.globals.execution_providers, - 'execution_thread_count': facefusion.globals.execution_thread_count, - 'execution_queue_count': facefusion.globals.execution_queue_count - }) - progress.refresh() - progress.update(1) diff --git a/facefusion/processors/frame/globals.py b/facefusion/processors/frame/globals.py old mode 100644 new mode 100755 index 102a4466..526b8573 --- a/facefusion/processors/frame/globals.py +++ b/facefusion/processors/frame/globals.py @@ -1,7 +1,10 @@ -from typing import Optional +from typing import List, Optional -face_swapper_model : Optional[str] = None -face_enhancer_model : Optional[str] = None +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[str] = 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/face_debugger.py b/facefusion/processors/frame/modules/face_debugger.py new file mode 100755 index 00000000..75477e5d --- /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 old mode 100644 new mode 100755 index d8b6b589..416e4991 --- a/facefusion/processors/frame/modules/face_enhancer.py +++ b/facefusion/processors/frame/modules/face_enhancer.py @@ -1,4 +1,4 @@ -from typing import Any, List, Tuple, Dict, Literal, Optional +from typing import Any, List, Dict, Literal, Optional from argparse import ArgumentParser import cv2 import threading @@ -6,11 +6,13 @@ import numpy import onnxruntime import facefusion.globals +import facefusion.processors.frame.core as frame_processors from facefusion import wording -from facefusion.core import update_status from facefusion.face_analyser import get_many_faces, clear_face_analyser -from facefusion.typing import Face, Frame, Matrix, Update_Process, ProcessMode, ModelValue, OptionsWithModel -from facefusion.utilities import conditional_download, resolve_relative_path, is_image, is_video, is_file, is_download_done +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 @@ -24,27 +26,51 @@ 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') + '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/GFPGANv1.2.onnx', - 'path': resolve_relative_path('../.assets/models/GFPGANv1.2.onnx') + '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/GFPGANv1.3.onnx', - 'path': resolve_relative_path('../.assets/models/GFPGANv1.3.onnx') + '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/GFPGANv1.4.onnx', - 'path': resolve_relative_path('../.assets/models/GFPGANv1.4.onnx') + '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') + '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 @@ -66,7 +92,7 @@ def clear_frame_processor() -> None: FRAME_PROCESSOR = None -def get_options(key : Literal[ 'model' ]) -> Any: +def get_options(key : Literal['model']) -> Any: global OPTIONS if OPTIONS is None: @@ -77,7 +103,7 @@ def get_options(key : Literal[ 'model' ]) -> Any: return OPTIONS.get(key) -def set_options(key : Literal[ 'model' ], value : Any) -> None: +def set_options(key : Literal['model'], value : Any) -> None: global OPTIONS OPTIONS[key] = value @@ -85,7 +111,7 @@ def set_options(key : Literal[ 'model' ], value : Any) -> None: 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= 100, choices = range(101), metavar = '[0-100]') + 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: @@ -123,12 +149,15 @@ def pre_process(mode : ProcessMode) -> bool: 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() - crop_frame, affine_matrix = warp_face(target_face, temp_frame) + 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(): @@ -139,25 +168,11 @@ def enhance_face(target_face: Face, temp_frame: Frame) -> Frame: 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) + 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 warp_face(target_face : Face, temp_frame : Frame) -> Tuple[Frame, Matrix]: - template = numpy.array( - [ - [ 192.98138, 239.94708 ], - [ 318.90277, 240.1936 ], - [ 256.63416, 314.01935 ], - [ 201.26117, 371.41043 ], - [ 313.08905, 371.15118 ] - ]) - affine_matrix = cv2.estimateAffinePartial2D(target_face['kps'], template, method = cv2.LMEDS)[0] - crop_frame = cv2.warpAffine(temp_frame, affine_matrix, (512, 512)) - return crop_frame, affine_matrix - - def prepare_crop_frame(crop_frame : Frame) -> Frame: crop_frame = crop_frame[:, :, ::-1] / 255.0 crop_frame = (crop_frame - 0.5) / 0.5 @@ -174,26 +189,6 @@ def normalize_crop_frame(crop_frame : Frame) -> Frame: return crop_frame -def paste_back(temp_frame : Frame, crop_frame : Frame, affine_matrix : Matrix) -> Frame: - inverse_affine_matrix = cv2.invertAffineTransform(affine_matrix) - temp_frame_height, temp_frame_width = temp_frame.shape[0:2] - crop_frame_height, crop_frame_width = crop_frame.shape[0:2] - inverse_crop_frame = cv2.warpAffine(crop_frame, inverse_affine_matrix, (temp_frame_width, temp_frame_height)) - inverse_mask = numpy.ones((crop_frame_height, crop_frame_width, 3), dtype = numpy.float32) - inverse_mask_frame = cv2.warpAffine(inverse_mask, inverse_affine_matrix, (temp_frame_width, temp_frame_height)) - inverse_mask_frame = cv2.erode(inverse_mask_frame, numpy.ones((2, 2))) - inverse_mask_border = inverse_mask_frame * inverse_crop_frame - inverse_mask_area = numpy.sum(inverse_mask_frame) // 3 - inverse_mask_edge = int(inverse_mask_area ** 0.5) // 20 - inverse_mask_radius = inverse_mask_edge * 2 - inverse_mask_center = cv2.erode(inverse_mask_frame, numpy.ones((inverse_mask_radius, inverse_mask_radius))) - inverse_mask_blur_size = inverse_mask_edge * 2 + 1 - inverse_mask_blur_area = cv2.GaussianBlur(inverse_mask_center, (inverse_mask_blur_size, inverse_mask_blur_size), 0) - temp_frame = inverse_mask_blur_area * inverse_mask_border + (1 - inverse_mask_blur_area) * temp_frame - temp_frame = temp_frame.clip(0, 255).astype(numpy.uint8) - return temp_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) @@ -223,4 +218,4 @@ def process_image(source_path : str, target_path : str, output_path : str) -> No def process_video(source_path : str, temp_frame_paths : List[str]) -> None: - facefusion.processors.frame.core.multi_process_frames(None, temp_frame_paths, process_frames) + 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 old mode 100644 new mode 100755 index e8513d80..5453348a --- a/facefusion/processors/frame/modules/face_swapper.py +++ b/facefusion/processors/frame/modules/face_swapper.py @@ -1,34 +1,79 @@ from typing import Any, List, Dict, Literal, Optional from argparse import ArgumentParser -import insightface 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.core import update_status 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, set_face_reference -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 +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') + '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') + '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 @@ -40,7 +85,7 @@ def get_frame_processor() -> Any: with THREAD_LOCK: if FRAME_PROCESSOR is None: model_path = get_options('model').get('path') - FRAME_PROCESSOR = insightface.model_zoo.get_model(model_path, providers = facefusion.globals.execution_providers) + FRAME_PROCESSOR = onnxruntime.InferenceSession(model_path, providers = facefusion.globals.execution_providers) return FRAME_PROCESSOR @@ -50,18 +95,35 @@ def clear_frame_processor() -> None: FRAME_PROCESSOR = None -def get_options(key : Literal[ 'model' ]) -> Any: +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 = \ + OPTIONS =\ { 'model': MODELS[frame_processors_globals.face_swapper_model] } return OPTIONS.get(key) -def set_options(key : Literal[ 'model' ], value : Any) -> None: +def set_options(key : Literal['model'], value : Any) -> None: global OPTIONS OPTIONS[key] = value @@ -74,6 +136,12 @@ def register_args(program : ArgumentParser) -> None: 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: @@ -110,21 +178,82 @@ def pre_process(mode : ProcessMode) -> bool: 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: - return get_frame_processor().get(temp_frame, target_face, source_face, paste_back = True) + 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_recognition: + 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 'many' in facefusion.globals.face_recognition: + 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: @@ -134,7 +263,7 @@ def process_frame(source_face : Face, reference_face : Face, temp_frame : 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_recognition else None + 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) @@ -145,18 +274,10 @@ def process_frames(source_path : str, temp_frame_paths : List[str], update_progr 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_recognition else None + 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: - conditional_set_face_reference(temp_frame_paths) frame_processors.multi_process_frames(source_path, temp_frame_paths, process_frames) - - -def conditional_set_face_reference(temp_frame_paths : List[str]) -> None: - if 'reference' in facefusion.globals.face_recognition and not get_face_reference(): - reference_frame = read_static_image(temp_frame_paths[facefusion.globals.reference_frame_number]) - reference_face = get_one_face(reference_frame, facefusion.globals.reference_face_position) - set_face_reference(reference_face) diff --git a/facefusion/processors/frame/modules/frame_enhancer.py b/facefusion/processors/frame/modules/frame_enhancer.py index 1fe9be71..c2194e6d 100644 --- a/facefusion/processors/frame/modules/frame_enhancer.py +++ b/facefusion/processors/frame/modules/frame_enhancer.py @@ -8,10 +8,10 @@ from realesrgan import RealESRGANer import facefusion.globals import facefusion.processors.frame.core as frame_processors from facefusion import wording -from facefusion.core import update_status 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, get_device +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 @@ -22,22 +22,22 @@ THREAD_LOCK : threading.Lock = threading.Lock() NAME = 'FACEFUSION.FRAME_PROCESSOR.FRAME_ENHANCER' MODELS: Dict[str, ModelValue] =\ { - 'realesrgan_x2plus': + 'real_esrgan_x2plus': { - 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/RealESRGAN_x2plus.pth', - 'path': resolve_relative_path('../.assets/models/RealESRGAN_x2plus.pth'), + '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 }, - 'realesrgan_x4plus': + 'real_esrgan_x4plus': { - 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/RealESRGAN_x4plus.pth', - 'path': resolve_relative_path('../.assets/models/RealESRGAN_x4plus.pth'), + '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 }, - 'realesrnet_x4plus': + 'real_esrnet_x4plus': { - 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models/RealESRNet_x4plus.pth', - 'path': resolve_relative_path('../.assets/models/RealESRNet_x4plus.pth'), + '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 } } @@ -58,7 +58,7 @@ def get_frame_processor() -> Any: num_out_ch = 3, scale = model_scale ), - device = get_device(facefusion.globals.execution_providers), + device = map_device(facefusion.globals.execution_providers), scale = model_scale ) return FRAME_PROCESSOR @@ -70,26 +70,26 @@ def clear_frame_processor() -> None: FRAME_PROCESSOR = None -def get_options(key : Literal[ 'model' ]) -> Any: +def get_options(key : Literal['model']) -> Any: global OPTIONS if OPTIONS is None: - OPTIONS = \ + OPTIONS =\ { 'model': MODELS[frame_processors_globals.frame_enhancer_model] } return OPTIONS.get(key) -def set_options(key : Literal[ 'model' ], value : Any) -> None: +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 = 'realesrgan_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 = 100, choices = range(101), metavar = '[0-100]') + 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: @@ -124,6 +124,7 @@ def pre_process(mode : ProcessMode) -> bool: def post_process() -> None: clear_frame_processor() clear_face_analyser() + clear_content_analyser() read_static_image.cache_clear() @@ -136,7 +137,8 @@ def enhance_frame(temp_frame : Frame) -> Frame: def blend_frame(temp_frame : Frame, paste_frame : Frame) -> Frame: frame_enhancer_blend = 1 - (frame_processors_globals.frame_enhancer_blend / 100) - temp_frame = cv2.resize(temp_frame, (paste_frame.shape[1], paste_frame.shape[0])) + 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 diff --git a/facefusion/processors/frame/typings.py b/facefusion/processors/frame/typings.py new file mode 100644 index 00000000..7323188a --- /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 old mode 100644 new mode 100755 index bfbdb849..64a24e8e --- a/facefusion/typing.py +++ b/facefusion/typing.py @@ -1,23 +1,40 @@ -from typing import Any, Literal, Callable, List, TypedDict, Dict -from insightface.app.common import Face +from collections import namedtuple +from typing import Any, Literal, Callable, List, Tuple, Dict, TypedDict import numpy -Face = Face +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] -ProcessMode = Literal[ 'output', 'preview', 'stream' ] -FaceRecognition = Literal[ 'reference', 'many' ] -FaceAnalyserDirection = Literal[ 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small' ] -FaceAnalyserAge = Literal[ 'child', 'teen', 'adult', 'senior' ] -FaceAnalyserGender = Literal[ 'male', 'female' ] -TempFrameFormat = Literal[ 'jpg', 'png' ] -OutputVideoEncoder = Literal[ 'libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc' ] +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] +ModelValue = Dict[str, Any] OptionsWithModel = TypedDict('OptionsWithModel', { 'model' : ModelValue diff --git a/facefusion/uis/assets/overrides.css b/facefusion/uis/assets/overrides.css index 25791ec9..86ca371d 100644 --- a/facefusion/uis/assets/overrides.css +++ b/facefusion/uis/assets/overrides.css @@ -30,3 +30,15 @@ { 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 index 0f3c919b..92ae5491 100644 --- a/facefusion/uis/choices.py +++ b/facefusion/uis/choices.py @@ -4,4 +4,4 @@ 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', '1280x720', '1920x1080', '2560x1440', '3840x2160' ] +webcam_resolutions : List[str] = [ '320x240', '640x480', '800x600', '1024x768', '1280x720', '1280x960', '1920x1080', '2560x1440', '3840x2160' ] diff --git a/facefusion/uis/components/common_options.py b/facefusion/uis/components/common_options.py index 18a6962d..0b3e2d39 100644 --- a/facefusion/uis/components/common_options.py +++ b/facefusion/uis/components/common_options.py @@ -3,7 +3,7 @@ import gradio import facefusion.globals from facefusion import wording -from facefusion.uis import choices +from facefusion.uis import choices as uis_choices COMMON_OPTIONS_CHECKBOX_GROUP : Optional[gradio.Checkboxgroup] = None @@ -22,7 +22,7 @@ def render() -> None: value.append('skip-download') COMMON_OPTIONS_CHECKBOX_GROUP = gradio.Checkboxgroup( label = wording.get('common_options_checkbox_group_label'), - choices = choices.common_options, + choices = uis_choices.common_options, value = value ) diff --git a/facefusion/uis/components/execution_queue_count.py b/facefusion/uis/components/execution_queue_count.py index 75f3c455..fc8a3c87 100644 --- a/facefusion/uis/components/execution_queue_count.py +++ b/facefusion/uis/components/execution_queue_count.py @@ -2,6 +2,7 @@ from typing import Optional import gradio import facefusion.globals +import facefusion.choices from facefusion import wording EXECUTION_QUEUE_COUNT_SLIDER : Optional[gradio.Slider] = None @@ -13,9 +14,9 @@ def render() -> None: EXECUTION_QUEUE_COUNT_SLIDER = gradio.Slider( label = wording.get('execution_queue_count_slider_label'), value = facefusion.globals.execution_queue_count, - step = 1, - minimum = 1, - maximum = 16 + 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] ) @@ -25,4 +26,3 @@ def listen() -> None: 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 index 2353ec11..615d1642 100644 --- a/facefusion/uis/components/execution_thread_count.py +++ b/facefusion/uis/components/execution_thread_count.py @@ -2,6 +2,7 @@ from typing import Optional import gradio import facefusion.globals +import facefusion.choices from facefusion import wording EXECUTION_THREAD_COUNT_SLIDER : Optional[gradio.Slider] = None @@ -13,9 +14,9 @@ def render() -> None: EXECUTION_THREAD_COUNT_SLIDER = gradio.Slider( label = wording.get('execution_thread_count_slider_label'), value = facefusion.globals.execution_thread_count, - step = 1, - minimum = 1, - maximum = 128 + 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] ) diff --git a/facefusion/uis/components/face_analyser.py b/facefusion/uis/components/face_analyser.py index bb533214..3c701182 100644 --- a/facefusion/uis/components/face_analyser.py +++ b/facefusion/uis/components/face_analyser.py @@ -2,49 +2,97 @@ from typing import Optional import gradio -import facefusion.choices 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_DIRECTION_DROPDOWN : Optional[gradio.Dropdown] = None +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_DIRECTION_DROPDOWN + 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 - FACE_ANALYSER_DIRECTION_DROPDOWN = gradio.Dropdown( - label = wording.get('face_analyser_direction_dropdown_label'), - choices = facefusion.choices.face_analyser_directions, - value = facefusion.globals.face_analyser_direction + 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_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_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_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_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_direction_dropdown', FACE_ANALYSER_DIRECTION_DROPDOWN) + 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_DIRECTION_DROPDOWN.select(lambda value: update_dropdown('face_analyser_direction', value), inputs = FACE_ANALYSER_DIRECTION_DROPDOWN) - FACE_ANALYSER_AGE_DROPDOWN.select(lambda value: update_dropdown('face_analyser_age', value), inputs = FACE_ANALYSER_AGE_DROPDOWN) - FACE_ANALYSER_GENDER_DROPDOWN.select(lambda value: update_dropdown('face_analyser_gender', value), inputs = FACE_ANALYSER_GENDER_DROPDOWN) + 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_dropdown(name : str, value : str) -> None: - if value == 'none': - setattr(facefusion.globals, name, None) - else: - setattr(facefusion.globals, name, value) +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 100755 index 00000000..08780fba --- /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 index 140d9dea..5ac5f5ed 100644 --- a/facefusion/uis/components/face_selector.py +++ b/facefusion/uis/components/face_selector.py @@ -2,35 +2,35 @@ from typing import List, Optional, Tuple, Any, Dict import gradio -import facefusion.choices import facefusion.globals +import facefusion.choices from facefusion import wording -from facefusion.vision import get_video_frame, normalize_frame_color, read_static_image +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, FaceRecognition +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_RECOGNITION_DROPDOWN : Optional[gradio.Dropdown] = None +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_RECOGNITION_DROPDOWN + 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'), - 'height': 120, 'object_fit': 'cover', - 'columns': 10, + 'columns': 8, 'allow_preview': False, - 'visible': 'reference' in facefusion.globals.face_recognition + 'visible': 'reference' in facefusion.globals.face_selector_mode } if is_image(facefusion.globals.target_path): reference_frame = read_static_image(facefusion.globals.target_path) @@ -38,32 +38,31 @@ def render() -> None: 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_RECOGNITION_DROPDOWN = gradio.Dropdown( - label = wording.get('face_recognition_dropdown_label'), - choices = facefusion.choices.face_recognitions, - value = facefusion.globals.face_recognition + 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 = 0.05, - minimum = 0, - maximum = 3, - visible = 'reference' in facefusion.globals.face_recognition + 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_recognition_dropdown', FACE_RECOGNITION_DROPDOWN) + 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_RECOGNITION_DROPDOWN.select(update_face_recognition, inputs = FACE_RECOGNITION_DROPDOWN, outputs = [ REFERENCE_FACE_POSITION_GALLERY, REFERENCE_FACE_DISTANCE_SLIDER ]) - REFERENCE_FACE_POSITION_GALLERY.select(clear_and_update_face_reference_position) + 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] =\ [ - 'source_image', 'target_image', 'target_video' ] @@ -71,39 +70,73 @@ def listen() -> None: component = get_ui_component(component_name) if component: for method in [ 'upload', 'change', 'clear' ]: - getattr(component, method)(update_face_reference_position, outputs = REFERENCE_FACE_POSITION_GALLERY) - select_component_names : List[ComponentName] =\ + 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_direction_dropdown', + 'face_analyser_order_dropdown', 'face_analyser_age_dropdown', 'face_analyser_gender_dropdown' ] - for component_name in select_component_names: + for component_name in change_one_component_names: component = get_ui_component(component_name) if component: - component.select(update_face_reference_position, outputs = REFERENCE_FACE_POSITION_GALLERY) + 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.release(update_face_reference_position, outputs = REFERENCE_FACE_POSITION_GALLERY) + 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_recognition(face_recognition : FaceRecognition) -> Tuple[gradio.Gallery, gradio.Slider]: - if face_recognition == 'reference': - facefusion.globals.face_recognition = face_recognition +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_recognition == 'many': - facefusion.globals.face_recognition = face_recognition + 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_face_reference_position(event: gradio.SelectData) -> gradio.Gallery: +def clear_and_update_reference_face_position(event : gradio.SelectData) -> gradio.Gallery: clear_face_reference() - return update_face_reference_position(event.index) + clear_faces_cache() + update_reference_face_position(event.index) + return update_reference_position_gallery() -def update_face_reference_position(reference_face_position : int = 0) -> gradio.Gallery: - gallery_frames = [] +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) @@ -115,15 +148,11 @@ def update_face_reference_position(reference_face_position : int = 0) -> gradio. return gradio.Gallery(value = None) -def update_reference_face_distance(reference_face_distance : float) -> None: - facefusion.globals.reference_face_distance = reference_face_distance - - 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']) + 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) diff --git a/facefusion/uis/components/frame_processors_options.py b/facefusion/uis/components/frame_processors_options.py old mode 100644 new mode 100755 index 7125ea21..0b4aa547 --- a/facefusion/uis/components/frame_processors_options.py +++ b/facefusion/uis/components/frame_processors_options.py @@ -5,6 +5,7 @@ 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 @@ -12,6 +13,7 @@ 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: @@ -20,6 +22,7 @@ def render() -> None: 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'), @@ -36,9 +39,9 @@ def render() -> None: FACE_ENHANCER_BLEND_SLIDER = gradio.Slider( label = wording.get('face_enhancer_blend_slider_label'), value = frame_processors_globals.face_enhancer_blend, - step = 1, - minimum = 0, - maximum = 100, + 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( @@ -50,16 +53,24 @@ def render() -> None: FRAME_ENHANCER_BLEND_SLIDER = gradio.Slider( label = wording.get('frame_enhancer_blend_slider_label'), value = frame_processors_globals.frame_enhancer_blend, - step = 1, - minimum = 0, - maximum = 100, + 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: @@ -68,13 +79,20 @@ def listen() -> None: 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 ]) + 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 : str) -> gradio.Dropdown: +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]) @@ -83,7 +101,7 @@ def update_face_swapper_model(face_swapper_model : str) -> gradio.Dropdown: return gradio.Dropdown(value = face_swapper_model) -def update_face_enhancer_model(face_enhancer_model : str) -> gradio.Dropdown: +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() @@ -97,7 +115,7 @@ 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 : str) -> gradio.Dropdown: +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() @@ -111,8 +129,13 @@ def update_frame_enhancer_blend(frame_enhancer_blend : int) -> None: frame_processors_globals.frame_enhancer_blend = frame_enhancer_blend -def toggle_face_swapper_model(frame_processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Dropdown, gradio.Slider, gradio.Dropdown, gradio.Slider]: +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 - 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) + 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 index 7b6a4298..6703cf1a 100644 --- a/facefusion/uis/components/limit_resources.py +++ b/facefusion/uis/components/limit_resources.py @@ -2,6 +2,7 @@ from typing import Optional import gradio import facefusion.globals +import facefusion.choices from facefusion import wording MAX_MEMORY_SLIDER : Optional[gradio.Slider] = None @@ -12,9 +13,9 @@ def render() -> None: MAX_MEMORY_SLIDER = gradio.Slider( label = wording.get('max_memory_slider_label'), - step = 1, - minimum = 0, - maximum = 128 + 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] ) diff --git a/facefusion/uis/components/output_options.py b/facefusion/uis/components/output_options.py index db5d01ec..900a92cc 100644 --- a/facefusion/uis/components/output_options.py +++ b/facefusion/uis/components/output_options.py @@ -2,8 +2,8 @@ from typing import Optional, Tuple, List import tempfile import gradio -import facefusion.choices import facefusion.globals +import facefusion.choices from facefusion import wording from facefusion.typing import OutputVideoEncoder from facefusion.utilities import is_image, is_video @@ -30,9 +30,9 @@ def render() -> None: OUTPUT_IMAGE_QUALITY_SLIDER = gradio.Slider( label = wording.get('output_image_quality_slider_label'), value = facefusion.globals.output_image_quality, - step = 1, - minimum = 0, - maximum = 100, + 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( @@ -44,9 +44,9 @@ def render() -> None: OUTPUT_VIDEO_QUALITY_SLIDER = gradio.Slider( label = wording.get('output_video_quality_slider_label'), value = facefusion.globals.output_video_quality, - step = 1, - minimum = 0, - maximum = 100, + 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) diff --git a/facefusion/uis/components/preview.py b/facefusion/uis/components/preview.py old mode 100644 new mode 100755 index 92a15b38..7d0fddfe --- a/facefusion/uis/components/preview.py +++ b/facefusion/uis/components/preview.py @@ -4,11 +4,13 @@ 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 -from facefusion.face_reference import get_face_reference, set_face_reference -from facefusion.predictor import predict_frame +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 @@ -37,7 +39,7 @@ def render() -> None: } 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_recognition else None + 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) @@ -57,34 +59,31 @@ def render() -> None: def listen() -> None: PREVIEW_FRAME_SLIDER.change(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) - multi_component_names : List[ComponentName] =\ + multi_one_component_names : List[ComponentName] =\ [ 'source_image', 'target_image', 'target_video' ] - for component_name in multi_component_names: + 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) - getattr(component, method)(update_preview_frame_slider, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_FRAME_SLIDER) - update_component_names : List[ComponentName] =\ + multi_two_component_names : List[ComponentName] =\ [ - 'face_recognition_dropdown', - 'frame_processors_checkbox_group', - 'face_swapper_model_dropdown', - 'face_enhancer_model_dropdown', - 'frame_enhancer_model_dropdown' + 'target_image', + 'target_video' ] - for component_name in update_component_names: + for component_name in multi_two_component_names: component = get_ui_component(component_name) if component: - component.change(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + 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_direction_dropdown', + 'face_analyser_order_dropdown', 'face_analyser_age_dropdown', 'face_analyser_gender_dropdown' ] @@ -92,49 +91,73 @@ def listen() -> None: component = get_ui_component(component_name) if component: component.select(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) - change_component_names : List[ComponentName] =\ + change_one_component_names : List[ComponentName] =\ [ - 'reference_face_distance_slider', + 'frame_processors_checkbox_group', + 'face_debugger_items_checkbox_group', + 'face_enhancer_model_dropdown', 'face_enhancer_blend_slider', - 'frame_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_component_names: + 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_recognition else None + 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): - facefusion.globals.reference_frame_number = frame_number - temp_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) + 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(frame_number : int = 0) -> gradio.Slider: - if is_image(facefusion.globals.target_path): - return gradio.Slider(value = None, maximum = None, visible = False) +def update_preview_frame_slider() -> gradio.Slider: if is_video(facefusion.globals.target_path): - facefusion.globals.reference_frame_number = frame_number video_frame_total = count_video_frame_total(facefusion.globals.target_path) return gradio.Slider(maximum = video_frame_total, visible = True) - return gradio.Slider() + 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 predict_frame(temp_frame): + if analyse_frame(temp_frame): return cv2.GaussianBlur(temp_frame, (99, 99), 0) for frame_processor in facefusion.globals.frame_processors: frame_processor_module = load_frame_processor_module(frame_processor) @@ -145,10 +168,3 @@ def process_preview_frame(source_face : Face, reference_face : Face, temp_frame temp_frame ) return temp_frame - - -def conditional_set_face_reference() -> None: - if 'reference' in facefusion.globals.face_recognition and not get_face_reference(): - reference_frame = get_video_frame(facefusion.globals.target_path, facefusion.globals.reference_frame_number) - reference_face = get_one_face(reference_frame, facefusion.globals.reference_face_position) - set_face_reference(reference_face) diff --git a/facefusion/uis/components/target.py b/facefusion/uis/components/target.py index cd404bbd..b89ac187 100644 --- a/facefusion/uis/components/target.py +++ b/facefusion/uis/components/target.py @@ -3,6 +3,7 @@ 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 @@ -51,6 +52,7 @@ def listen() -> None: 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) diff --git a/facefusion/uis/components/temp_frame.py b/facefusion/uis/components/temp_frame.py index 3b345f66..dfab64fe 100644 --- a/facefusion/uis/components/temp_frame.py +++ b/facefusion/uis/components/temp_frame.py @@ -1,8 +1,8 @@ from typing import Optional, Tuple import gradio -import facefusion.choices import facefusion.globals +import facefusion.choices from facefusion import wording from facefusion.typing import TempFrameFormat from facefusion.utilities import is_video @@ -25,9 +25,9 @@ def render() -> None: TEMP_FRAME_QUALITY_SLIDER = gradio.Slider( label = wording.get('temp_frame_quality_slider_label'), value = facefusion.globals.temp_frame_quality, - step = 1, - minimum = 0, - maximum = 100, + 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) ) diff --git a/facefusion/uis/components/trim_frame.py b/facefusion/uis/components/trim_frame.py index 82e771e4..1e6048c3 100644 --- a/facefusion/uis/components/trim_frame.py +++ b/facefusion/uis/components/trim_frame.py @@ -39,8 +39,9 @@ def render() -> None: 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 - TRIM_FRAME_START_SLIDER = gradio.Slider(**trim_frame_start_slider_args) - TRIM_FRAME_END_SLIDER = gradio.Slider(**trim_frame_end_slider_args) + 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: diff --git a/facefusion/uis/components/webcam.py b/facefusion/uis/components/webcam.py index 0431228c..0b7ba8d0 100644 --- a/facefusion/uis/components/webcam.py +++ b/facefusion/uis/components/webcam.py @@ -10,7 +10,7 @@ from tqdm import tqdm import facefusion.globals from facefusion import wording -from facefusion.predictor import predict_stream +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 @@ -19,11 +19,33 @@ 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 @@ -50,9 +72,6 @@ def listen() -> None: 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_mode_radio.change(stop, outputs = WEBCAM_IMAGE, cancels = start_event) - webcam_resolution_dropdown.change(stop, outputs = WEBCAM_IMAGE, cancels = start_event) - webcam_fps_slider.change(stop, outputs = WEBCAM_IMAGE, cancels = start_event) WEBCAM_STOP_BUTTON.click(stop, cancels = start_event) source_image = get_ui_component('source_image') if source_image: @@ -60,57 +79,53 @@ def listen() -> None: getattr(source_image, method)(stop, cancels = start_event) -def start(mode: WebcamMode, resolution: str, fps: float) -> Generator[Frame, None, None]: - facefusion.globals.face_recognition = 'many' +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] - capture = capture_webcam(resolution, fps) - if capture.isOpened(): - for capture_frame in multi_process_capture(source_face, capture): - if stream is not None: + 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 normalize_frame_color(capture_frame) + yield None -def multi_process_capture(source_face: Face, capture : cv2.VideoCapture) -> Generator[Frame, None, None]: - progress = tqdm(desc = wording.get('processing'), unit = 'frame', dynamic_ncols = True) - with ThreadPoolExecutor(max_workers = facefusion.globals.execution_thread_count) as executor: - futures = [] - deque_capture_frames : Deque[Frame] = deque() - while True: - _, capture_frame = capture.read() - if predict_stream(capture_frame): - 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: - yield deque_capture_frames.popleft() - progress.update() +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 capture_webcam(resolution : str, fps : float) -> cv2.VideoCapture: - width, height = resolution.split('x') - if platform.system().lower() == 'windows': - capture = cv2.VideoCapture(0, cv2.CAP_DSHOW) - else: - capture = cv2.VideoCapture(0) - capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) # type: ignore[attr-defined] - capture.set(cv2.CAP_PROP_FRAME_WIDTH, int(width)) - capture.set(cv2.CAP_PROP_FRAME_HEIGHT, int(height)) - capture.set(cv2.CAP_PROP_FPS, fps) - return capture - - 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'): diff --git a/facefusion/uis/components/webcam_options.py b/facefusion/uis/components/webcam_options.py index 2876e616..edb245c8 100644 --- a/facefusion/uis/components/webcam_options.py +++ b/facefusion/uis/components/webcam_options.py @@ -2,7 +2,7 @@ from typing import Optional import gradio from facefusion import wording -from facefusion.uis import choices +from facefusion.uis import choices as uis_choices from facefusion.uis.core import register_ui_component WEBCAM_MODE_RADIO : Optional[gradio.Radio] = None @@ -17,13 +17,13 @@ def render() -> None: WEBCAM_MODE_RADIO = gradio.Radio( label = wording.get('webcam_mode_radio_label'), - choices = choices.webcam_modes, + choices = uis_choices.webcam_modes, value = 'inline' ) WEBCAM_RESOLUTION_DROPDOWN = gradio.Dropdown( label = wording.get('webcam_resolution_dropdown'), - choices = choices.webcam_resolutions, - value = choices.webcam_resolutions[0] + choices = uis_choices.webcam_resolutions, + value = uis_choices.webcam_resolutions[0] ) WEBCAM_FPS_SLIDER = gradio.Slider( label = wording.get('webcam_fps_slider'), diff --git a/facefusion/uis/core.py b/facefusion/uis/core.py index 8e52e9a5..d8b565e0 100644 --- a/facefusion/uis/core.py +++ b/facefusion/uis/core.py @@ -44,13 +44,13 @@ def get_ui_layouts_modules(ui_layouts : List[str]) -> List[ModuleType]: return UI_LAYOUT_MODULES -def get_ui_component(name: ComponentName) -> Optional[Component]: +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: +def register_ui_component(name : ComponentName, component: Component) -> None: UI_COMPONENTS[name] = component diff --git a/facefusion/uis/layouts/benchmark.py b/facefusion/uis/layouts/benchmark.py index 2a197f6e..829db2fd 100644 --- a/facefusion/uis/layouts/benchmark.py +++ b/facefusion/uis/layouts/benchmark.py @@ -43,7 +43,7 @@ def render() -> gradio.Blocks: limit_resources.render() with gradio.Blocks(): benchmark_options.render() - with gradio.Column(scale= 5): + with gradio.Column(scale = 5): with gradio.Blocks(): benchmark.render() return layout diff --git a/facefusion/uis/layouts/default.py b/facefusion/uis/layouts/default.py old mode 100644 new mode 100755 index d13dbad9..3e9da2e7 --- a/facefusion/uis/layouts/default.py +++ b/facefusion/uis/layouts/default.py @@ -1,6 +1,6 @@ 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, preview, trim_frame, face_analyser, face_selector, output +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: @@ -30,6 +30,8 @@ def render() -> 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() @@ -40,14 +42,14 @@ def render() -> gradio.Blocks: with gradio.Column(scale = 3): with gradio.Blocks(): preview.render() - with gradio.Row(): + with gradio.Blocks(): trim_frame.render() with gradio.Blocks(): face_selector.render() - with gradio.Row(): - face_analyser.render() with gradio.Blocks(): - common_options.render() + face_mask.render() + with gradio.Blocks(): + face_analyser.render() return layout @@ -63,11 +65,12 @@ def listen() -> None: common_options.listen() source.listen() target.listen() + output.listen() preview.listen() trim_frame.listen() face_selector.listen() + face_mask.listen() face_analyser.listen() - output.listen() def run(ui : gradio.Blocks) -> None: diff --git a/facefusion/uis/typing.py b/facefusion/uis/typing.py index e7e800c6..e104d0fc 100644 --- a/facefusion/uis/typing.py +++ b/facefusion/uis/typing.py @@ -8,25 +8,33 @@ ComponentName = Literal\ 'target_image', 'target_video', 'preview_frame_slider', - 'face_recognition_dropdown', + 'face_selector_mode_dropdown', 'reference_face_position_gallery', 'reference_face_distance_slider', - 'face_analyser_direction_dropdown', + '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', - 'player_url_textbox_label', 'webcam_mode_radio', 'webcam_resolution_dropdown', 'webcam_fps_slider' ] -WebcamMode = Literal[ 'inline', 'udp', 'v4l2' ] -StreamMode = Literal[ 'udp', 'v4l2' ] +WebcamMode = Literal['inline', 'udp', 'v4l2'] +StreamMode = Literal['udp', 'v4l2'] diff --git a/facefusion/utilities.py b/facefusion/utilities.py index 48fb860b..dcdaae13 100644 --- a/facefusion/utilities.py +++ b/facefusion/utilities.py @@ -1,20 +1,22 @@ -from typing import List, Optional +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 mimetypes +import filetype import os import platform import shutil import ssl import subprocess import tempfile -import urllib +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') @@ -94,7 +96,7 @@ def restore_audio(target_path : str, output_path : str) -> bool: 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 ]) + commands.extend([ '-i', target_path, '-c', 'copy', '-map', '0:v:0', '-map', '1:a:0', '-shortest', '-y', output_path ]) return run_ffmpeg(commands) @@ -118,21 +120,6 @@ def get_temp_output_video_path(target_path : str) -> str: return os.path.join(temp_directory_path, TEMP_OUTPUT_VIDEO_NAME) -def normalize_output_path(source_path : Optional[str], target_path : Optional[str], output_path : Optional[str]) -> Optional[str]: - if is_file(source_path) and is_file(target_path) and is_directory(output_path): - source_name, _ = os.path.splitext(os.path.basename(source_path)) - target_name, target_extension = os.path.splitext(os.path.basename(target_path)) - return os.path.join(output_path, source_name + '-' + target_name + target_extension) - if is_file(target_path) and output_path: - target_name, 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 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) @@ -155,6 +142,35 @@ def clear_temp(target_path : str) -> None: 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)) @@ -165,19 +181,22 @@ def is_directory(directory_path : str) -> bool: def is_image(image_path : str) -> bool: if is_file(image_path): - mimetype, _ = mimetypes.guess_type(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, _ = mimetypes.guess_type(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) @@ -186,7 +205,7 @@ def conditional_download(download_directory_path : str, urls : List[str]) -> Non else: initial = 0 if initial < total: - with tqdm(total = total, initial = initial, desc = wording.get('downloading'), unit = 'B', unit_scale = True, unit_divisor = 1024) as progress: + 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: @@ -198,7 +217,7 @@ def conditional_download(download_directory_path : str, urls : List[str]) -> Non @lru_cache(maxsize = None) def get_download_size(url : str) -> int: try: - response = urllib.request.urlopen(url) # type: ignore[attr-defined] + response = urllib.request.urlopen(url, timeout = 10) return int(response.getheader('Content-Length')) except (OSError, ValueError): return 0 @@ -217,7 +236,7 @@ def resolve_relative_path(path : str) -> str: 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 [ Path(file).stem for file in files if not Path(file).stem.startswith(('.', '__')) ] return None @@ -231,9 +250,19 @@ def decode_execution_providers(execution_providers: List[str]) -> List[str]: 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 get_device(execution_providers : List[str]) -> str: - if 'CUDAExecutionProvider' in execution_providers: - return 'cuda' +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 index baf40bc2..f5ee547d 100644 --- a/facefusion/vision.py +++ b/facefusion/vision.py @@ -7,12 +7,12 @@ from facefusion.typing import Frame def get_video_frame(video_path : str, frame_number : int = 0) -> Optional[Frame]: if video_path: - capture = cv2.VideoCapture(video_path) - if capture.isOpened(): - frame_total = capture.get(cv2.CAP_PROP_FRAME_COUNT) - capture.set(cv2.CAP_PROP_POS_FRAMES, min(frame_total, frame_number - 1)) - has_frame, frame = capture.read() - capture.release() + 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 @@ -20,18 +20,18 @@ def get_video_frame(video_path : str, frame_number : int = 0) -> Optional[Frame] def detect_fps(video_path : str) -> Optional[float]: if video_path: - capture = cv2.VideoCapture(video_path) - if capture.isOpened(): - return capture.get(cv2.CAP_PROP_FPS) + 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: - capture = cv2.VideoCapture(video_path) - if capture.isOpened(): - video_frame_total = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) - capture.release() + 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 diff --git a/facefusion/wording.py b/facefusion/wording.py old mode 100644 new mode 100755 index 884d5b10..c7c739bd --- a/facefusion/wording.py +++ b/facefusion/wording.py @@ -7,19 +7,25 @@ WORDING =\ '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 from the mode for the frame processor', + '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_recognition_help': 'specify the method for face recognition', - 'face_analyser_direction_help': 'specify the direction used for face analysis', - 'face_analyser_age_help': 'specify the age used for face analysis', - 'face_analyser_gender_help': 'specify the gender used for face analysis', + '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', @@ -28,13 +34,14 @@ WORDING =\ '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 (choices: {choices}, ...)', + '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', @@ -70,12 +77,20 @@ WORDING =\ '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_direction_dropdown_label': 'FACE ANALYSER DIRECTION', + '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', - 'face_recognition_dropdown_label': 'FACE RECOGNITION', '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', @@ -90,6 +105,7 @@ WORDING =\ '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', diff --git a/requirements.txt b/requirements.txt index 49b421fc..66a16b95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,11 @@ basicsr==1.4.2 -gradio==3.47.1 -insightface==0.7.3 -numpy==1.24.3 -onnx==1.14.1 +filetype==1.2.0 +gradio==3.50.2 +numpy==1.26.1 +onnx==1.15.0 onnxruntime==1.16.0 opencv-python==4.8.1.78 -opennsfw2==0.10.2 -pillow==10.0.1 -protobuf==4.24.2 -psutil==5.9.5 +psutil==5.9.6 realesrgan==0.3.0 -tensorflow==2.13.0 torch==2.1.0 tqdm==4.66.1 diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 2fc18ce3..934b3400 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -4,7 +4,7 @@ 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, is_file, is_directory, is_image, is_video, get_download_size, is_download_done, encode_execution_providers, decode_execution_providers +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) @@ -107,6 +107,7 @@ 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' @@ -116,6 +117,14 @@ def test_normalize_output_path() -> 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