diff --git a/.github/preview.png b/.github/preview.png index 5a2644a9..d2f2942a 100644 Binary files a/.github/preview.png and b/.github/preview.png differ diff --git a/facefusion/face_landmarker.py b/facefusion/face_landmarker.py index 66d5d188..1af1b002 100644 --- a/facefusion/face_landmarker.py +++ b/facefusion/face_landmarker.py @@ -42,6 +42,32 @@ def create_static_model_set(download_scope : DownloadScope) -> ModelSet: }, 'size': (256, 256) }, + 'hrffa': + { + '__metadata__': + { + 'vendor': 'PINTO0309', + 'license': 'MIT', + 'year': 2025 + }, + 'hashes': + { + 'hrffa': + { + 'url': resolve_download_url('models-3.9.0', 'hrffa.hash'), + 'path': resolve_relative_path('../.assets/models/hrffa.hash') + } + }, + 'sources': + { + 'hrffa': + { + 'url': resolve_download_url('models-3.9.0', 'hrffa.onnx'), + 'path': resolve_relative_path('../.assets/models/hrffa.onnx') + } + }, + 'size': (256, 256) + }, 'peppa_wutz': { '__metadata__': @@ -119,6 +145,10 @@ def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]: 'fan_68_5': model_set.get('fan_68_5').get('sources').get('fan_68_5') } + if state_manager.get_item('face_landmarker_model') == 'hrffa': + model_hash_set['hrffa'] = model_set.get('hrffa').get('hashes').get('hrffa') + model_source_set['hrffa'] = model_set.get('hrffa').get('sources').get('hrffa') + for face_landmarker_model in [ '2dfan4', 'peppa_wutz' ]: if state_manager.get_item('face_landmarker_model') in [ 'many', face_landmarker_model ]: model_hash_set[face_landmarker_model] = model_set.get(face_landmarker_model).get('hashes').get(face_landmarker_model) @@ -134,19 +164,21 @@ def pre_check() -> bool: def detect_face_landmark(vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]: - face_landmark_2dfan4 = None - face_landmark_peppa_wutz = None - face_landmark_score_2dfan4 = 0.0 - face_landmark_score_peppa_wutz = 0.0 + if state_manager.get_item('face_landmarker_model') == '2dfan4': + return detect_with_2dfan4(vision_frame, bounding_box, face_angle) - if state_manager.get_item('face_landmarker_model') in [ 'many', '2dfan4' ]: - face_landmark_2dfan4, face_landmark_score_2dfan4 = detect_with_2dfan4(vision_frame, bounding_box, face_angle) + if state_manager.get_item('face_landmarker_model') == 'hrffa': + return detect_with_hrffa(vision_frame, bounding_box) - if state_manager.get_item('face_landmarker_model') in [ 'many', 'peppa_wutz' ]: - face_landmark_peppa_wutz, face_landmark_score_peppa_wutz = detect_with_peppa_wutz(vision_frame, bounding_box, face_angle) + if state_manager.get_item('face_landmarker_model') == 'peppa_wutz': + return detect_with_peppa_wutz(vision_frame, bounding_box, face_angle) + + face_landmark_2dfan4, face_landmark_score_2dfan4 = detect_with_2dfan4(vision_frame, bounding_box, face_angle) + face_landmark_peppa_wutz, face_landmark_score_peppa_wutz = detect_with_peppa_wutz(vision_frame, bounding_box, face_angle) if face_landmark_score_2dfan4 > face_landmark_score_peppa_wutz - 0.2: return face_landmark_2dfan4, face_landmark_score_2dfan4 + return face_landmark_peppa_wutz, face_landmark_score_peppa_wutz @@ -155,10 +187,12 @@ def detect_with_2dfan4(temp_vision_frame: VisionFrame, bounding_box: BoundingBox scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None) translation = (model_size[0] - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5 rotation_matrix, rotation_size = create_rotation_matrix_and_size(face_angle, model_size) + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, model_size) crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotation_matrix, rotation_size) crop_vision_frame = conditional_optimize_contrast(crop_vision_frame) crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0 + face_landmark_68, face_heatmap = forward_with_2dfan4(crop_vision_frame) face_landmark_68 = face_landmark_68[:, :, :2][0] / 64 * 256 face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotation_matrix)) @@ -166,6 +200,25 @@ def detect_with_2dfan4(temp_vision_frame: VisionFrame, bounding_box: BoundingBox face_landmark_score_68 = numpy.amax(face_heatmap, axis = (2, 3)) face_landmark_score_68 = numpy.mean(face_landmark_score_68) face_landmark_score_68 = numpy.interp(face_landmark_score_68, [ 0, 0.9 ], [ 0, 1 ]) + + return face_landmark_68, face_landmark_score_68 + + +def detect_with_hrffa(temp_vision_frame : VisionFrame, bounding_box : BoundingBox) -> Tuple[FaceLandmark68, Score]: + model_size = create_static_model_set('full').get('hrffa').get('size') + scale = model_size[0] / (numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None) * 1.7) + translation = (model_size[0] - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5 + + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, model_size) + crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32) / 255.0 + crop_vision_frame = (crop_vision_frame - 0.5) / 0.5 + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + + face_landmark_68 = forward_with_hrffa(crop_vision_frame) + face_landmark_68 = face_landmark_68.reshape(-1, 2) * model_size[0] + face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix)) + face_landmark_score_68 = 1.0 + return face_landmark_68, face_landmark_score_68 @@ -174,17 +227,20 @@ def detect_with_peppa_wutz(temp_vision_frame : VisionFrame, bounding_box : Bound scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None) translation = (model_size[0] - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5 rotation_matrix, rotation_size = create_rotation_matrix_and_size(face_angle, model_size) + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, model_size) crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotation_matrix, rotation_size) crop_vision_frame = conditional_optimize_contrast(crop_vision_frame) crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0 crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + prediction = forward_with_peppa_wutz(crop_vision_frame) face_landmark_68 = prediction.reshape(-1, 3)[:, :2] / 64 * model_size[0] face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotation_matrix)) face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix)) face_landmark_score_68 = prediction.reshape(-1, 3)[:, 2].mean() face_landmark_score_68 = numpy.interp(face_landmark_score_68, [ 0, 0.95 ], [ 0, 1 ]) + return face_landmark_68, face_landmark_score_68 @@ -216,6 +272,18 @@ def forward_with_2dfan4(crop_vision_frame : VisionFrame) -> Tuple[Prediction, Pr return prediction +def forward_with_hrffa(crop_vision_frame : VisionFrame) -> Prediction: + face_landmarker = get_inference_pool().get('hrffa') + + with conditional_thread_semaphore(): + prediction = face_landmarker.run(None, + { + 'input': crop_vision_frame + })[0] + + return prediction + + def forward_with_peppa_wutz(crop_vision_frame : VisionFrame) -> Prediction: face_landmarker = get_inference_pool().get('peppa_wutz') diff --git a/facefusion/jobs/job_manager.py b/facefusion/jobs/job_manager.py index 4c9acda8..ca1cd8b5 100644 --- a/facefusion/jobs/job_manager.py +++ b/facefusion/jobs/job_manager.py @@ -10,7 +10,7 @@ from facefusion.sanitizer import sanitize_job_id from facefusion.time_helper import get_current_date_time from facefusion.types import Args, Job, JobSet, JobStatus, JobStep, JobStepStatus -JOBS_PATH : Optional[str] = None +JOBS_PATH : str = '.jobs' def init_jobs(jobs_path : str) -> bool: diff --git a/facefusion/metadata.py b/facefusion/metadata.py index 0d0a0e16..d63ad8bd 100644 --- a/facefusion/metadata.py +++ b/facefusion/metadata.py @@ -4,7 +4,7 @@ METADATA =\ { 'name': 'FaceFusion', 'description': 'Industry leading face manipulation platform', - 'version': '3.8.3', + 'version': '3.9.0', 'license': 'OpenRAIL-AS', 'author': 'Henry Ruhs', 'url': 'https://facefusion.io' diff --git a/facefusion/processors/modules/background_remover/core.py b/facefusion/processors/modules/background_remover/core.py index f6af0472..ecc8a299 100644 --- a/facefusion/processors/modules/background_remover/core.py +++ b/facefusion/processors/modules/background_remover/core.py @@ -122,7 +122,7 @@ def create_static_model_set(download_scope : DownloadScope) -> ModelSet: { 'vendor': 'nikopueringer', 'license': 'Non-Commercial', - 'year': 2025 + 'year': 2026 }, 'hashes': { @@ -151,7 +151,7 @@ def create_static_model_set(download_scope : DownloadScope) -> ModelSet: { 'vendor': 'nikopueringer', 'license': 'Non-Commercial', - 'year': 2025 + 'year': 2026 }, 'hashes': { diff --git a/facefusion/processors/modules/face_swapper/choices.py b/facefusion/processors/modules/face_swapper/choices.py index ad7122da..f66318a0 100644 --- a/facefusion/processors/modules/face_swapper/choices.py +++ b/facefusion/processors/modules/face_swapper/choices.py @@ -6,6 +6,7 @@ from facefusion.processors.modules.face_swapper.types import FaceSwapperModel, F face_swapper_set : FaceSwapperSet =\ { + 'alphaface_256': [ '256x256', '512x512', '768x768', '1024x1024' ], 'blendswap_256': [ '256x256', '384x384', '512x512', '768x768', '1024x1024' ], 'ghost_1_256': [ '256x256', '512x512', '768x768', '1024x1024' ], 'ghost_2_256': [ '256x256', '512x512', '768x768', '1024x1024' ], diff --git a/facefusion/processors/modules/face_swapper/core.py b/facefusion/processors/modules/face_swapper/core.py index da1736b2..08055e3d 100755 --- a/facefusion/processors/modules/face_swapper/core.py +++ b/facefusion/processors/modules/face_swapper/core.py @@ -33,6 +33,36 @@ from facefusion.vision import read_static_image, read_static_images, read_static def create_static_model_set(download_scope : DownloadScope) -> ModelSet: return\ { + 'alphaface_256': + { + '__metadata__': + { + 'vendor': 'AlphaFace', + 'license': 'Non-Commercial', + 'year': 2026 + }, + 'hashes': + { + 'face_swapper': + { + 'url': resolve_download_url('models-3.9.0', 'alphaface_256.hash'), + 'path': resolve_relative_path('../.assets/models/alphaface_256.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': resolve_download_url('models-3.9.0', 'alphaface_256.onnx'), + 'path': resolve_relative_path('../.assets/models/alphaface_256.onnx') + } + }, + 'type': 'alphaface', + 'template': 'arcface_128', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, 'blendswap_256': { '__metadata__': @@ -689,6 +719,10 @@ def prepare_source_frame(source_face : Face, source_vision_frame : VisionFrame) def prepare_source_embedding(source_face : Face) -> Embedding: model_type = get_model_options().get('type') + if model_type == 'alphaface': + source_embedding = source_face.embedding.reshape((1, -1)) + return source_embedding + if model_type == 'ghost': source_embedding = source_face.embedding.reshape(-1, 512) source_embedding, _ = convert_source_embedding(source_embedding) @@ -717,7 +751,7 @@ def balance_source_embedding(source_embedding : Embedding, target_embedding : Em face_swapper_weight = state_manager.get_item('face_swapper_weight') face_swapper_weight = numpy.interp(face_swapper_weight, [ 0, 1 ], [ 0.35, -0.35 ]).astype(numpy.float32) - if model_type in [ 'hififace', 'hyperswap', 'inswapper', 'simswap' ]: + if model_type in [ 'alphaface', 'hififace', 'hyperswap', 'inswapper', 'simswap' ]: target_embedding = target_embedding / numpy.linalg.norm(target_embedding) source_embedding = source_embedding.reshape(1, -1) diff --git a/facefusion/processors/modules/face_swapper/types.py b/facefusion/processors/modules/face_swapper/types.py index addda8de..1c7a71d4 100644 --- a/facefusion/processors/modules/face_swapper/types.py +++ b/facefusion/processors/modules/face_swapper/types.py @@ -11,7 +11,7 @@ FaceSwapperInputs = TypedDict('FaceSwapperInputs', 'temp_vision_mask' : Mask }) -FaceSwapperModel = Literal['blendswap_256', 'ghost_1_256', 'ghost_2_256', 'ghost_3_256', 'hififace_unofficial_256', 'hyperswap_1a_256', 'hyperswap_1b_256', 'hyperswap_1c_256', 'inswapper_128', 'inswapper_128_fp16', 'simswap_256', 'simswap_unofficial_512', 'uniface_256'] +FaceSwapperModel = Literal['alphaface_256', 'blendswap_256', 'ghost_1_256', 'ghost_2_256', 'ghost_3_256', 'hififace_unofficial_256', 'hyperswap_1a_256', 'hyperswap_1b_256', 'hyperswap_1c_256', 'inswapper_128', 'inswapper_128_fp16', 'simswap_256', 'simswap_unofficial_512', 'uniface_256'] FaceSwapperWeight : TypeAlias = float diff --git a/facefusion/types.py b/facefusion/types.py index b7abe057..dcf4ee84 100755 --- a/facefusion/types.py +++ b/facefusion/types.py @@ -175,7 +175,7 @@ TableHeader : TypeAlias = str TableContent : TypeAlias = Any FaceDetectorModel = Literal['many', 'retinaface', 'scrfd', 'yolo_face', 'yunet'] -FaceLandmarkerModel = Literal['many', '2dfan4', 'peppa_wutz'] +FaceLandmarkerModel = Literal['many', '2dfan4', 'hrffa', 'peppa_wutz'] FaceDetectorSet : TypeAlias = Dict[FaceDetectorModel, List[str]] FaceSelectorMode = Literal['many', 'one', 'reference'] FaceSelectorOrder = Literal['left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best'] diff --git a/tests/test_job_runner.py b/tests/test_job_runner.py index 48f28ef2..0f42180c 100644 --- a/tests/test_job_runner.py +++ b/tests/test_job_runner.py @@ -5,7 +5,7 @@ from facefusion import ffmpeg, ffmpeg_builder, process_manager from facefusion.download import conditional_download from facefusion.filesystem import copy_file from facefusion.jobs.job_manager import add_step, clear_jobs, create_job, init_jobs, move_job_file, submit_job, submit_jobs -from facefusion.jobs.job_runner import collect_output_set, finalize_steps, retry_job, retry_jobs, run_job, run_jobs, run_steps +from facefusion.jobs.job_runner import clean_steps, collect_output_set, finalize_steps, retry_job, retry_jobs, run_job, run_jobs, run_steps from facefusion.types import Args from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory @@ -241,6 +241,35 @@ def test_finalize_steps() -> None: assert is_test_output_file('output-3.jpg') is True +def test_clean_steps() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-2.jpg') + } + + create_job('job-test-clean-steps') + add_step('job-test-clean-steps', args_1) + add_step('job-test-clean-steps', args_2) + + copy_file(args_1.get('target_path'), get_test_output_file('output-1-job-test-clean-steps-0.mp4')) + copy_file(args_2.get('target_path'), get_test_output_file('output-2-job-test-clean-steps-1.jpg')) + + assert is_test_output_file('output-1-job-test-clean-steps-0.mp4') is True + assert is_test_output_file('output-2-job-test-clean-steps-1.jpg') is True + + assert clean_steps('job-test-clean-steps') is True + assert is_test_output_file('output-1-job-test-clean-steps-0.mp4') is False + assert is_test_output_file('output-2-job-test-clean-steps-1.jpg') is False + + def test_collect_output_set() -> None: args_1 =\ {