Spaces:
Sleeping
Sleeping
feat: init
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app.py +10 -0
- facefusion/__init__.py +0 -0
- facefusion/app_context.py +16 -0
- facefusion/args.py +117 -0
- facefusion/audio.py +139 -0
- facefusion/choices.py +64 -0
- facefusion/common_helper.py +72 -0
- facefusion/config.py +92 -0
- facefusion/content_analyser.py +124 -0
- facefusion/core.py +445 -0
- facefusion/date_helper.py +28 -0
- facefusion/download.py +131 -0
- facefusion/execution.py +134 -0
- facefusion/exit_helper.py +24 -0
- facefusion/face_analyser.py +124 -0
- facefusion/face_classifier.py +128 -0
- facefusion/face_detector.py +309 -0
- facefusion/face_helper.py +210 -0
- facefusion/face_landmarker.py +217 -0
- facefusion/face_masker.py +173 -0
- facefusion/face_recognizer.py +81 -0
- facefusion/face_selector.py +91 -0
- facefusion/face_store.py +53 -0
- facefusion/ffmpeg.py +176 -0
- facefusion/filesystem.py +140 -0
- facefusion/hash_helper.py +32 -0
- facefusion/inference_manager.py +79 -0
- facefusion/installer.py +93 -0
- facefusion/jobs/__init__.py +0 -0
- facefusion/jobs/job_helper.py +15 -0
- facefusion/jobs/job_list.py +34 -0
- facefusion/jobs/job_manager.py +263 -0
- facefusion/jobs/job_runner.py +106 -0
- facefusion/jobs/job_store.py +27 -0
- facefusion/json.py +22 -0
- facefusion/logger.py +80 -0
- facefusion/memory.py +21 -0
- facefusion/metadata.py +17 -0
- facefusion/normalizer.py +21 -0
- facefusion/process_manager.py +53 -0
- facefusion/processors/__init__.py +0 -0
- facefusion/processors/choices.py +46 -0
- facefusion/processors/core.py +110 -0
- facefusion/processors/live_portrait.py +101 -0
- facefusion/processors/modules/__init__.py +0 -0
- facefusion/processors/modules/age_modifier.py +268 -0
- facefusion/processors/modules/expression_restorer.py +290 -0
- facefusion/processors/modules/face_debugger.py +222 -0
- facefusion/processors/modules/face_editor.py +528 -0
- facefusion/processors/modules/face_enhancer.py +397 -0
app.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
|
3 |
+
import os
|
4 |
+
|
5 |
+
os.environ['OMP_NUM_THREADS'] = '1'
|
6 |
+
|
7 |
+
from facefusion import core
|
8 |
+
|
9 |
+
if __name__ == '__main__':
|
10 |
+
core.cli()
|
facefusion/__init__.py
ADDED
File without changes
|
facefusion/app_context.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
|
4 |
+
from facefusion.typing import AppContext
|
5 |
+
|
6 |
+
|
7 |
+
def detect_app_context() -> AppContext:
|
8 |
+
frame = sys._getframe(1)
|
9 |
+
|
10 |
+
while frame:
|
11 |
+
if os.path.join('facefusion', 'jobs') in frame.f_code.co_filename:
|
12 |
+
return 'cli'
|
13 |
+
if os.path.join('facefusion', 'uis') in frame.f_code.co_filename:
|
14 |
+
return 'ui'
|
15 |
+
frame = frame.f_back
|
16 |
+
return 'cli'
|
facefusion/args.py
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from facefusion import state_manager
|
2 |
+
from facefusion.filesystem import is_image, is_video, list_directory
|
3 |
+
from facefusion.jobs import job_store
|
4 |
+
from facefusion.normalizer import normalize_fps, normalize_padding
|
5 |
+
from facefusion.processors.core import get_processors_modules
|
6 |
+
from facefusion.typing import ApplyStateItem, Args
|
7 |
+
from facefusion.vision import create_image_resolutions, create_video_resolutions, detect_image_resolution, detect_video_fps, detect_video_resolution, pack_resolution
|
8 |
+
|
9 |
+
|
10 |
+
def reduce_step_args(args : Args) -> Args:
|
11 |
+
step_args =\
|
12 |
+
{
|
13 |
+
key: args[key] for key in args if key in job_store.get_step_keys()
|
14 |
+
}
|
15 |
+
return step_args
|
16 |
+
|
17 |
+
|
18 |
+
def collect_step_args() -> Args:
|
19 |
+
step_args =\
|
20 |
+
{
|
21 |
+
key: state_manager.get_item(key) for key in job_store.get_step_keys() #type:ignore[arg-type]
|
22 |
+
}
|
23 |
+
return step_args
|
24 |
+
|
25 |
+
|
26 |
+
def collect_job_args() -> Args:
|
27 |
+
job_args =\
|
28 |
+
{
|
29 |
+
key: state_manager.get_item(key) for key in job_store.get_job_keys() #type:ignore[arg-type]
|
30 |
+
}
|
31 |
+
return job_args
|
32 |
+
|
33 |
+
|
34 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
35 |
+
# general
|
36 |
+
apply_state_item('command', args.get('command'))
|
37 |
+
# paths
|
38 |
+
apply_state_item('jobs_path', args.get('jobs_path'))
|
39 |
+
apply_state_item('source_paths', args.get('source_paths'))
|
40 |
+
apply_state_item('target_path', args.get('target_path'))
|
41 |
+
apply_state_item('output_path', args.get('output_path'))
|
42 |
+
# face detector
|
43 |
+
apply_state_item('face_detector_model', args.get('face_detector_model'))
|
44 |
+
apply_state_item('face_detector_size', args.get('face_detector_size'))
|
45 |
+
apply_state_item('face_detector_angles', args.get('face_detector_angles'))
|
46 |
+
apply_state_item('face_detector_score', args.get('face_detector_score'))
|
47 |
+
# face landmarker
|
48 |
+
apply_state_item('face_landmarker_model', args.get('face_landmarker_model'))
|
49 |
+
apply_state_item('face_landmarker_score', args.get('face_landmarker_score'))
|
50 |
+
# face selector
|
51 |
+
state_manager.init_item('face_selector_mode', args.get('face_selector_mode'))
|
52 |
+
state_manager.init_item('face_selector_order', args.get('face_selector_order'))
|
53 |
+
state_manager.init_item('face_selector_age_start', args.get('face_selector_age_start'))
|
54 |
+
state_manager.init_item('face_selector_age_end', args.get('face_selector_age_end'))
|
55 |
+
state_manager.init_item('face_selector_gender', args.get('face_selector_gender'))
|
56 |
+
state_manager.init_item('face_selector_race', args.get('face_selector_race'))
|
57 |
+
state_manager.init_item('reference_face_position', args.get('reference_face_position'))
|
58 |
+
state_manager.init_item('reference_face_distance', args.get('reference_face_distance'))
|
59 |
+
state_manager.init_item('reference_frame_number', args.get('reference_frame_number'))
|
60 |
+
# face masker
|
61 |
+
apply_state_item('face_mask_types', args.get('face_mask_types'))
|
62 |
+
apply_state_item('face_mask_blur', args.get('face_mask_blur'))
|
63 |
+
apply_state_item('face_mask_padding', normalize_padding(args.get('face_mask_padding')))
|
64 |
+
apply_state_item('face_mask_regions', args.get('face_mask_regions'))
|
65 |
+
# frame extraction
|
66 |
+
apply_state_item('trim_frame_start', args.get('trim_frame_start'))
|
67 |
+
apply_state_item('trim_frame_end', args.get('trim_frame_end'))
|
68 |
+
apply_state_item('temp_frame_format', args.get('temp_frame_format'))
|
69 |
+
apply_state_item('keep_temp', args.get('keep_temp'))
|
70 |
+
# output creation
|
71 |
+
apply_state_item('output_image_quality', args.get('output_image_quality'))
|
72 |
+
if is_image(args.get('target_path')):
|
73 |
+
output_image_resolution = detect_image_resolution(args.get('target_path'))
|
74 |
+
output_image_resolutions = create_image_resolutions(output_image_resolution)
|
75 |
+
if args.get('output_image_resolution') in output_image_resolutions:
|
76 |
+
apply_state_item('output_image_resolution', args.get('output_image_resolution'))
|
77 |
+
else:
|
78 |
+
apply_state_item('output_image_resolution', pack_resolution(output_image_resolution))
|
79 |
+
apply_state_item('output_audio_encoder', args.get('output_audio_encoder'))
|
80 |
+
apply_state_item('output_video_encoder', args.get('output_video_encoder'))
|
81 |
+
apply_state_item('output_video_preset', args.get('output_video_preset'))
|
82 |
+
apply_state_item('output_video_quality', args.get('output_video_quality'))
|
83 |
+
if is_video(args.get('target_path')):
|
84 |
+
output_video_resolution = detect_video_resolution(args.get('target_path'))
|
85 |
+
output_video_resolutions = create_video_resolutions(output_video_resolution)
|
86 |
+
if args.get('output_video_resolution') in output_video_resolutions:
|
87 |
+
apply_state_item('output_video_resolution', args.get('output_video_resolution'))
|
88 |
+
else:
|
89 |
+
apply_state_item('output_video_resolution', pack_resolution(output_video_resolution))
|
90 |
+
if args.get('output_video_fps') or is_video(args.get('target_path')):
|
91 |
+
output_video_fps = normalize_fps(args.get('output_video_fps')) or detect_video_fps(args.get('target_path'))
|
92 |
+
apply_state_item('output_video_fps', output_video_fps)
|
93 |
+
apply_state_item('skip_audio', args.get('skip_audio'))
|
94 |
+
# processors
|
95 |
+
available_processors = list_directory('facefusion/processors/modules')
|
96 |
+
apply_state_item('processors', args.get('processors'))
|
97 |
+
for processor_module in get_processors_modules(available_processors):
|
98 |
+
processor_module.apply_args(args, apply_state_item)
|
99 |
+
# uis
|
100 |
+
apply_state_item('open_browser', args.get('open_browser'))
|
101 |
+
apply_state_item('ui_layouts', args.get('ui_layouts'))
|
102 |
+
apply_state_item('ui_workflow', args.get('ui_workflow'))
|
103 |
+
# execution
|
104 |
+
apply_state_item('execution_device_id', args.get('execution_device_id'))
|
105 |
+
apply_state_item('execution_providers', args.get('execution_providers'))
|
106 |
+
apply_state_item('execution_thread_count', args.get('execution_thread_count'))
|
107 |
+
apply_state_item('execution_queue_count', args.get('execution_queue_count'))
|
108 |
+
# memory
|
109 |
+
apply_state_item('video_memory_strategy', args.get('video_memory_strategy'))
|
110 |
+
apply_state_item('system_memory_limit', args.get('system_memory_limit'))
|
111 |
+
# misc
|
112 |
+
apply_state_item('skip_download', args.get('skip_download'))
|
113 |
+
apply_state_item('log_level', args.get('log_level'))
|
114 |
+
# jobs
|
115 |
+
apply_state_item('job_id', args.get('job_id'))
|
116 |
+
apply_state_item('job_status', args.get('job_status'))
|
117 |
+
apply_state_item('step_index', args.get('step_index'))
|
facefusion/audio.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
from typing import Any, List, Optional
|
3 |
+
|
4 |
+
import numpy
|
5 |
+
import scipy
|
6 |
+
from numpy._typing import NDArray
|
7 |
+
|
8 |
+
from facefusion.ffmpeg import read_audio_buffer
|
9 |
+
from facefusion.filesystem import is_audio
|
10 |
+
from facefusion.typing import Audio, AudioFrame, Fps, Mel, MelFilterBank, Spectrogram
|
11 |
+
from facefusion.voice_extractor import batch_extract_voice
|
12 |
+
|
13 |
+
|
14 |
+
@lru_cache(maxsize = 128)
|
15 |
+
def read_static_audio(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]:
|
16 |
+
return read_audio(audio_path, fps)
|
17 |
+
|
18 |
+
|
19 |
+
def read_audio(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]:
|
20 |
+
sample_rate = 48000
|
21 |
+
channel_total = 2
|
22 |
+
|
23 |
+
if is_audio(audio_path):
|
24 |
+
audio_buffer = read_audio_buffer(audio_path, sample_rate, channel_total)
|
25 |
+
audio = numpy.frombuffer(audio_buffer, dtype = numpy.int16).reshape(-1, 2)
|
26 |
+
audio = prepare_audio(audio)
|
27 |
+
spectrogram = create_spectrogram(audio)
|
28 |
+
audio_frames = extract_audio_frames(spectrogram, fps)
|
29 |
+
return audio_frames
|
30 |
+
return None
|
31 |
+
|
32 |
+
|
33 |
+
@lru_cache(maxsize = 128)
|
34 |
+
def read_static_voice(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]:
|
35 |
+
return read_voice(audio_path, fps)
|
36 |
+
|
37 |
+
|
38 |
+
def read_voice(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]:
|
39 |
+
sample_rate = 48000
|
40 |
+
channel_total = 2
|
41 |
+
chunk_size = 240 * 1024
|
42 |
+
step_size = 180 * 1024
|
43 |
+
|
44 |
+
if is_audio(audio_path):
|
45 |
+
audio_buffer = read_audio_buffer(audio_path, sample_rate, channel_total)
|
46 |
+
audio = numpy.frombuffer(audio_buffer, dtype = numpy.int16).reshape(-1, 2)
|
47 |
+
audio = batch_extract_voice(audio, chunk_size, step_size)
|
48 |
+
audio = prepare_voice(audio)
|
49 |
+
spectrogram = create_spectrogram(audio)
|
50 |
+
audio_frames = extract_audio_frames(spectrogram, fps)
|
51 |
+
return audio_frames
|
52 |
+
return None
|
53 |
+
|
54 |
+
|
55 |
+
def get_audio_frame(audio_path : str, fps : Fps, frame_number : int = 0) -> Optional[AudioFrame]:
|
56 |
+
if is_audio(audio_path):
|
57 |
+
audio_frames = read_static_audio(audio_path, fps)
|
58 |
+
if frame_number in range(len(audio_frames)):
|
59 |
+
return audio_frames[frame_number]
|
60 |
+
return None
|
61 |
+
|
62 |
+
|
63 |
+
def get_voice_frame(audio_path : str, fps : Fps, frame_number : int = 0) -> Optional[AudioFrame]:
|
64 |
+
if is_audio(audio_path):
|
65 |
+
voice_frames = read_static_voice(audio_path, fps)
|
66 |
+
if frame_number in range(len(voice_frames)):
|
67 |
+
return voice_frames[frame_number]
|
68 |
+
return None
|
69 |
+
|
70 |
+
|
71 |
+
def create_empty_audio_frame() -> AudioFrame:
|
72 |
+
mel_filter_total = 80
|
73 |
+
step_size = 16
|
74 |
+
audio_frame = numpy.zeros((mel_filter_total, step_size)).astype(numpy.int16)
|
75 |
+
return audio_frame
|
76 |
+
|
77 |
+
|
78 |
+
def prepare_audio(audio : Audio) -> Audio:
|
79 |
+
if audio.ndim > 1:
|
80 |
+
audio = numpy.mean(audio, axis = 1)
|
81 |
+
audio = audio / numpy.max(numpy.abs(audio), axis = 0)
|
82 |
+
audio = scipy.signal.lfilter([ 1.0, -0.97 ], [ 1.0 ], audio)
|
83 |
+
return audio
|
84 |
+
|
85 |
+
|
86 |
+
def prepare_voice(audio : Audio) -> Audio:
|
87 |
+
sample_rate = 48000
|
88 |
+
resample_rate = 16000
|
89 |
+
|
90 |
+
audio = scipy.signal.resample(audio, int(len(audio) * resample_rate / sample_rate))
|
91 |
+
audio = prepare_audio(audio)
|
92 |
+
return audio
|
93 |
+
|
94 |
+
|
95 |
+
def convert_hertz_to_mel(hertz : float) -> float:
|
96 |
+
return 2595 * numpy.log10(1 + hertz / 700)
|
97 |
+
|
98 |
+
|
99 |
+
def convert_mel_to_hertz(mel : Mel) -> NDArray[Any]:
|
100 |
+
return 700 * (10 ** (mel / 2595) - 1)
|
101 |
+
|
102 |
+
|
103 |
+
def create_mel_filter_bank() -> MelFilterBank:
|
104 |
+
mel_filter_total = 80
|
105 |
+
mel_bin_total = 800
|
106 |
+
sample_rate = 16000
|
107 |
+
min_frequency = 55.0
|
108 |
+
max_frequency = 7600.0
|
109 |
+
mel_filter_bank = numpy.zeros((mel_filter_total, mel_bin_total // 2 + 1))
|
110 |
+
mel_frequency_range = numpy.linspace(convert_hertz_to_mel(min_frequency), convert_hertz_to_mel(max_frequency), mel_filter_total + 2)
|
111 |
+
indices = numpy.floor((mel_bin_total + 1) * convert_mel_to_hertz(mel_frequency_range) / sample_rate).astype(numpy.int16)
|
112 |
+
|
113 |
+
for index in range(mel_filter_total):
|
114 |
+
start = indices[index]
|
115 |
+
end = indices[index + 1]
|
116 |
+
mel_filter_bank[index, start:end] = scipy.signal.windows.triang(end - start)
|
117 |
+
return mel_filter_bank
|
118 |
+
|
119 |
+
|
120 |
+
def create_spectrogram(audio : Audio) -> Spectrogram:
|
121 |
+
mel_bin_total = 800
|
122 |
+
mel_bin_overlap = 600
|
123 |
+
mel_filter_bank = create_mel_filter_bank()
|
124 |
+
spectrogram = scipy.signal.stft(audio, nperseg = mel_bin_total, nfft = mel_bin_total, noverlap = mel_bin_overlap)[2]
|
125 |
+
spectrogram = numpy.dot(mel_filter_bank, numpy.abs(spectrogram))
|
126 |
+
return spectrogram
|
127 |
+
|
128 |
+
|
129 |
+
def extract_audio_frames(spectrogram : Spectrogram, fps : Fps) -> List[AudioFrame]:
|
130 |
+
mel_filter_total = 80
|
131 |
+
step_size = 16
|
132 |
+
audio_frames = []
|
133 |
+
indices = numpy.arange(0, spectrogram.shape[1], mel_filter_total / fps).astype(numpy.int16)
|
134 |
+
indices = indices[indices >= step_size]
|
135 |
+
|
136 |
+
for index in indices:
|
137 |
+
start = max(0, index - step_size)
|
138 |
+
audio_frames.append(spectrogram[:, start:index])
|
139 |
+
return audio_frames
|
facefusion/choices.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from typing import List, Sequence
|
3 |
+
|
4 |
+
from facefusion.common_helper import create_float_range, create_int_range
|
5 |
+
from facefusion.typing import Angle, ExecutionProviderSet, FaceDetectorSet, FaceLandmarkerModel, FaceMaskRegion, FaceMaskType, FaceSelectorMode, FaceSelectorOrder, Gender, JobStatus, LogLevelSet, OutputAudioEncoder, OutputVideoEncoder, OutputVideoPreset, Race, Score, TempFrameFormat, UiWorkflow, VideoMemoryStrategy
|
6 |
+
|
7 |
+
video_memory_strategies : List[VideoMemoryStrategy] = [ 'strict', 'moderate', 'tolerant' ]
|
8 |
+
|
9 |
+
face_detector_set : FaceDetectorSet =\
|
10 |
+
{
|
11 |
+
'many': [ '640x640' ],
|
12 |
+
'retinaface': [ '160x160', '320x320', '480x480', '512x512', '640x640' ],
|
13 |
+
'scrfd': [ '160x160', '320x320', '480x480', '512x512', '640x640' ],
|
14 |
+
'yoloface': [ '640x640' ]
|
15 |
+
}
|
16 |
+
face_landmarker_models : List[FaceLandmarkerModel] = [ 'many', '2dfan4', 'peppa_wutz' ]
|
17 |
+
face_selector_modes : List[FaceSelectorMode] = [ 'many', 'one', 'reference' ]
|
18 |
+
face_selector_orders : List[FaceSelectorOrder] = [ 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best' ]
|
19 |
+
face_selector_genders : List[Gender] = ['female', 'male']
|
20 |
+
face_selector_races : List[Race] = ['white', 'black', 'latino', 'asian', 'indian', 'arabic']
|
21 |
+
face_mask_types : List[FaceMaskType] = [ 'box', 'occlusion', 'region' ]
|
22 |
+
face_mask_regions : List[FaceMaskRegion] = [ 'skin', 'left-eyebrow', 'right-eyebrow', 'left-eye', 'right-eye', 'glasses', 'nose', 'mouth', 'upper-lip', 'lower-lip' ]
|
23 |
+
temp_frame_formats : List[TempFrameFormat] = [ 'bmp', 'jpg', 'png' ]
|
24 |
+
output_audio_encoders : List[OutputAudioEncoder] = [ 'aac', 'libmp3lame', 'libopus', 'libvorbis' ]
|
25 |
+
output_video_encoders : List[OutputVideoEncoder] = [ 'libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc', 'h264_amf', 'hevc_amf', 'h264_videotoolbox', 'hevc_videotoolbox' ]
|
26 |
+
output_video_presets : List[OutputVideoPreset] = [ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow' ]
|
27 |
+
|
28 |
+
image_template_sizes : List[float] = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 3.5, 4 ]
|
29 |
+
video_template_sizes : List[int] = [ 240, 360, 480, 540, 720, 1080, 1440, 2160, 4320 ]
|
30 |
+
|
31 |
+
log_level_set : LogLevelSet =\
|
32 |
+
{
|
33 |
+
'error': logging.ERROR,
|
34 |
+
'warn': logging.WARNING,
|
35 |
+
'info': logging.INFO,
|
36 |
+
'debug': logging.DEBUG
|
37 |
+
}
|
38 |
+
|
39 |
+
execution_provider_set : ExecutionProviderSet =\
|
40 |
+
{
|
41 |
+
'cpu': 'CPUExecutionProvider',
|
42 |
+
'coreml': 'CoreMLExecutionProvider',
|
43 |
+
'cuda': 'CUDAExecutionProvider',
|
44 |
+
'directml': 'DmlExecutionProvider',
|
45 |
+
'openvino': 'OpenVINOExecutionProvider',
|
46 |
+
'rocm': 'ROCMExecutionProvider',
|
47 |
+
'tensorrt': 'TensorrtExecutionProvider'
|
48 |
+
}
|
49 |
+
|
50 |
+
ui_workflows : List[UiWorkflow] = [ 'instant_runner', 'job_runner', 'job_manager' ]
|
51 |
+
job_statuses : List[JobStatus] = [ 'drafted', 'queued', 'completed', 'failed' ]
|
52 |
+
|
53 |
+
execution_thread_count_range : Sequence[int] = create_int_range(1, 32, 1)
|
54 |
+
execution_queue_count_range : Sequence[int] = create_int_range(1, 4, 1)
|
55 |
+
system_memory_limit_range : Sequence[int] = create_int_range(0, 128, 4)
|
56 |
+
face_detector_angles : Sequence[Angle] = create_int_range(0, 270, 90)
|
57 |
+
face_detector_score_range : Sequence[Score] = create_float_range(0.0, 1.0, 0.05)
|
58 |
+
face_landmarker_score_range : Sequence[Score] = create_float_range(0.0, 1.0, 0.05)
|
59 |
+
face_mask_blur_range : Sequence[float] = create_float_range(0.0, 1.0, 0.05)
|
60 |
+
face_mask_padding_range : Sequence[int] = create_int_range(0, 100, 1)
|
61 |
+
face_selector_age_range : Sequence[int] = create_int_range(0, 100, 1)
|
62 |
+
reference_face_distance_range : Sequence[float] = create_float_range(0.0, 1.5, 0.05)
|
63 |
+
output_image_quality_range : Sequence[int] = create_int_range(0, 100, 1)
|
64 |
+
output_video_quality_range : Sequence[int] = create_int_range(0, 100, 1)
|
facefusion/common_helper.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import platform
|
2 |
+
from typing import Any, Optional, Sequence
|
3 |
+
|
4 |
+
|
5 |
+
def is_linux() -> bool:
|
6 |
+
return platform.system().lower() == 'linux'
|
7 |
+
|
8 |
+
|
9 |
+
def is_macos() -> bool:
|
10 |
+
return platform.system().lower() == 'darwin'
|
11 |
+
|
12 |
+
|
13 |
+
def is_windows() -> bool:
|
14 |
+
return platform.system().lower() == 'windows'
|
15 |
+
|
16 |
+
|
17 |
+
def create_int_metavar(int_range : Sequence[int]) -> str:
|
18 |
+
return '[' + str(int_range[0]) + '..' + str(int_range[-1]) + ':' + str(calc_int_step(int_range)) + ']'
|
19 |
+
|
20 |
+
|
21 |
+
def create_float_metavar(float_range : Sequence[float]) -> str:
|
22 |
+
return '[' + str(float_range[0]) + '..' + str(float_range[-1]) + ':' + str(calc_float_step(float_range)) + ']'
|
23 |
+
|
24 |
+
|
25 |
+
def create_int_range(start : int, end : int, step : int) -> Sequence[int]:
|
26 |
+
int_range = []
|
27 |
+
current = start
|
28 |
+
|
29 |
+
while current <= end:
|
30 |
+
int_range.append(current)
|
31 |
+
current += step
|
32 |
+
return int_range
|
33 |
+
|
34 |
+
|
35 |
+
def create_float_range(start : float, end : float, step : float) -> Sequence[float]:
|
36 |
+
float_range = []
|
37 |
+
current = start
|
38 |
+
|
39 |
+
while current <= end:
|
40 |
+
float_range.append(round(current, 2))
|
41 |
+
current = round(current + step, 2)
|
42 |
+
return float_range
|
43 |
+
|
44 |
+
|
45 |
+
def calc_int_step(int_range : Sequence[int]) -> int:
|
46 |
+
return int_range[1] - int_range[0]
|
47 |
+
|
48 |
+
|
49 |
+
def calc_float_step(float_range : Sequence[float]) -> float:
|
50 |
+
return round(float_range[1] - float_range[0], 2)
|
51 |
+
|
52 |
+
|
53 |
+
def cast_int(value : Any) -> Optional[Any]:
|
54 |
+
try:
|
55 |
+
return int(value)
|
56 |
+
except (ValueError, TypeError):
|
57 |
+
return None
|
58 |
+
|
59 |
+
|
60 |
+
def cast_float(value : Any) -> Optional[Any]:
|
61 |
+
try:
|
62 |
+
return float(value)
|
63 |
+
except (ValueError, TypeError):
|
64 |
+
return None
|
65 |
+
|
66 |
+
|
67 |
+
def get_first(__list__ : Any) -> Any:
|
68 |
+
return next(iter(__list__), None)
|
69 |
+
|
70 |
+
|
71 |
+
def get_last(__list__ : Any) -> Any:
|
72 |
+
return next(reversed(__list__), None)
|
facefusion/config.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from configparser import ConfigParser
|
2 |
+
from typing import Any, List, Optional
|
3 |
+
|
4 |
+
from facefusion import state_manager
|
5 |
+
from facefusion.common_helper import cast_float, cast_int
|
6 |
+
|
7 |
+
CONFIG = None
|
8 |
+
|
9 |
+
|
10 |
+
def get_config() -> ConfigParser:
|
11 |
+
global CONFIG
|
12 |
+
|
13 |
+
if CONFIG is None:
|
14 |
+
CONFIG = ConfigParser()
|
15 |
+
CONFIG.read(state_manager.get_item('config_path'), encoding = 'utf-8')
|
16 |
+
return CONFIG
|
17 |
+
|
18 |
+
|
19 |
+
def clear_config() -> None:
|
20 |
+
global CONFIG
|
21 |
+
|
22 |
+
CONFIG = None
|
23 |
+
|
24 |
+
|
25 |
+
def get_str_value(key : str, fallback : Optional[str] = None) -> Optional[str]:
|
26 |
+
value = get_value_by_notation(key)
|
27 |
+
|
28 |
+
if value or fallback:
|
29 |
+
return str(value or fallback)
|
30 |
+
return None
|
31 |
+
|
32 |
+
|
33 |
+
def get_int_value(key : str, fallback : Optional[str] = None) -> Optional[int]:
|
34 |
+
value = get_value_by_notation(key)
|
35 |
+
|
36 |
+
if value or fallback:
|
37 |
+
return cast_int(value or fallback)
|
38 |
+
return None
|
39 |
+
|
40 |
+
|
41 |
+
def get_float_value(key : str, fallback : Optional[str] = None) -> Optional[float]:
|
42 |
+
value = get_value_by_notation(key)
|
43 |
+
|
44 |
+
if value or fallback:
|
45 |
+
return cast_float(value or fallback)
|
46 |
+
return None
|
47 |
+
|
48 |
+
|
49 |
+
def get_bool_value(key : str, fallback : Optional[str] = None) -> Optional[bool]:
|
50 |
+
value = get_value_by_notation(key)
|
51 |
+
|
52 |
+
if value == 'True' or fallback == 'True':
|
53 |
+
return True
|
54 |
+
if value == 'False' or fallback == 'False':
|
55 |
+
return False
|
56 |
+
return None
|
57 |
+
|
58 |
+
|
59 |
+
def get_str_list(key : str, fallback : Optional[str] = None) -> Optional[List[str]]:
|
60 |
+
value = get_value_by_notation(key)
|
61 |
+
|
62 |
+
if value or fallback:
|
63 |
+
return [ str(value) for value in (value or fallback).split(' ') ]
|
64 |
+
return None
|
65 |
+
|
66 |
+
|
67 |
+
def get_int_list(key : str, fallback : Optional[str] = None) -> Optional[List[int]]:
|
68 |
+
value = get_value_by_notation(key)
|
69 |
+
|
70 |
+
if value or fallback:
|
71 |
+
return [ cast_int(value) for value in (value or fallback).split(' ') ]
|
72 |
+
return None
|
73 |
+
|
74 |
+
|
75 |
+
def get_float_list(key : str, fallback : Optional[str] = None) -> Optional[List[float]]:
|
76 |
+
value = get_value_by_notation(key)
|
77 |
+
|
78 |
+
if value or fallback:
|
79 |
+
return [ cast_float(value) for value in (value or fallback).split(' ') ]
|
80 |
+
return None
|
81 |
+
|
82 |
+
|
83 |
+
def get_value_by_notation(key : str) -> Optional[Any]:
|
84 |
+
config = get_config()
|
85 |
+
|
86 |
+
if '.' in key:
|
87 |
+
section, name = key.split('.')
|
88 |
+
if section in config and name in config[section]:
|
89 |
+
return config[section][name]
|
90 |
+
if key in config:
|
91 |
+
return config[key]
|
92 |
+
return None
|
facefusion/content_analyser.py
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
|
3 |
+
import cv2
|
4 |
+
import numpy
|
5 |
+
from tqdm import tqdm
|
6 |
+
|
7 |
+
from facefusion import inference_manager, state_manager, wording
|
8 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
9 |
+
from facefusion.filesystem import resolve_relative_path
|
10 |
+
from facefusion.thread_helper import conditional_thread_semaphore
|
11 |
+
from facefusion.typing import Fps, InferencePool, ModelOptions, ModelSet, VisionFrame
|
12 |
+
from facefusion.vision import count_video_frame_total, detect_video_fps, get_video_frame, read_image
|
13 |
+
|
14 |
+
MODEL_SET : ModelSet =\
|
15 |
+
{
|
16 |
+
'open_nsfw':
|
17 |
+
{
|
18 |
+
'hashes':
|
19 |
+
{
|
20 |
+
'content_analyser':
|
21 |
+
{
|
22 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/open_nsfw.hash',
|
23 |
+
'path': resolve_relative_path('../.assets/models/open_nsfw.hash')
|
24 |
+
}
|
25 |
+
},
|
26 |
+
'sources':
|
27 |
+
{
|
28 |
+
'content_analyser':
|
29 |
+
{
|
30 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/open_nsfw.onnx',
|
31 |
+
'path': resolve_relative_path('../.assets/models/open_nsfw.onnx')
|
32 |
+
}
|
33 |
+
},
|
34 |
+
'size': (224, 224),
|
35 |
+
'mean': [ 104, 117, 123 ]
|
36 |
+
}
|
37 |
+
}
|
38 |
+
PROBABILITY_LIMIT = 0.80
|
39 |
+
RATE_LIMIT = 10
|
40 |
+
STREAM_COUNTER = 0
|
41 |
+
|
42 |
+
|
43 |
+
def get_inference_pool() -> InferencePool:
|
44 |
+
model_sources = get_model_options().get('sources')
|
45 |
+
return inference_manager.get_inference_pool(__name__, model_sources)
|
46 |
+
|
47 |
+
|
48 |
+
def clear_inference_pool() -> None:
|
49 |
+
inference_manager.clear_inference_pool(__name__)
|
50 |
+
|
51 |
+
|
52 |
+
def get_model_options() -> ModelOptions:
|
53 |
+
return MODEL_SET.get('open_nsfw')
|
54 |
+
|
55 |
+
|
56 |
+
def pre_check() -> bool:
|
57 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
58 |
+
model_hashes = get_model_options().get('hashes')
|
59 |
+
model_sources = get_model_options().get('sources')
|
60 |
+
|
61 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
62 |
+
|
63 |
+
|
64 |
+
def analyse_stream(vision_frame : VisionFrame, video_fps : Fps) -> bool:
|
65 |
+
global STREAM_COUNTER
|
66 |
+
|
67 |
+
STREAM_COUNTER = STREAM_COUNTER + 1
|
68 |
+
if STREAM_COUNTER % int(video_fps) == 0:
|
69 |
+
return analyse_frame(vision_frame)
|
70 |
+
return False
|
71 |
+
|
72 |
+
|
73 |
+
def analyse_frame(vision_frame : VisionFrame) -> bool:
|
74 |
+
vision_frame = prepare_frame(vision_frame)
|
75 |
+
probability = forward(vision_frame)
|
76 |
+
|
77 |
+
return probability > PROBABILITY_LIMIT
|
78 |
+
|
79 |
+
|
80 |
+
def forward(vision_frame : VisionFrame) -> float:
|
81 |
+
content_analyser = get_inference_pool().get('content_analyser')
|
82 |
+
|
83 |
+
with conditional_thread_semaphore():
|
84 |
+
probability = content_analyser.run(None,
|
85 |
+
{
|
86 |
+
'input': vision_frame
|
87 |
+
})[0][0][1]
|
88 |
+
|
89 |
+
return probability
|
90 |
+
|
91 |
+
|
92 |
+
def prepare_frame(vision_frame : VisionFrame) -> VisionFrame:
|
93 |
+
model_size = get_model_options().get('size')
|
94 |
+
model_mean = get_model_options().get('mean')
|
95 |
+
vision_frame = cv2.resize(vision_frame, model_size).astype(numpy.float32)
|
96 |
+
vision_frame -= numpy.array(model_mean).astype(numpy.float32)
|
97 |
+
vision_frame = numpy.expand_dims(vision_frame, axis = 0)
|
98 |
+
return vision_frame
|
99 |
+
|
100 |
+
|
101 |
+
@lru_cache(maxsize = None)
|
102 |
+
def analyse_image(image_path : str) -> bool:
|
103 |
+
frame = read_image(image_path)
|
104 |
+
return analyse_frame(frame)
|
105 |
+
|
106 |
+
|
107 |
+
@lru_cache(maxsize = None)
|
108 |
+
def analyse_video(video_path : str, start_frame : int, end_frame : int) -> bool:
|
109 |
+
video_frame_total = count_video_frame_total(video_path)
|
110 |
+
video_fps = detect_video_fps(video_path)
|
111 |
+
frame_range = range(start_frame or 0, end_frame or video_frame_total)
|
112 |
+
rate = 0.0
|
113 |
+
counter = 0
|
114 |
+
|
115 |
+
with tqdm(total = len(frame_range), desc = wording.get('analysing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress:
|
116 |
+
for frame_number in frame_range:
|
117 |
+
if frame_number % int(video_fps) == 0:
|
118 |
+
frame = get_video_frame(video_path, frame_number)
|
119 |
+
if analyse_frame(frame):
|
120 |
+
counter += 1
|
121 |
+
rate = counter * int(video_fps) / len(frame_range) * 100
|
122 |
+
progress.update()
|
123 |
+
progress.set_postfix(rate = rate)
|
124 |
+
return rate > RATE_LIMIT
|
facefusion/core.py
ADDED
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import shutil
|
2 |
+
import signal
|
3 |
+
import sys
|
4 |
+
from time import time
|
5 |
+
|
6 |
+
import numpy
|
7 |
+
|
8 |
+
from facefusion import content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, logger, process_manager, state_manager, voice_extractor, wording
|
9 |
+
from facefusion.args import apply_args, collect_job_args, reduce_step_args
|
10 |
+
from facefusion.common_helper import get_first
|
11 |
+
from facefusion.content_analyser import analyse_image, analyse_video
|
12 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
13 |
+
from facefusion.exit_helper import conditional_exit, graceful_exit, hard_exit
|
14 |
+
from facefusion.face_analyser import get_average_face, get_many_faces, get_one_face
|
15 |
+
from facefusion.face_selector import sort_and_filter_faces
|
16 |
+
from facefusion.face_store import append_reference_face, clear_reference_faces, get_reference_faces
|
17 |
+
from facefusion.ffmpeg import copy_image, extract_frames, finalize_image, merge_video, replace_audio, restore_audio
|
18 |
+
from facefusion.filesystem import filter_audio_paths, is_image, is_video, list_directory, resolve_relative_path
|
19 |
+
from facefusion.jobs import job_helper, job_manager, job_runner
|
20 |
+
from facefusion.jobs.job_list import compose_job_list
|
21 |
+
from facefusion.memory import limit_system_memory
|
22 |
+
from facefusion.processors.core import get_processors_modules
|
23 |
+
from facefusion.program import create_program
|
24 |
+
from facefusion.program_helper import validate_args
|
25 |
+
from facefusion.statistics import conditional_log_statistics
|
26 |
+
from facefusion.temp_helper import clear_temp_directory, create_temp_directory, get_temp_file_path, get_temp_frame_paths, move_temp_file
|
27 |
+
from facefusion.typing import Args, ErrorCode
|
28 |
+
from facefusion.vision import get_video_frame, pack_resolution, read_image, read_static_images, restrict_image_resolution, restrict_video_fps, restrict_video_resolution, unpack_resolution
|
29 |
+
|
30 |
+
|
31 |
+
def cli() -> None:
|
32 |
+
signal.signal(signal.SIGINT, lambda signal_number, frame: graceful_exit(0))
|
33 |
+
program = create_program()
|
34 |
+
|
35 |
+
if validate_args(program):
|
36 |
+
args = vars(program.parse_args())
|
37 |
+
apply_args(args, state_manager.init_item)
|
38 |
+
|
39 |
+
if state_manager.get_item('command'):
|
40 |
+
logger.init(state_manager.get_item('log_level'))
|
41 |
+
route(args)
|
42 |
+
else:
|
43 |
+
program.print_help()
|
44 |
+
|
45 |
+
|
46 |
+
def route(args : Args) -> None:
|
47 |
+
system_memory_limit = state_manager.get_item('system_memory_limit')
|
48 |
+
if system_memory_limit and system_memory_limit > 0:
|
49 |
+
limit_system_memory(system_memory_limit)
|
50 |
+
if state_manager.get_item('command') == 'force-download':
|
51 |
+
error_code = force_download()
|
52 |
+
return conditional_exit(error_code)
|
53 |
+
if state_manager.get_item('command') in [ 'job-list', 'job-create', 'job-submit', 'job-submit-all', 'job-delete', 'job-delete-all', 'job-add-step', 'job-remix-step', 'job-insert-step', 'job-remove-step' ]:
|
54 |
+
if not job_manager.init_jobs(state_manager.get_item('jobs_path')):
|
55 |
+
hard_exit(1)
|
56 |
+
error_code = route_job_manager(args)
|
57 |
+
hard_exit(error_code)
|
58 |
+
if not pre_check():
|
59 |
+
return conditional_exit(2)
|
60 |
+
if state_manager.get_item('command') == 'run':
|
61 |
+
import facefusion.uis.core as ui
|
62 |
+
|
63 |
+
if not common_pre_check() or not processors_pre_check():
|
64 |
+
return conditional_exit(2)
|
65 |
+
for ui_layout in ui.get_ui_layouts_modules(state_manager.get_item('ui_layouts')):
|
66 |
+
if not ui_layout.pre_check():
|
67 |
+
return conditional_exit(2)
|
68 |
+
ui.launch()
|
69 |
+
if state_manager.get_item('command') == 'headless-run':
|
70 |
+
if not job_manager.init_jobs(state_manager.get_item('jobs_path')):
|
71 |
+
hard_exit(1)
|
72 |
+
error_core = process_headless(args)
|
73 |
+
hard_exit(error_core)
|
74 |
+
if state_manager.get_item('command') in [ 'job-run', 'job-run-all', 'job-retry', 'job-retry-all' ]:
|
75 |
+
if not job_manager.init_jobs(state_manager.get_item('jobs_path')):
|
76 |
+
hard_exit(1)
|
77 |
+
error_code = route_job_runner()
|
78 |
+
hard_exit(error_code)
|
79 |
+
|
80 |
+
|
81 |
+
def pre_check() -> bool:
|
82 |
+
if sys.version_info < (3, 9):
|
83 |
+
logger.error(wording.get('python_not_supported').format(version = '3.9'), __name__)
|
84 |
+
return False
|
85 |
+
if not shutil.which('curl'):
|
86 |
+
logger.error(wording.get('curl_not_installed'), __name__)
|
87 |
+
return False
|
88 |
+
if not shutil.which('ffmpeg'):
|
89 |
+
logger.error(wording.get('ffmpeg_not_installed'), __name__)
|
90 |
+
return False
|
91 |
+
return True
|
92 |
+
|
93 |
+
|
94 |
+
def common_pre_check() -> bool:
|
95 |
+
modules =\
|
96 |
+
[
|
97 |
+
content_analyser,
|
98 |
+
face_classifier,
|
99 |
+
face_detector,
|
100 |
+
face_landmarker,
|
101 |
+
face_masker,
|
102 |
+
face_recognizer,
|
103 |
+
voice_extractor
|
104 |
+
]
|
105 |
+
|
106 |
+
return all(module.pre_check() for module in modules)
|
107 |
+
|
108 |
+
|
109 |
+
def processors_pre_check() -> bool:
|
110 |
+
for processor_module in get_processors_modules(state_manager.get_item('processors')):
|
111 |
+
if not processor_module.pre_check():
|
112 |
+
return False
|
113 |
+
return True
|
114 |
+
|
115 |
+
|
116 |
+
def conditional_process() -> ErrorCode:
|
117 |
+
start_time = time()
|
118 |
+
for processor_module in get_processors_modules(state_manager.get_item('processors')):
|
119 |
+
if not processor_module.pre_process('output'):
|
120 |
+
return 2
|
121 |
+
conditional_append_reference_faces()
|
122 |
+
if is_image(state_manager.get_item('target_path')):
|
123 |
+
return process_image(start_time)
|
124 |
+
if is_video(state_manager.get_item('target_path')):
|
125 |
+
return process_video(start_time)
|
126 |
+
return 0
|
127 |
+
|
128 |
+
|
129 |
+
def conditional_append_reference_faces() -> None:
|
130 |
+
if 'reference' in state_manager.get_item('face_selector_mode') and not get_reference_faces():
|
131 |
+
source_frames = read_static_images(state_manager.get_item('source_paths'))
|
132 |
+
source_faces = get_many_faces(source_frames)
|
133 |
+
source_face = get_average_face(source_faces)
|
134 |
+
if is_video(state_manager.get_item('target_path')):
|
135 |
+
reference_frame = get_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number'))
|
136 |
+
else:
|
137 |
+
reference_frame = read_image(state_manager.get_item('target_path'))
|
138 |
+
reference_faces = sort_and_filter_faces(get_many_faces([ reference_frame ]))
|
139 |
+
reference_face = get_one_face(reference_faces, state_manager.get_item('reference_face_position'))
|
140 |
+
append_reference_face('origin', reference_face)
|
141 |
+
|
142 |
+
if source_face and reference_face:
|
143 |
+
for processor_module in get_processors_modules(state_manager.get_item('processors')):
|
144 |
+
abstract_reference_frame = processor_module.get_reference_frame(source_face, reference_face, reference_frame)
|
145 |
+
if numpy.any(abstract_reference_frame):
|
146 |
+
abstract_reference_faces = sort_and_filter_faces(get_many_faces([ abstract_reference_frame ]))
|
147 |
+
abstract_reference_face = get_one_face(abstract_reference_faces, state_manager.get_item('reference_face_position'))
|
148 |
+
append_reference_face(processor_module.__name__, abstract_reference_face)
|
149 |
+
|
150 |
+
|
151 |
+
def force_download() -> ErrorCode:
|
152 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
153 |
+
available_processors = list_directory('facefusion/processors/modules')
|
154 |
+
common_modules =\
|
155 |
+
[
|
156 |
+
content_analyser,
|
157 |
+
face_classifier,
|
158 |
+
face_detector,
|
159 |
+
face_landmarker,
|
160 |
+
face_recognizer,
|
161 |
+
face_masker,
|
162 |
+
voice_extractor
|
163 |
+
]
|
164 |
+
processor_modules = get_processors_modules(available_processors)
|
165 |
+
|
166 |
+
for module in common_modules + processor_modules:
|
167 |
+
if hasattr(module, 'MODEL_SET'):
|
168 |
+
for model in module.MODEL_SET.values():
|
169 |
+
model_hashes = model.get('hashes')
|
170 |
+
model_sources = model.get('sources')
|
171 |
+
|
172 |
+
if model_hashes and model_sources:
|
173 |
+
if not conditional_download_hashes(download_directory_path, model_hashes) or not conditional_download_sources(download_directory_path, model_sources):
|
174 |
+
return 1
|
175 |
+
|
176 |
+
return 0
|
177 |
+
|
178 |
+
|
179 |
+
def route_job_manager(args : Args) -> ErrorCode:
|
180 |
+
if state_manager.get_item('command') == 'job-list':
|
181 |
+
job_headers, job_contents = compose_job_list(state_manager.get_item('job_status'))
|
182 |
+
|
183 |
+
if job_contents:
|
184 |
+
logger.table(job_headers, job_contents)
|
185 |
+
return 0
|
186 |
+
return 1
|
187 |
+
if state_manager.get_item('command') == 'job-create':
|
188 |
+
if job_manager.create_job(state_manager.get_item('job_id')):
|
189 |
+
logger.info(wording.get('job_created').format(job_id = state_manager.get_item('job_id')), __name__)
|
190 |
+
return 0
|
191 |
+
logger.error(wording.get('job_not_created').format(job_id = state_manager.get_item('job_id')), __name__)
|
192 |
+
return 1
|
193 |
+
if state_manager.get_item('command') == 'job-submit':
|
194 |
+
if job_manager.submit_job(state_manager.get_item('job_id')):
|
195 |
+
logger.info(wording.get('job_submitted').format(job_id = state_manager.get_item('job_id')), __name__)
|
196 |
+
return 0
|
197 |
+
logger.error(wording.get('job_not_submitted').format(job_id = state_manager.get_item('job_id')), __name__)
|
198 |
+
return 1
|
199 |
+
if state_manager.get_item('command') == 'job-submit-all':
|
200 |
+
if job_manager.submit_jobs():
|
201 |
+
logger.info(wording.get('job_all_submitted'), __name__)
|
202 |
+
return 0
|
203 |
+
logger.error(wording.get('job_all_not_submitted'), __name__)
|
204 |
+
return 1
|
205 |
+
if state_manager.get_item('command') == 'job-delete':
|
206 |
+
if job_manager.delete_job(state_manager.get_item('job_id')):
|
207 |
+
logger.info(wording.get('job_deleted').format(job_id = state_manager.get_item('job_id')), __name__)
|
208 |
+
return 0
|
209 |
+
logger.error(wording.get('job_not_deleted').format(job_id = state_manager.get_item('job_id')), __name__)
|
210 |
+
return 1
|
211 |
+
if state_manager.get_item('command') == 'job-delete-all':
|
212 |
+
if job_manager.delete_jobs():
|
213 |
+
logger.info(wording.get('job_all_deleted'), __name__)
|
214 |
+
return 0
|
215 |
+
logger.error(wording.get('job_all_not_deleted'), __name__)
|
216 |
+
return 1
|
217 |
+
if state_manager.get_item('command') == 'job-add-step':
|
218 |
+
step_args = reduce_step_args(args)
|
219 |
+
|
220 |
+
if job_manager.add_step(state_manager.get_item('job_id'), step_args):
|
221 |
+
logger.info(wording.get('job_step_added').format(job_id = state_manager.get_item('job_id')), __name__)
|
222 |
+
return 0
|
223 |
+
logger.error(wording.get('job_step_not_added').format(job_id = state_manager.get_item('job_id')), __name__)
|
224 |
+
return 1
|
225 |
+
if state_manager.get_item('command') == 'job-remix-step':
|
226 |
+
step_args = reduce_step_args(args)
|
227 |
+
|
228 |
+
if job_manager.remix_step(state_manager.get_item('job_id'), state_manager.get_item('step_index'), step_args):
|
229 |
+
logger.info(wording.get('job_remix_step_added').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
230 |
+
return 0
|
231 |
+
logger.error(wording.get('job_remix_step_not_added').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
232 |
+
return 1
|
233 |
+
if state_manager.get_item('command') == 'job-insert-step':
|
234 |
+
step_args = reduce_step_args(args)
|
235 |
+
|
236 |
+
if job_manager.insert_step(state_manager.get_item('job_id'), state_manager.get_item('step_index'), step_args):
|
237 |
+
logger.info(wording.get('job_step_inserted').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
238 |
+
return 0
|
239 |
+
logger.error(wording.get('job_step_not_inserted').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
240 |
+
return 1
|
241 |
+
if state_manager.get_item('command') == 'job-remove-step':
|
242 |
+
if job_manager.remove_step(state_manager.get_item('job_id'), state_manager.get_item('step_index')):
|
243 |
+
logger.info(wording.get('job_step_removed').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
244 |
+
return 0
|
245 |
+
logger.error(wording.get('job_step_not_removed').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__)
|
246 |
+
return 1
|
247 |
+
return 1
|
248 |
+
|
249 |
+
|
250 |
+
def route_job_runner() -> ErrorCode:
|
251 |
+
if state_manager.get_item('command') == 'job-run':
|
252 |
+
logger.info(wording.get('running_job').format(job_id = state_manager.get_item('job_id')), __name__)
|
253 |
+
if job_runner.run_job(state_manager.get_item('job_id'), process_step):
|
254 |
+
logger.info(wording.get('processing_job_succeed').format(job_id = state_manager.get_item('job_id')), __name__)
|
255 |
+
return 0
|
256 |
+
logger.info(wording.get('processing_job_failed').format(job_id = state_manager.get_item('job_id')), __name__)
|
257 |
+
return 1
|
258 |
+
if state_manager.get_item('command') == 'job-run-all':
|
259 |
+
logger.info(wording.get('running_jobs'), __name__)
|
260 |
+
if job_runner.run_jobs(process_step):
|
261 |
+
logger.info(wording.get('processing_jobs_succeed'), __name__)
|
262 |
+
return 0
|
263 |
+
logger.info(wording.get('processing_jobs_failed'), __name__)
|
264 |
+
return 1
|
265 |
+
if state_manager.get_item('command') == 'job-retry':
|
266 |
+
logger.info(wording.get('retrying_job').format(job_id = state_manager.get_item('job_id')), __name__)
|
267 |
+
if job_runner.retry_job(state_manager.get_item('job_id'), process_step):
|
268 |
+
logger.info(wording.get('processing_job_succeed').format(job_id = state_manager.get_item('job_id')), __name__)
|
269 |
+
return 0
|
270 |
+
logger.info(wording.get('processing_job_failed').format(job_id = state_manager.get_item('job_id')), __name__)
|
271 |
+
return 1
|
272 |
+
if state_manager.get_item('command') == 'job-retry-all':
|
273 |
+
logger.info(wording.get('retrying_jobs'), __name__)
|
274 |
+
if job_runner.retry_jobs(process_step):
|
275 |
+
logger.info(wording.get('processing_jobs_succeed'), __name__)
|
276 |
+
return 0
|
277 |
+
logger.info(wording.get('processing_jobs_failed'), __name__)
|
278 |
+
return 1
|
279 |
+
return 2
|
280 |
+
|
281 |
+
|
282 |
+
def process_step(job_id : str, step_index : int, step_args : Args) -> bool:
|
283 |
+
clear_reference_faces()
|
284 |
+
step_total = job_manager.count_step_total(job_id)
|
285 |
+
step_args.update(collect_job_args())
|
286 |
+
apply_args(step_args, state_manager.set_item)
|
287 |
+
|
288 |
+
logger.info(wording.get('processing_step').format(step_current = step_index + 1, step_total = step_total), __name__)
|
289 |
+
if common_pre_check() and processors_pre_check():
|
290 |
+
error_code = conditional_process()
|
291 |
+
return error_code == 0
|
292 |
+
return False
|
293 |
+
|
294 |
+
|
295 |
+
def process_headless(args : Args) -> ErrorCode:
|
296 |
+
job_id = job_helper.suggest_job_id('headless')
|
297 |
+
step_args = reduce_step_args(args)
|
298 |
+
|
299 |
+
if job_manager.create_job(job_id) and job_manager.add_step(job_id, step_args) and job_manager.submit_job(job_id) and job_runner.run_job(job_id, process_step):
|
300 |
+
return 0
|
301 |
+
return 1
|
302 |
+
|
303 |
+
|
304 |
+
def process_image(start_time : float) -> ErrorCode:
|
305 |
+
if analyse_image(state_manager.get_item('target_path')):
|
306 |
+
return 3
|
307 |
+
# clear temp
|
308 |
+
logger.debug(wording.get('clearing_temp'), __name__)
|
309 |
+
clear_temp_directory(state_manager.get_item('target_path'))
|
310 |
+
# create temp
|
311 |
+
logger.debug(wording.get('creating_temp'), __name__)
|
312 |
+
create_temp_directory(state_manager.get_item('target_path'))
|
313 |
+
# copy image
|
314 |
+
process_manager.start()
|
315 |
+
temp_image_resolution = pack_resolution(restrict_image_resolution(state_manager.get_item('target_path'), unpack_resolution(state_manager.get_item('output_image_resolution'))))
|
316 |
+
logger.info(wording.get('copying_image').format(resolution = temp_image_resolution), __name__)
|
317 |
+
if copy_image(state_manager.get_item('target_path'), temp_image_resolution):
|
318 |
+
logger.debug(wording.get('copying_image_succeed'), __name__)
|
319 |
+
else:
|
320 |
+
logger.error(wording.get('copying_image_failed'), __name__)
|
321 |
+
process_manager.end()
|
322 |
+
return 1
|
323 |
+
# process image
|
324 |
+
temp_file_path = get_temp_file_path(state_manager.get_item('target_path'))
|
325 |
+
for processor_module in get_processors_modules(state_manager.get_item('processors')):
|
326 |
+
logger.info(wording.get('processing'), processor_module.__name__)
|
327 |
+
processor_module.process_image(state_manager.get_item('source_paths'), temp_file_path, temp_file_path)
|
328 |
+
processor_module.post_process()
|
329 |
+
if is_process_stopping():
|
330 |
+
process_manager.end()
|
331 |
+
return 4
|
332 |
+
# finalize image
|
333 |
+
logger.info(wording.get('finalizing_image').format(resolution = state_manager.get_item('output_image_resolution')), __name__)
|
334 |
+
if finalize_image(state_manager.get_item('target_path'), state_manager.get_item('output_path'), state_manager.get_item('output_image_resolution')):
|
335 |
+
logger.debug(wording.get('finalizing_image_succeed'), __name__)
|
336 |
+
else:
|
337 |
+
logger.warn(wording.get('finalizing_image_skipped'), __name__)
|
338 |
+
# clear temp
|
339 |
+
logger.debug(wording.get('clearing_temp'), __name__)
|
340 |
+
clear_temp_directory(state_manager.get_item('target_path'))
|
341 |
+
# validate image
|
342 |
+
if is_image(state_manager.get_item('output_path')):
|
343 |
+
seconds = '{:.2f}'.format((time() - start_time) % 60)
|
344 |
+
logger.info(wording.get('processing_image_succeed').format(seconds = seconds), __name__)
|
345 |
+
conditional_log_statistics()
|
346 |
+
else:
|
347 |
+
logger.error(wording.get('processing_image_failed'), __name__)
|
348 |
+
process_manager.end()
|
349 |
+
return 1
|
350 |
+
process_manager.end()
|
351 |
+
return 0
|
352 |
+
|
353 |
+
|
354 |
+
def process_video(start_time : float) -> ErrorCode:
|
355 |
+
if analyse_video(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end')):
|
356 |
+
return 3
|
357 |
+
# clear temp
|
358 |
+
logger.debug(wording.get('clearing_temp'), __name__)
|
359 |
+
clear_temp_directory(state_manager.get_item('target_path'))
|
360 |
+
# create temp
|
361 |
+
logger.debug(wording.get('creating_temp'), __name__)
|
362 |
+
create_temp_directory(state_manager.get_item('target_path'))
|
363 |
+
# extract frames
|
364 |
+
process_manager.start()
|
365 |
+
temp_video_resolution = pack_resolution(restrict_video_resolution(state_manager.get_item('target_path'), unpack_resolution(state_manager.get_item('output_video_resolution'))))
|
366 |
+
temp_video_fps = restrict_video_fps(state_manager.get_item('target_path'), state_manager.get_item('output_video_fps'))
|
367 |
+
logger.info(wording.get('extracting_frames').format(resolution = temp_video_resolution, fps = temp_video_fps), __name__)
|
368 |
+
if extract_frames(state_manager.get_item('target_path'), temp_video_resolution, temp_video_fps):
|
369 |
+
logger.debug(wording.get('extracting_frames_succeed'), __name__)
|
370 |
+
else:
|
371 |
+
if is_process_stopping():
|
372 |
+
process_manager.end()
|
373 |
+
return 4
|
374 |
+
logger.error(wording.get('extracting_frames_failed'), __name__)
|
375 |
+
process_manager.end()
|
376 |
+
return 1
|
377 |
+
# process frames
|
378 |
+
temp_frame_paths = get_temp_frame_paths(state_manager.get_item('target_path'))
|
379 |
+
if temp_frame_paths:
|
380 |
+
for processor_module in get_processors_modules(state_manager.get_item('processors')):
|
381 |
+
logger.info(wording.get('processing'), processor_module.__name__)
|
382 |
+
processor_module.process_video(state_manager.get_item('source_paths'), temp_frame_paths)
|
383 |
+
processor_module.post_process()
|
384 |
+
if is_process_stopping():
|
385 |
+
return 4
|
386 |
+
else:
|
387 |
+
logger.error(wording.get('temp_frames_not_found'), __name__)
|
388 |
+
process_manager.end()
|
389 |
+
return 1
|
390 |
+
# merge video
|
391 |
+
logger.info(wording.get('merging_video').format(resolution = state_manager.get_item('output_video_resolution'), fps = state_manager.get_item('output_video_fps')), __name__)
|
392 |
+
if merge_video(state_manager.get_item('target_path'), state_manager.get_item('output_video_resolution'), state_manager.get_item('output_video_fps')):
|
393 |
+
logger.debug(wording.get('merging_video_succeed'), __name__)
|
394 |
+
else:
|
395 |
+
if is_process_stopping():
|
396 |
+
process_manager.end()
|
397 |
+
return 4
|
398 |
+
logger.error(wording.get('merging_video_failed'), __name__)
|
399 |
+
process_manager.end()
|
400 |
+
return 1
|
401 |
+
# handle audio
|
402 |
+
if state_manager.get_item('skip_audio'):
|
403 |
+
logger.info(wording.get('skipping_audio'), __name__)
|
404 |
+
move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path'))
|
405 |
+
else:
|
406 |
+
source_audio_path = get_first(filter_audio_paths(state_manager.get_item('source_paths')))
|
407 |
+
if source_audio_path:
|
408 |
+
if replace_audio(state_manager.get_item('target_path'), source_audio_path, state_manager.get_item('output_path')):
|
409 |
+
logger.debug(wording.get('replacing_audio_succeed'), __name__)
|
410 |
+
else:
|
411 |
+
if is_process_stopping():
|
412 |
+
process_manager.end()
|
413 |
+
return 4
|
414 |
+
logger.warn(wording.get('replacing_audio_skipped'), __name__)
|
415 |
+
move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path'))
|
416 |
+
else:
|
417 |
+
if restore_audio(state_manager.get_item('target_path'), state_manager.get_item('output_path'), state_manager.get_item('output_video_fps')):
|
418 |
+
logger.debug(wording.get('restoring_audio_succeed'), __name__)
|
419 |
+
else:
|
420 |
+
if is_process_stopping():
|
421 |
+
process_manager.end()
|
422 |
+
return 4
|
423 |
+
logger.warn(wording.get('restoring_audio_skipped'), __name__)
|
424 |
+
move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path'))
|
425 |
+
# clear temp
|
426 |
+
logger.debug(wording.get('clearing_temp'), __name__)
|
427 |
+
clear_temp_directory(state_manager.get_item('target_path'))
|
428 |
+
# validate video
|
429 |
+
if is_video(state_manager.get_item('output_path')):
|
430 |
+
seconds = '{:.2f}'.format((time() - start_time))
|
431 |
+
logger.info(wording.get('processing_video_succeed').format(seconds = seconds), __name__)
|
432 |
+
conditional_log_statistics()
|
433 |
+
else:
|
434 |
+
logger.error(wording.get('processing_video_failed'), __name__)
|
435 |
+
process_manager.end()
|
436 |
+
return 1
|
437 |
+
process_manager.end()
|
438 |
+
return 0
|
439 |
+
|
440 |
+
|
441 |
+
def is_process_stopping() -> bool:
|
442 |
+
if process_manager.is_stopping():
|
443 |
+
process_manager.end()
|
444 |
+
logger.info(wording.get('processing_stopped'), __name__)
|
445 |
+
return process_manager.is_pending()
|
facefusion/date_helper.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timedelta
|
2 |
+
from typing import Optional, Tuple
|
3 |
+
|
4 |
+
from facefusion import wording
|
5 |
+
|
6 |
+
|
7 |
+
def get_current_date_time() -> datetime:
|
8 |
+
return datetime.now().astimezone()
|
9 |
+
|
10 |
+
|
11 |
+
def split_time_delta(time_delta : timedelta) -> Tuple[int, int, int, int]:
|
12 |
+
days, hours = divmod(time_delta.total_seconds(), 86400)
|
13 |
+
hours, minutes = divmod(hours, 3600)
|
14 |
+
minutes, seconds = divmod(minutes, 60)
|
15 |
+
return int(days), int(hours), int(minutes), int(seconds)
|
16 |
+
|
17 |
+
|
18 |
+
def describe_time_ago(date_time : datetime) -> Optional[str]:
|
19 |
+
time_ago = datetime.now(date_time.tzinfo) - date_time
|
20 |
+
days, hours, minutes, _ = split_time_delta(time_ago)
|
21 |
+
|
22 |
+
if timedelta(days = 1) < time_ago:
|
23 |
+
return wording.get('time_ago_days').format(days = days, hours = hours, minutes = minutes)
|
24 |
+
if timedelta(hours = 1) < time_ago:
|
25 |
+
return wording.get('time_ago_hours').format(hours = hours, minutes = minutes)
|
26 |
+
if timedelta(minutes = 1) < time_ago:
|
27 |
+
return wording.get('time_ago_minutes').format(minutes = minutes)
|
28 |
+
return wording.get('time_ago_now')
|
facefusion/download.py
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
import ssl
|
4 |
+
import subprocess
|
5 |
+
import urllib.request
|
6 |
+
from functools import lru_cache
|
7 |
+
from typing import List, Tuple
|
8 |
+
from urllib.parse import urlparse
|
9 |
+
|
10 |
+
from tqdm import tqdm
|
11 |
+
|
12 |
+
from facefusion import logger, process_manager, state_manager, wording
|
13 |
+
from facefusion.common_helper import is_macos
|
14 |
+
from facefusion.filesystem import get_file_size, is_file, remove_file
|
15 |
+
from facefusion.hash_helper import validate_hash
|
16 |
+
from facefusion.typing import DownloadSet
|
17 |
+
|
18 |
+
if is_macos():
|
19 |
+
ssl._create_default_https_context = ssl._create_unverified_context
|
20 |
+
|
21 |
+
|
22 |
+
def conditional_download(download_directory_path : str, urls : List[str]) -> None:
|
23 |
+
for url in urls:
|
24 |
+
download_file_name = os.path.basename(urlparse(url).path)
|
25 |
+
download_file_path = os.path.join(download_directory_path, download_file_name)
|
26 |
+
initial_size = get_file_size(download_file_path)
|
27 |
+
download_size = get_download_size(url)
|
28 |
+
|
29 |
+
if initial_size < download_size:
|
30 |
+
with tqdm(total = download_size, initial = initial_size, desc = wording.get('downloading'), unit = 'B', unit_scale = True, unit_divisor = 1024, ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress:
|
31 |
+
subprocess.Popen([ shutil.which('curl'), '--create-dirs', '--silent', '--insecure', '--location', '--continue-at', '-', '--output', download_file_path, url ])
|
32 |
+
current_size = initial_size
|
33 |
+
|
34 |
+
progress.set_postfix(file = download_file_name)
|
35 |
+
while current_size < download_size:
|
36 |
+
if is_file(download_file_path):
|
37 |
+
current_size = get_file_size(download_file_path)
|
38 |
+
progress.update(current_size - progress.n)
|
39 |
+
|
40 |
+
|
41 |
+
@lru_cache(maxsize = None)
|
42 |
+
def get_download_size(url : str) -> int:
|
43 |
+
try:
|
44 |
+
response = urllib.request.urlopen(url, timeout = 10)
|
45 |
+
content_length = response.headers.get('Content-Length')
|
46 |
+
return int(content_length)
|
47 |
+
except (OSError, TypeError, ValueError):
|
48 |
+
return 0
|
49 |
+
|
50 |
+
|
51 |
+
def is_download_done(url : str, file_path : str) -> bool:
|
52 |
+
if is_file(file_path):
|
53 |
+
return get_download_size(url) == get_file_size(file_path)
|
54 |
+
return False
|
55 |
+
|
56 |
+
|
57 |
+
def conditional_download_hashes(download_directory_path : str, hashes : DownloadSet) -> bool:
|
58 |
+
hash_paths = [ hashes.get(hash_key).get('path') for hash_key in hashes.keys() ]
|
59 |
+
|
60 |
+
process_manager.check()
|
61 |
+
if not state_manager.get_item('skip_download'):
|
62 |
+
_, invalid_hash_paths = validate_hash_paths(hash_paths)
|
63 |
+
if invalid_hash_paths:
|
64 |
+
for index in hashes:
|
65 |
+
if hashes.get(index).get('path') in invalid_hash_paths:
|
66 |
+
invalid_hash_url = hashes.get(index).get('url')
|
67 |
+
conditional_download(download_directory_path, [ invalid_hash_url ])
|
68 |
+
|
69 |
+
valid_hash_paths, invalid_hash_paths = validate_hash_paths(hash_paths)
|
70 |
+
for valid_hash_path in valid_hash_paths:
|
71 |
+
valid_hash_file_name, _ = os.path.splitext(os.path.basename(valid_hash_path))
|
72 |
+
logger.debug(wording.get('validating_hash_succeed').format(hash_file_name = valid_hash_file_name), __name__)
|
73 |
+
for invalid_hash_path in invalid_hash_paths:
|
74 |
+
invalid_hash_file_name, _ = os.path.splitext(os.path.basename(invalid_hash_path))
|
75 |
+
logger.error(wording.get('validating_hash_failed').format(hash_file_name = invalid_hash_file_name), __name__)
|
76 |
+
|
77 |
+
if not invalid_hash_paths:
|
78 |
+
process_manager.end()
|
79 |
+
return not invalid_hash_paths
|
80 |
+
|
81 |
+
|
82 |
+
def conditional_download_sources(download_directory_path : str, sources : DownloadSet) -> bool:
|
83 |
+
source_paths = [ sources.get(source_key).get('path') for source_key in sources.keys() ]
|
84 |
+
|
85 |
+
process_manager.check()
|
86 |
+
if not state_manager.get_item('skip_download'):
|
87 |
+
_, invalid_source_paths = validate_source_paths(source_paths)
|
88 |
+
if invalid_source_paths:
|
89 |
+
for index in sources:
|
90 |
+
if sources.get(index).get('path') in invalid_source_paths:
|
91 |
+
invalid_source_url = sources.get(index).get('url')
|
92 |
+
conditional_download(download_directory_path, [ invalid_source_url ])
|
93 |
+
|
94 |
+
valid_source_paths, invalid_source_paths = validate_source_paths(source_paths)
|
95 |
+
for valid_source_path in valid_source_paths:
|
96 |
+
valid_source_file_name, _ = os.path.splitext(os.path.basename(valid_source_path))
|
97 |
+
logger.debug(wording.get('validating_source_succeed').format(source_file_name = valid_source_file_name), __name__)
|
98 |
+
for invalid_source_path in invalid_source_paths:
|
99 |
+
invalid_source_file_name, _ = os.path.splitext(os.path.basename(invalid_source_path))
|
100 |
+
logger.error(wording.get('validating_source_failed').format(source_file_name = invalid_source_file_name), __name__)
|
101 |
+
|
102 |
+
if remove_file(invalid_source_path):
|
103 |
+
logger.error(wording.get('deleting_corrupt_source').format(source_file_name = invalid_source_file_name), __name__)
|
104 |
+
|
105 |
+
if not invalid_source_paths:
|
106 |
+
process_manager.end()
|
107 |
+
return not invalid_source_paths
|
108 |
+
|
109 |
+
|
110 |
+
def validate_hash_paths(hash_paths : List[str]) -> Tuple[List[str], List[str]]:
|
111 |
+
valid_hash_paths = []
|
112 |
+
invalid_hash_paths = []
|
113 |
+
|
114 |
+
for hash_path in hash_paths:
|
115 |
+
if is_file(hash_path):
|
116 |
+
valid_hash_paths.append(hash_path)
|
117 |
+
else:
|
118 |
+
invalid_hash_paths.append(hash_path)
|
119 |
+
return valid_hash_paths, invalid_hash_paths
|
120 |
+
|
121 |
+
|
122 |
+
def validate_source_paths(source_paths : List[str]) -> Tuple[List[str], List[str]]:
|
123 |
+
valid_source_paths = []
|
124 |
+
invalid_source_paths = []
|
125 |
+
|
126 |
+
for source_path in source_paths:
|
127 |
+
if validate_hash(source_path):
|
128 |
+
valid_source_paths.append(source_path)
|
129 |
+
else:
|
130 |
+
invalid_source_paths.append(source_path)
|
131 |
+
return valid_source_paths, invalid_source_paths
|
facefusion/execution.py
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import xml.etree.ElementTree as ElementTree
|
3 |
+
from functools import lru_cache
|
4 |
+
from typing import Any, List
|
5 |
+
|
6 |
+
from onnxruntime import get_available_providers, set_default_logger_severity
|
7 |
+
|
8 |
+
from facefusion.choices import execution_provider_set
|
9 |
+
from facefusion.typing import ExecutionDevice, ExecutionProviderKey, ExecutionProviderSet, ValueAndUnit
|
10 |
+
|
11 |
+
set_default_logger_severity(3)
|
12 |
+
|
13 |
+
|
14 |
+
def get_execution_provider_choices() -> List[ExecutionProviderKey]:
|
15 |
+
return list(get_available_execution_provider_set().keys())
|
16 |
+
|
17 |
+
|
18 |
+
def has_execution_provider(execution_provider_key : ExecutionProviderKey) -> bool:
|
19 |
+
return execution_provider_key in get_execution_provider_choices()
|
20 |
+
|
21 |
+
|
22 |
+
def get_available_execution_provider_set() -> ExecutionProviderSet:
|
23 |
+
available_execution_providers = get_available_providers()
|
24 |
+
available_execution_provider_set : ExecutionProviderSet = {}
|
25 |
+
|
26 |
+
for execution_provider_key, execution_provider_value in execution_provider_set.items():
|
27 |
+
if execution_provider_value in available_execution_providers:
|
28 |
+
available_execution_provider_set[execution_provider_key] = execution_provider_value
|
29 |
+
return available_execution_provider_set
|
30 |
+
|
31 |
+
|
32 |
+
def create_execution_providers(execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> List[Any]:
|
33 |
+
execution_providers : List[Any] = []
|
34 |
+
|
35 |
+
for execution_provider_key in execution_provider_keys:
|
36 |
+
if execution_provider_key == 'cuda':
|
37 |
+
execution_providers.append((execution_provider_set.get(execution_provider_key),
|
38 |
+
{
|
39 |
+
'device_id': execution_device_id,
|
40 |
+
'cudnn_conv_algo_search': 'EXHAUSTIVE' if use_exhaustive() else 'DEFAULT'
|
41 |
+
}))
|
42 |
+
if execution_provider_key == 'tensorrt':
|
43 |
+
execution_providers.append((execution_provider_set.get(execution_provider_key),
|
44 |
+
{
|
45 |
+
'device_id': execution_device_id,
|
46 |
+
'trt_engine_cache_enable': True,
|
47 |
+
'trt_engine_cache_path': '.caches',
|
48 |
+
'trt_timing_cache_enable': True,
|
49 |
+
'trt_timing_cache_path': '.caches',
|
50 |
+
'trt_builder_optimization_level': 5
|
51 |
+
}))
|
52 |
+
if execution_provider_key == 'openvino':
|
53 |
+
execution_providers.append((execution_provider_set.get(execution_provider_key),
|
54 |
+
{
|
55 |
+
'device_type': 'GPU.' + execution_device_id,
|
56 |
+
'precision': 'FP32'
|
57 |
+
}))
|
58 |
+
if execution_provider_key in [ 'directml', 'rocm' ]:
|
59 |
+
execution_providers.append((execution_provider_set.get(execution_provider_key),
|
60 |
+
{
|
61 |
+
'device_id': execution_device_id
|
62 |
+
}))
|
63 |
+
if execution_provider_key == 'coreml':
|
64 |
+
execution_providers.append(execution_provider_set.get(execution_provider_key))
|
65 |
+
|
66 |
+
if 'cpu' in execution_provider_keys:
|
67 |
+
execution_providers.append(execution_provider_set.get('cpu'))
|
68 |
+
|
69 |
+
return execution_providers
|
70 |
+
|
71 |
+
|
72 |
+
def use_exhaustive() -> bool:
|
73 |
+
execution_devices = detect_static_execution_devices()
|
74 |
+
product_names = ('GeForce GTX 1630', 'GeForce GTX 1650', 'GeForce GTX 1660')
|
75 |
+
|
76 |
+
return any(execution_device.get('product').get('name').startswith(product_names) for execution_device in execution_devices)
|
77 |
+
|
78 |
+
|
79 |
+
def run_nvidia_smi() -> subprocess.Popen[bytes]:
|
80 |
+
commands = [ 'nvidia-smi', '--query', '--xml-format' ]
|
81 |
+
return subprocess.Popen(commands, stdout = subprocess.PIPE)
|
82 |
+
|
83 |
+
|
84 |
+
@lru_cache(maxsize = None)
|
85 |
+
def detect_static_execution_devices() -> List[ExecutionDevice]:
|
86 |
+
return detect_execution_devices()
|
87 |
+
|
88 |
+
|
89 |
+
def detect_execution_devices() -> List[ExecutionDevice]:
|
90 |
+
execution_devices : List[ExecutionDevice] = []
|
91 |
+
|
92 |
+
try:
|
93 |
+
output, _ = run_nvidia_smi().communicate()
|
94 |
+
root_element = ElementTree.fromstring(output)
|
95 |
+
except Exception:
|
96 |
+
root_element = ElementTree.Element('xml')
|
97 |
+
|
98 |
+
for gpu_element in root_element.findall('gpu'):
|
99 |
+
execution_devices.append(
|
100 |
+
{
|
101 |
+
'driver_version': root_element.find('driver_version').text,
|
102 |
+
'framework':
|
103 |
+
{
|
104 |
+
'name': 'CUDA',
|
105 |
+
'version': root_element.find('cuda_version').text
|
106 |
+
},
|
107 |
+
'product':
|
108 |
+
{
|
109 |
+
'vendor': 'NVIDIA',
|
110 |
+
'name': gpu_element.find('product_name').text.replace('NVIDIA ', '')
|
111 |
+
},
|
112 |
+
'video_memory':
|
113 |
+
{
|
114 |
+
'total': create_value_and_unit(gpu_element.find('fb_memory_usage/total').text),
|
115 |
+
'free': create_value_and_unit(gpu_element.find('fb_memory_usage/free').text)
|
116 |
+
},
|
117 |
+
'utilization':
|
118 |
+
{
|
119 |
+
'gpu': create_value_and_unit(gpu_element.find('utilization/gpu_util').text),
|
120 |
+
'memory': create_value_and_unit(gpu_element.find('utilization/memory_util').text)
|
121 |
+
}
|
122 |
+
})
|
123 |
+
return execution_devices
|
124 |
+
|
125 |
+
|
126 |
+
def create_value_and_unit(text : str) -> ValueAndUnit:
|
127 |
+
value, unit = text.split()
|
128 |
+
value_and_unit : ValueAndUnit =\
|
129 |
+
{
|
130 |
+
'value': int(value),
|
131 |
+
'unit': str(unit)
|
132 |
+
}
|
133 |
+
|
134 |
+
return value_and_unit
|
facefusion/exit_helper.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
from time import sleep
|
3 |
+
|
4 |
+
from facefusion import process_manager, state_manager
|
5 |
+
from facefusion.temp_helper import clear_temp_directory
|
6 |
+
from facefusion.typing import ErrorCode
|
7 |
+
|
8 |
+
|
9 |
+
def hard_exit(error_code : ErrorCode) -> None:
|
10 |
+
sys.exit(error_code)
|
11 |
+
|
12 |
+
|
13 |
+
def conditional_exit(error_code : ErrorCode) -> None:
|
14 |
+
if state_manager.get_item('command') == 'headless-run':
|
15 |
+
hard_exit(error_code)
|
16 |
+
|
17 |
+
|
18 |
+
def graceful_exit(error_code : ErrorCode) -> None:
|
19 |
+
process_manager.stop()
|
20 |
+
while process_manager.is_processing():
|
21 |
+
sleep(0.5)
|
22 |
+
if state_manager.get_item('target_path'):
|
23 |
+
clear_temp_directory(state_manager.get_item('target_path'))
|
24 |
+
hard_exit(error_code)
|
facefusion/face_analyser.py
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Optional
|
2 |
+
|
3 |
+
import numpy
|
4 |
+
|
5 |
+
from facefusion import state_manager
|
6 |
+
from facefusion.common_helper import get_first
|
7 |
+
from facefusion.face_classifier import classify_face
|
8 |
+
from facefusion.face_detector import detect_faces, detect_rotated_faces
|
9 |
+
from facefusion.face_helper import apply_nms, convert_to_face_landmark_5, estimate_face_angle, get_nms_threshold
|
10 |
+
from facefusion.face_landmarker import detect_face_landmarks, estimate_face_landmark_68_5
|
11 |
+
from facefusion.face_recognizer import calc_embedding
|
12 |
+
from facefusion.face_store import get_static_faces, set_static_faces
|
13 |
+
from facefusion.typing import BoundingBox, Face, FaceLandmark5, FaceLandmarkSet, FaceScoreSet, Score, VisionFrame
|
14 |
+
|
15 |
+
|
16 |
+
def create_faces(vision_frame : VisionFrame, bounding_boxes : List[BoundingBox], face_scores : List[Score], face_landmarks_5 : List[FaceLandmark5]) -> List[Face]:
|
17 |
+
faces = []
|
18 |
+
nms_threshold = get_nms_threshold(state_manager.get_item('face_detector_model'), state_manager.get_item('face_detector_angles'))
|
19 |
+
keep_indices = apply_nms(bounding_boxes, face_scores, state_manager.get_item('face_detector_score'), nms_threshold)
|
20 |
+
|
21 |
+
for index in keep_indices:
|
22 |
+
bounding_box = bounding_boxes[index]
|
23 |
+
face_score = face_scores[index]
|
24 |
+
face_landmark_5 = face_landmarks_5[index]
|
25 |
+
face_landmark_5_68 = face_landmark_5
|
26 |
+
face_landmark_68_5 = estimate_face_landmark_68_5(face_landmark_5_68)
|
27 |
+
face_landmark_68 = face_landmark_68_5
|
28 |
+
face_landmark_score_68 = 0.0
|
29 |
+
face_angle = estimate_face_angle(face_landmark_68_5)
|
30 |
+
|
31 |
+
if state_manager.get_item('face_landmarker_score') > 0:
|
32 |
+
face_landmark_68, face_landmark_score_68 = detect_face_landmarks(vision_frame, bounding_box, face_angle)
|
33 |
+
if face_landmark_score_68 > state_manager.get_item('face_landmarker_score'):
|
34 |
+
face_landmark_5_68 = convert_to_face_landmark_5(face_landmark_68)
|
35 |
+
|
36 |
+
face_landmark_set : FaceLandmarkSet =\
|
37 |
+
{
|
38 |
+
'5': face_landmark_5,
|
39 |
+
'5/68': face_landmark_5_68,
|
40 |
+
'68': face_landmark_68,
|
41 |
+
'68/5': face_landmark_68_5
|
42 |
+
}
|
43 |
+
face_score_set : FaceScoreSet =\
|
44 |
+
{
|
45 |
+
'detector': face_score,
|
46 |
+
'landmarker': face_landmark_score_68
|
47 |
+
}
|
48 |
+
embedding, normed_embedding = calc_embedding(vision_frame, face_landmark_set.get('5/68'))
|
49 |
+
gender, age, race = classify_face(vision_frame, face_landmark_set.get('5/68'))
|
50 |
+
faces.append(Face(
|
51 |
+
bounding_box = bounding_box,
|
52 |
+
score_set = face_score_set,
|
53 |
+
landmark_set = face_landmark_set,
|
54 |
+
angle = face_angle,
|
55 |
+
embedding = embedding,
|
56 |
+
normed_embedding = normed_embedding,
|
57 |
+
gender = gender,
|
58 |
+
age = age,
|
59 |
+
race = race
|
60 |
+
))
|
61 |
+
return faces
|
62 |
+
|
63 |
+
|
64 |
+
def get_one_face(faces : List[Face], position : int = 0) -> Optional[Face]:
|
65 |
+
if faces:
|
66 |
+
position = min(position, len(faces) - 1)
|
67 |
+
return faces[position]
|
68 |
+
return None
|
69 |
+
|
70 |
+
|
71 |
+
def get_average_face(faces : List[Face]) -> Optional[Face]:
|
72 |
+
embeddings = []
|
73 |
+
normed_embeddings = []
|
74 |
+
|
75 |
+
if faces:
|
76 |
+
first_face = get_first(faces)
|
77 |
+
|
78 |
+
for face in faces:
|
79 |
+
embeddings.append(face.embedding)
|
80 |
+
normed_embeddings.append(face.normed_embedding)
|
81 |
+
|
82 |
+
return Face(
|
83 |
+
bounding_box = first_face.bounding_box,
|
84 |
+
score_set = first_face.score_set,
|
85 |
+
landmark_set = first_face.landmark_set,
|
86 |
+
angle = first_face.angle,
|
87 |
+
embedding = numpy.mean(embeddings, axis = 0),
|
88 |
+
normed_embedding = numpy.mean(normed_embeddings, axis = 0),
|
89 |
+
gender = first_face.gender,
|
90 |
+
age = first_face.age,
|
91 |
+
race = first_face.race
|
92 |
+
)
|
93 |
+
return None
|
94 |
+
|
95 |
+
|
96 |
+
def get_many_faces(vision_frames : List[VisionFrame]) -> List[Face]:
|
97 |
+
many_faces : List[Face] = []
|
98 |
+
|
99 |
+
for vision_frame in vision_frames:
|
100 |
+
if numpy.any(vision_frame):
|
101 |
+
static_faces = get_static_faces(vision_frame)
|
102 |
+
if static_faces:
|
103 |
+
many_faces.extend(static_faces)
|
104 |
+
else:
|
105 |
+
all_bounding_boxes = []
|
106 |
+
all_face_scores = []
|
107 |
+
all_face_landmarks_5 = []
|
108 |
+
|
109 |
+
for face_detector_angle in state_manager.get_item('face_detector_angles'):
|
110 |
+
if face_detector_angle == 0:
|
111 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_faces(vision_frame)
|
112 |
+
else:
|
113 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_rotated_faces(vision_frame, face_detector_angle)
|
114 |
+
all_bounding_boxes.extend(bounding_boxes)
|
115 |
+
all_face_scores.extend(face_scores)
|
116 |
+
all_face_landmarks_5.extend(face_landmarks_5)
|
117 |
+
|
118 |
+
if all_bounding_boxes and all_face_scores and all_face_landmarks_5 and state_manager.get_item('face_detector_score') > 0:
|
119 |
+
faces = create_faces(vision_frame, all_bounding_boxes, all_face_scores, all_face_landmarks_5)
|
120 |
+
|
121 |
+
if faces:
|
122 |
+
many_faces.extend(faces)
|
123 |
+
set_static_faces(vision_frame, faces)
|
124 |
+
return many_faces
|
facefusion/face_classifier.py
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple
|
2 |
+
|
3 |
+
import numpy
|
4 |
+
|
5 |
+
from facefusion import inference_manager
|
6 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
7 |
+
from facefusion.face_helper import warp_face_by_face_landmark_5
|
8 |
+
from facefusion.filesystem import resolve_relative_path
|
9 |
+
from facefusion.thread_helper import conditional_thread_semaphore
|
10 |
+
from facefusion.typing import Age, FaceLandmark5, Gender, InferencePool, ModelOptions, ModelSet, Race, VisionFrame
|
11 |
+
|
12 |
+
MODEL_SET : ModelSet =\
|
13 |
+
{
|
14 |
+
'fairface':
|
15 |
+
{
|
16 |
+
'hashes':
|
17 |
+
{
|
18 |
+
'face_classifier':
|
19 |
+
{
|
20 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/fairface.hash',
|
21 |
+
'path': resolve_relative_path('../.assets/models/fairface.hash')
|
22 |
+
}
|
23 |
+
},
|
24 |
+
'sources':
|
25 |
+
{
|
26 |
+
'face_classifier':
|
27 |
+
{
|
28 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/fairface.onnx',
|
29 |
+
'path': resolve_relative_path('../.assets/models/fairface.onnx')
|
30 |
+
}
|
31 |
+
},
|
32 |
+
'template': 'arcface_112_v2',
|
33 |
+
'size': (224, 224),
|
34 |
+
'mean': [ 0.485, 0.456, 0.406 ],
|
35 |
+
'standard_deviation': [ 0.229, 0.224, 0.225 ]
|
36 |
+
}
|
37 |
+
}
|
38 |
+
|
39 |
+
|
40 |
+
def get_inference_pool() -> InferencePool:
|
41 |
+
model_sources = get_model_options().get('sources')
|
42 |
+
return inference_manager.get_inference_pool(__name__, model_sources)
|
43 |
+
|
44 |
+
|
45 |
+
def clear_inference_pool() -> None:
|
46 |
+
inference_manager.clear_inference_pool(__name__)
|
47 |
+
|
48 |
+
|
49 |
+
def get_model_options() -> ModelOptions:
|
50 |
+
return MODEL_SET.get('fairface')
|
51 |
+
|
52 |
+
|
53 |
+
def pre_check() -> bool:
|
54 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
55 |
+
model_hashes = get_model_options().get('hashes')
|
56 |
+
model_sources = get_model_options().get('sources')
|
57 |
+
|
58 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
59 |
+
|
60 |
+
|
61 |
+
def classify_face(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5) -> Tuple[Gender, Age, Race]:
|
62 |
+
model_template = get_model_options().get('template')
|
63 |
+
model_size = get_model_options().get('size')
|
64 |
+
model_mean = get_model_options().get('mean')
|
65 |
+
model_standard_deviation = get_model_options().get('standard_deviation')
|
66 |
+
crop_vision_frame, _ = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, model_size)
|
67 |
+
crop_vision_frame = crop_vision_frame.astype(numpy.float32)[:, :, ::-1] / 255
|
68 |
+
crop_vision_frame -= model_mean
|
69 |
+
crop_vision_frame /= model_standard_deviation
|
70 |
+
crop_vision_frame = crop_vision_frame.transpose(2, 0, 1)
|
71 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0)
|
72 |
+
gender_id, age_id, race_id = forward(crop_vision_frame)
|
73 |
+
gender = categorize_gender(gender_id[0])
|
74 |
+
age = categorize_age(age_id[0])
|
75 |
+
race = categorize_race(race_id[0])
|
76 |
+
return gender, age, race
|
77 |
+
|
78 |
+
|
79 |
+
def forward(crop_vision_frame : VisionFrame) -> Tuple[List[int], List[int], List[int]]:
|
80 |
+
face_classifier = get_inference_pool().get('face_classifier')
|
81 |
+
|
82 |
+
with conditional_thread_semaphore():
|
83 |
+
race_id, gender_id, age_id = face_classifier.run(None,
|
84 |
+
{
|
85 |
+
'input': crop_vision_frame
|
86 |
+
})
|
87 |
+
|
88 |
+
return gender_id, age_id, race_id
|
89 |
+
|
90 |
+
|
91 |
+
def categorize_gender(gender_id : int) -> Gender:
|
92 |
+
if gender_id == 1:
|
93 |
+
return 'female'
|
94 |
+
return 'male'
|
95 |
+
|
96 |
+
|
97 |
+
def categorize_age(age_id : int) -> Age:
|
98 |
+
if age_id == 0:
|
99 |
+
return range(0, 2)
|
100 |
+
if age_id == 1:
|
101 |
+
return range(3, 9)
|
102 |
+
if age_id == 2:
|
103 |
+
return range(10, 19)
|
104 |
+
if age_id == 3:
|
105 |
+
return range(20, 29)
|
106 |
+
if age_id == 4:
|
107 |
+
return range(30, 39)
|
108 |
+
if age_id == 5:
|
109 |
+
return range(40, 49)
|
110 |
+
if age_id == 6:
|
111 |
+
return range(50, 59)
|
112 |
+
if age_id == 7:
|
113 |
+
return range(60, 69)
|
114 |
+
return range(70, 100)
|
115 |
+
|
116 |
+
|
117 |
+
def categorize_race(race_id : int) -> Race:
|
118 |
+
if race_id == 1:
|
119 |
+
return 'black'
|
120 |
+
if race_id == 2:
|
121 |
+
return 'latino'
|
122 |
+
if race_id == 3 or race_id == 4:
|
123 |
+
return 'asian'
|
124 |
+
if race_id == 5:
|
125 |
+
return 'indian'
|
126 |
+
if race_id == 6:
|
127 |
+
return 'arabic'
|
128 |
+
return 'white'
|
facefusion/face_detector.py
ADDED
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple
|
2 |
+
|
3 |
+
import cv2
|
4 |
+
import numpy
|
5 |
+
|
6 |
+
from facefusion import inference_manager, state_manager
|
7 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
8 |
+
from facefusion.face_helper import create_rotated_matrix_and_size, create_static_anchors, distance_to_bounding_box, distance_to_face_landmark_5, normalize_bounding_box, transform_bounding_box, transform_points
|
9 |
+
from facefusion.filesystem import resolve_relative_path
|
10 |
+
from facefusion.thread_helper import thread_semaphore
|
11 |
+
from facefusion.typing import Angle, BoundingBox, Detection, DownloadSet, FaceLandmark5, InferencePool, ModelSet, Score, VisionFrame
|
12 |
+
from facefusion.vision import resize_frame_resolution, unpack_resolution
|
13 |
+
|
14 |
+
MODEL_SET : ModelSet =\
|
15 |
+
{
|
16 |
+
'retinaface':
|
17 |
+
{
|
18 |
+
'hashes':
|
19 |
+
{
|
20 |
+
'retinaface':
|
21 |
+
{
|
22 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/retinaface_10g.hash',
|
23 |
+
'path': resolve_relative_path('../.assets/models/retinaface_10g.hash')
|
24 |
+
}
|
25 |
+
},
|
26 |
+
'sources':
|
27 |
+
{
|
28 |
+
'retinaface':
|
29 |
+
{
|
30 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/retinaface_10g.onnx',
|
31 |
+
'path': resolve_relative_path('../.assets/models/retinaface_10g.onnx')
|
32 |
+
}
|
33 |
+
}
|
34 |
+
},
|
35 |
+
'scrfd':
|
36 |
+
{
|
37 |
+
'hashes':
|
38 |
+
{
|
39 |
+
'scrfd':
|
40 |
+
{
|
41 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/scrfd_2.5g.hash',
|
42 |
+
'path': resolve_relative_path('../.assets/models/scrfd_2.5g.hash')
|
43 |
+
}
|
44 |
+
},
|
45 |
+
'sources':
|
46 |
+
{
|
47 |
+
'scrfd':
|
48 |
+
{
|
49 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/scrfd_2.5g.onnx',
|
50 |
+
'path': resolve_relative_path('../.assets/models/scrfd_2.5g.onnx')
|
51 |
+
}
|
52 |
+
}
|
53 |
+
},
|
54 |
+
'yoloface':
|
55 |
+
{
|
56 |
+
'hashes':
|
57 |
+
{
|
58 |
+
'yoloface':
|
59 |
+
{
|
60 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/yoloface_8n.hash',
|
61 |
+
'path': resolve_relative_path('../.assets/models/yoloface_8n.hash')
|
62 |
+
}
|
63 |
+
},
|
64 |
+
'sources':
|
65 |
+
{
|
66 |
+
'yoloface':
|
67 |
+
{
|
68 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/yoloface_8n.onnx',
|
69 |
+
'path': resolve_relative_path('../.assets/models/yoloface_8n.onnx')
|
70 |
+
}
|
71 |
+
}
|
72 |
+
}
|
73 |
+
}
|
74 |
+
|
75 |
+
|
76 |
+
def get_inference_pool() -> InferencePool:
|
77 |
+
_, model_sources = collect_model_downloads()
|
78 |
+
model_context = __name__ + '.' + state_manager.get_item('face_detector_model')
|
79 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
80 |
+
|
81 |
+
|
82 |
+
def clear_inference_pool() -> None:
|
83 |
+
model_context = __name__ + '.' + state_manager.get_item('face_detector_model')
|
84 |
+
inference_manager.clear_inference_pool(model_context)
|
85 |
+
|
86 |
+
|
87 |
+
def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]:
|
88 |
+
model_hashes = {}
|
89 |
+
model_sources = {}
|
90 |
+
|
91 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'retinaface' ]:
|
92 |
+
model_hashes['retinaface'] = MODEL_SET.get('retinaface').get('hashes').get('retinaface')
|
93 |
+
model_sources['retinaface'] = MODEL_SET.get('retinaface').get('sources').get('retinaface')
|
94 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'scrfd' ]:
|
95 |
+
model_hashes['scrfd'] = MODEL_SET.get('scrfd').get('hashes').get('scrfd')
|
96 |
+
model_sources['scrfd'] = MODEL_SET.get('scrfd').get('sources').get('scrfd')
|
97 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'yoloface' ]:
|
98 |
+
model_hashes['yoloface'] = MODEL_SET.get('yoloface').get('hashes').get('yoloface')
|
99 |
+
model_sources['yoloface'] = MODEL_SET.get('yoloface').get('sources').get('yoloface')
|
100 |
+
return model_hashes, model_sources
|
101 |
+
|
102 |
+
|
103 |
+
def pre_check() -> bool:
|
104 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
105 |
+
model_hashes, model_sources = collect_model_downloads()
|
106 |
+
|
107 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
108 |
+
|
109 |
+
|
110 |
+
def detect_faces(vision_frame : VisionFrame) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]:
|
111 |
+
all_bounding_boxes : List[BoundingBox] = []
|
112 |
+
all_face_scores : List[Score] = []
|
113 |
+
all_face_landmarks_5 : List[FaceLandmark5] = []
|
114 |
+
|
115 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'retinaface' ]:
|
116 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_with_retinaface(vision_frame, state_manager.get_item('face_detector_size'))
|
117 |
+
all_bounding_boxes.extend(bounding_boxes)
|
118 |
+
all_face_scores.extend(face_scores)
|
119 |
+
all_face_landmarks_5.extend(face_landmarks_5)
|
120 |
+
|
121 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'scrfd' ]:
|
122 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_with_scrfd(vision_frame, state_manager.get_item('face_detector_size'))
|
123 |
+
all_bounding_boxes.extend(bounding_boxes)
|
124 |
+
all_face_scores.extend(face_scores)
|
125 |
+
all_face_landmarks_5.extend(face_landmarks_5)
|
126 |
+
|
127 |
+
if state_manager.get_item('face_detector_model') in [ 'many', 'yoloface' ]:
|
128 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_with_yoloface(vision_frame, state_manager.get_item('face_detector_size'))
|
129 |
+
all_bounding_boxes.extend(bounding_boxes)
|
130 |
+
all_face_scores.extend(face_scores)
|
131 |
+
all_face_landmarks_5.extend(face_landmarks_5)
|
132 |
+
|
133 |
+
all_bounding_boxes = [ normalize_bounding_box(all_bounding_box) for all_bounding_box in all_bounding_boxes ]
|
134 |
+
return all_bounding_boxes, all_face_scores, all_face_landmarks_5
|
135 |
+
|
136 |
+
|
137 |
+
def detect_rotated_faces(vision_frame : VisionFrame, angle : Angle) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]:
|
138 |
+
rotated_matrix, rotated_size = create_rotated_matrix_and_size(angle, vision_frame.shape[:2][::-1])
|
139 |
+
rotated_vision_frame = cv2.warpAffine(vision_frame, rotated_matrix, rotated_size)
|
140 |
+
rotated_inverse_matrix = cv2.invertAffineTransform(rotated_matrix)
|
141 |
+
bounding_boxes, face_scores, face_landmarks_5 = detect_faces(rotated_vision_frame)
|
142 |
+
bounding_boxes = [ transform_bounding_box(bounding_box, rotated_inverse_matrix) for bounding_box in bounding_boxes ]
|
143 |
+
face_landmarks_5 = [ transform_points(face_landmark_5, rotated_inverse_matrix) for face_landmark_5 in face_landmarks_5 ]
|
144 |
+
return bounding_boxes, face_scores, face_landmarks_5
|
145 |
+
|
146 |
+
|
147 |
+
def detect_with_retinaface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]:
|
148 |
+
bounding_boxes = []
|
149 |
+
face_scores = []
|
150 |
+
face_landmarks_5 = []
|
151 |
+
feature_strides = [ 8, 16, 32 ]
|
152 |
+
feature_map_channel = 3
|
153 |
+
anchor_total = 2
|
154 |
+
face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
|
155 |
+
temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
|
156 |
+
ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
|
157 |
+
ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
|
158 |
+
detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
|
159 |
+
detection = forward_with_retinaface(detect_vision_frame)
|
160 |
+
|
161 |
+
for index, feature_stride in enumerate(feature_strides):
|
162 |
+
keep_indices = numpy.where(detection[index] >= state_manager.get_item('face_detector_score'))[0]
|
163 |
+
|
164 |
+
if numpy.any(keep_indices):
|
165 |
+
stride_height = face_detector_height // feature_stride
|
166 |
+
stride_width = face_detector_width // feature_stride
|
167 |
+
anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width)
|
168 |
+
bounding_box_raw = detection[index + feature_map_channel] * feature_stride
|
169 |
+
face_landmark_5_raw = detection[index + feature_map_channel * 2] * feature_stride
|
170 |
+
|
171 |
+
for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]:
|
172 |
+
bounding_boxes.append(numpy.array(
|
173 |
+
[
|
174 |
+
bounding_box[0] * ratio_width,
|
175 |
+
bounding_box[1] * ratio_height,
|
176 |
+
bounding_box[2] * ratio_width,
|
177 |
+
bounding_box[3] * ratio_height,
|
178 |
+
]))
|
179 |
+
|
180 |
+
for score in detection[index][keep_indices]:
|
181 |
+
face_scores.append(score[0])
|
182 |
+
|
183 |
+
for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]:
|
184 |
+
face_landmarks_5.append(face_landmark_5 * [ ratio_width, ratio_height ])
|
185 |
+
|
186 |
+
return bounding_boxes, face_scores, face_landmarks_5
|
187 |
+
|
188 |
+
|
189 |
+
def detect_with_scrfd(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]:
|
190 |
+
bounding_boxes = []
|
191 |
+
face_scores = []
|
192 |
+
face_landmarks_5 = []
|
193 |
+
feature_strides = [ 8, 16, 32 ]
|
194 |
+
feature_map_channel = 3
|
195 |
+
anchor_total = 2
|
196 |
+
face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
|
197 |
+
temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
|
198 |
+
ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
|
199 |
+
ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
|
200 |
+
detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
|
201 |
+
detection = forward_with_scrfd(detect_vision_frame)
|
202 |
+
|
203 |
+
for index, feature_stride in enumerate(feature_strides):
|
204 |
+
keep_indices = numpy.where(detection[index] >= state_manager.get_item('face_detector_score'))[0]
|
205 |
+
|
206 |
+
if numpy.any(keep_indices):
|
207 |
+
stride_height = face_detector_height // feature_stride
|
208 |
+
stride_width = face_detector_width // feature_stride
|
209 |
+
anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width)
|
210 |
+
bounding_box_raw = detection[index + feature_map_channel] * feature_stride
|
211 |
+
face_landmark_5_raw = detection[index + feature_map_channel * 2] * feature_stride
|
212 |
+
|
213 |
+
for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]:
|
214 |
+
bounding_boxes.append(numpy.array(
|
215 |
+
[
|
216 |
+
bounding_box[0] * ratio_width,
|
217 |
+
bounding_box[1] * ratio_height,
|
218 |
+
bounding_box[2] * ratio_width,
|
219 |
+
bounding_box[3] * ratio_height,
|
220 |
+
]))
|
221 |
+
|
222 |
+
for score in detection[index][keep_indices]:
|
223 |
+
face_scores.append(score[0])
|
224 |
+
|
225 |
+
for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]:
|
226 |
+
face_landmarks_5.append(face_landmark_5 * [ ratio_width, ratio_height ])
|
227 |
+
|
228 |
+
return bounding_boxes, face_scores, face_landmarks_5
|
229 |
+
|
230 |
+
|
231 |
+
def detect_with_yoloface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]:
|
232 |
+
bounding_boxes = []
|
233 |
+
face_scores = []
|
234 |
+
face_landmarks_5 = []
|
235 |
+
face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
|
236 |
+
temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height))
|
237 |
+
ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0]
|
238 |
+
ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1]
|
239 |
+
detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size)
|
240 |
+
detection = forward_with_yoloface(detect_vision_frame)
|
241 |
+
detection = numpy.squeeze(detection).T
|
242 |
+
bounding_box_raw, score_raw, face_landmark_5_raw = numpy.split(detection, [ 4, 5 ], axis = 1)
|
243 |
+
keep_indices = numpy.where(score_raw > state_manager.get_item('face_detector_score'))[0]
|
244 |
+
|
245 |
+
if numpy.any(keep_indices):
|
246 |
+
bounding_box_raw, face_landmark_5_raw, score_raw = bounding_box_raw[keep_indices], face_landmark_5_raw[keep_indices], score_raw[keep_indices]
|
247 |
+
|
248 |
+
for bounding_box in bounding_box_raw:
|
249 |
+
bounding_boxes.append(numpy.array(
|
250 |
+
[
|
251 |
+
(bounding_box[0] - bounding_box[2] / 2) * ratio_width,
|
252 |
+
(bounding_box[1] - bounding_box[3] / 2) * ratio_height,
|
253 |
+
(bounding_box[0] + bounding_box[2] / 2) * ratio_width,
|
254 |
+
(bounding_box[1] + bounding_box[3] / 2) * ratio_height,
|
255 |
+
]))
|
256 |
+
|
257 |
+
face_scores = score_raw.ravel().tolist()
|
258 |
+
face_landmark_5_raw[:, 0::3] = (face_landmark_5_raw[:, 0::3]) * ratio_width
|
259 |
+
face_landmark_5_raw[:, 1::3] = (face_landmark_5_raw[:, 1::3]) * ratio_height
|
260 |
+
|
261 |
+
for face_landmark_5 in face_landmark_5_raw:
|
262 |
+
face_landmarks_5.append(numpy.array(face_landmark_5.reshape(-1, 3)[:, :2]))
|
263 |
+
|
264 |
+
return bounding_boxes, face_scores, face_landmarks_5
|
265 |
+
|
266 |
+
|
267 |
+
def forward_with_retinaface(detect_vision_frame : VisionFrame) -> Detection:
|
268 |
+
face_detector = get_inference_pool().get('retinaface')
|
269 |
+
|
270 |
+
with thread_semaphore():
|
271 |
+
detection = face_detector.run(None,
|
272 |
+
{
|
273 |
+
'input': detect_vision_frame
|
274 |
+
})
|
275 |
+
|
276 |
+
return detection
|
277 |
+
|
278 |
+
|
279 |
+
def forward_with_scrfd(detect_vision_frame : VisionFrame) -> Detection:
|
280 |
+
face_detector = get_inference_pool().get('scrfd')
|
281 |
+
|
282 |
+
with thread_semaphore():
|
283 |
+
detection = face_detector.run(None,
|
284 |
+
{
|
285 |
+
'input': detect_vision_frame
|
286 |
+
})
|
287 |
+
|
288 |
+
return detection
|
289 |
+
|
290 |
+
|
291 |
+
def forward_with_yoloface(detect_vision_frame : VisionFrame) -> Detection:
|
292 |
+
face_detector = get_inference_pool().get('yoloface')
|
293 |
+
|
294 |
+
with thread_semaphore():
|
295 |
+
detection = face_detector.run(None,
|
296 |
+
{
|
297 |
+
'input': detect_vision_frame
|
298 |
+
})
|
299 |
+
|
300 |
+
return detection
|
301 |
+
|
302 |
+
|
303 |
+
def prepare_detect_frame(temp_vision_frame : VisionFrame, face_detector_size : str) -> VisionFrame:
|
304 |
+
face_detector_width, face_detector_height = unpack_resolution(face_detector_size)
|
305 |
+
detect_vision_frame = numpy.zeros((face_detector_height, face_detector_width, 3))
|
306 |
+
detect_vision_frame[:temp_vision_frame.shape[0], :temp_vision_frame.shape[1], :] = temp_vision_frame
|
307 |
+
detect_vision_frame = (detect_vision_frame - 127.5) / 128.0
|
308 |
+
detect_vision_frame = numpy.expand_dims(detect_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
|
309 |
+
return detect_vision_frame
|
facefusion/face_helper.py
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
from typing import List, Sequence, Tuple
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
from cv2.typing import Size
|
7 |
+
|
8 |
+
from facefusion.typing import Anchors, Angle, BoundingBox, Distance, FaceDetectorModel, FaceLandmark5, FaceLandmark68, Mask, Matrix, Points, Scale, Score, Translation, VisionFrame, WarpTemplate, WarpTemplateSet
|
9 |
+
|
10 |
+
WARP_TEMPLATES : WarpTemplateSet =\
|
11 |
+
{
|
12 |
+
'arcface_112_v1': numpy.array(
|
13 |
+
[
|
14 |
+
[ 0.35473214, 0.45658929 ],
|
15 |
+
[ 0.64526786, 0.45658929 ],
|
16 |
+
[ 0.50000000, 0.61154464 ],
|
17 |
+
[ 0.37913393, 0.77687500 ],
|
18 |
+
[ 0.62086607, 0.77687500 ]
|
19 |
+
]),
|
20 |
+
'arcface_112_v2': numpy.array(
|
21 |
+
[
|
22 |
+
[ 0.34191607, 0.46157411 ],
|
23 |
+
[ 0.65653393, 0.45983393 ],
|
24 |
+
[ 0.50022500, 0.64050536 ],
|
25 |
+
[ 0.37097589, 0.82469196 ],
|
26 |
+
[ 0.63151696, 0.82325089 ]
|
27 |
+
]),
|
28 |
+
'arcface_128_v2': numpy.array(
|
29 |
+
[
|
30 |
+
[ 0.36167656, 0.40387734 ],
|
31 |
+
[ 0.63696719, 0.40235469 ],
|
32 |
+
[ 0.50019687, 0.56044219 ],
|
33 |
+
[ 0.38710391, 0.72160547 ],
|
34 |
+
[ 0.61507734, 0.72034453 ]
|
35 |
+
]),
|
36 |
+
'ffhq_512': numpy.array(
|
37 |
+
[
|
38 |
+
[ 0.37691676, 0.46864664 ],
|
39 |
+
[ 0.62285697, 0.46912813 ],
|
40 |
+
[ 0.50123859, 0.61331904 ],
|
41 |
+
[ 0.39308822, 0.72541100 ],
|
42 |
+
[ 0.61150205, 0.72490465 ]
|
43 |
+
])
|
44 |
+
}
|
45 |
+
|
46 |
+
|
47 |
+
def estimate_matrix_by_face_landmark_5(face_landmark_5 : FaceLandmark5, warp_template : WarpTemplate, crop_size : Size) -> Matrix:
|
48 |
+
normed_warp_template = WARP_TEMPLATES.get(warp_template) * crop_size
|
49 |
+
affine_matrix = cv2.estimateAffinePartial2D(face_landmark_5, normed_warp_template, method = cv2.RANSAC, ransacReprojThreshold = 100)[0]
|
50 |
+
return affine_matrix
|
51 |
+
|
52 |
+
|
53 |
+
def warp_face_by_face_landmark_5(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5, warp_template : WarpTemplate, crop_size : Size) -> Tuple[VisionFrame, Matrix]:
|
54 |
+
affine_matrix = estimate_matrix_by_face_landmark_5(face_landmark_5, warp_template, crop_size)
|
55 |
+
crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size, borderMode = cv2.BORDER_REPLICATE, flags = cv2.INTER_AREA)
|
56 |
+
return crop_vision_frame, affine_matrix
|
57 |
+
|
58 |
+
|
59 |
+
def warp_face_by_bounding_box(temp_vision_frame : VisionFrame, bounding_box : BoundingBox, crop_size : Size) -> Tuple[VisionFrame, Matrix]:
|
60 |
+
source_points = numpy.array([ [ bounding_box[0], bounding_box[1] ], [bounding_box[2], bounding_box[1] ], [ bounding_box[0], bounding_box[3] ] ]).astype(numpy.float32)
|
61 |
+
target_points = numpy.array([ [ 0, 0 ], [ crop_size[0], 0 ], [ 0, crop_size[1] ] ]).astype(numpy.float32)
|
62 |
+
affine_matrix = cv2.getAffineTransform(source_points, target_points)
|
63 |
+
if bounding_box[2] - bounding_box[0] > crop_size[0] or bounding_box[3] - bounding_box[1] > crop_size[1]:
|
64 |
+
interpolation_method = cv2.INTER_AREA
|
65 |
+
else:
|
66 |
+
interpolation_method = cv2.INTER_LINEAR
|
67 |
+
crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size, flags = interpolation_method)
|
68 |
+
return crop_vision_frame, affine_matrix
|
69 |
+
|
70 |
+
|
71 |
+
def warp_face_by_translation(temp_vision_frame : VisionFrame, translation : Translation, scale : float, crop_size : Size) -> Tuple[VisionFrame, Matrix]:
|
72 |
+
affine_matrix = numpy.array([ [ scale, 0, translation[0] ], [ 0, scale, translation[1] ] ])
|
73 |
+
crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size)
|
74 |
+
return crop_vision_frame, affine_matrix
|
75 |
+
|
76 |
+
|
77 |
+
def paste_back(temp_vision_frame : VisionFrame, crop_vision_frame : VisionFrame, crop_mask : Mask, affine_matrix : Matrix) -> VisionFrame:
|
78 |
+
inverse_matrix = cv2.invertAffineTransform(affine_matrix)
|
79 |
+
temp_size = temp_vision_frame.shape[:2][::-1]
|
80 |
+
inverse_mask = cv2.warpAffine(crop_mask, inverse_matrix, temp_size).clip(0, 1)
|
81 |
+
inverse_vision_frame = cv2.warpAffine(crop_vision_frame, inverse_matrix, temp_size, borderMode = cv2.BORDER_REPLICATE)
|
82 |
+
paste_vision_frame = temp_vision_frame.copy()
|
83 |
+
paste_vision_frame[:, :, 0] = inverse_mask * inverse_vision_frame[:, :, 0] + (1 - inverse_mask) * temp_vision_frame[:, :, 0]
|
84 |
+
paste_vision_frame[:, :, 1] = inverse_mask * inverse_vision_frame[:, :, 1] + (1 - inverse_mask) * temp_vision_frame[:, :, 1]
|
85 |
+
paste_vision_frame[:, :, 2] = inverse_mask * inverse_vision_frame[:, :, 2] + (1 - inverse_mask) * temp_vision_frame[:, :, 2]
|
86 |
+
return paste_vision_frame
|
87 |
+
|
88 |
+
|
89 |
+
@lru_cache(maxsize = None)
|
90 |
+
def create_static_anchors(feature_stride : int, anchor_total : int, stride_height : int, stride_width : int) -> Anchors:
|
91 |
+
y, x = numpy.mgrid[:stride_height, :stride_width][::-1]
|
92 |
+
anchors = numpy.stack((y, x), axis = -1)
|
93 |
+
anchors = (anchors * feature_stride).reshape((-1, 2))
|
94 |
+
anchors = numpy.stack([ anchors ] * anchor_total, axis = 1).reshape((-1, 2))
|
95 |
+
return anchors
|
96 |
+
|
97 |
+
|
98 |
+
def create_rotated_matrix_and_size(angle : Angle, size : Size) -> Tuple[Matrix, Size]:
|
99 |
+
rotated_matrix = cv2.getRotationMatrix2D((size[0] / 2, size[1] / 2), angle, 1)
|
100 |
+
rotated_size = numpy.dot(numpy.abs(rotated_matrix[:, :2]), size)
|
101 |
+
rotated_matrix[:, -1] += (rotated_size - size) * 0.5 #type:ignore[misc]
|
102 |
+
rotated_size = int(rotated_size[0]), int(rotated_size[1])
|
103 |
+
return rotated_matrix, rotated_size
|
104 |
+
|
105 |
+
|
106 |
+
def create_bounding_box(face_landmark_68 : FaceLandmark68) -> BoundingBox:
|
107 |
+
min_x, min_y = numpy.min(face_landmark_68, axis = 0)
|
108 |
+
max_x, max_y = numpy.max(face_landmark_68, axis = 0)
|
109 |
+
bounding_box = normalize_bounding_box(numpy.array([ min_x, min_y, max_x, max_y ]))
|
110 |
+
return bounding_box
|
111 |
+
|
112 |
+
|
113 |
+
def normalize_bounding_box(bounding_box : BoundingBox) -> BoundingBox:
|
114 |
+
x1, y1, x2, y2 = bounding_box
|
115 |
+
x1, x2 = sorted([ x1, x2 ])
|
116 |
+
y1, y2 = sorted([ y1, y2 ])
|
117 |
+
return numpy.array([ x1, y1, x2, y2 ])
|
118 |
+
|
119 |
+
|
120 |
+
def transform_points(points : Points, matrix : Matrix) -> Points:
|
121 |
+
points = points.reshape(-1, 1, 2)
|
122 |
+
points = cv2.transform(points, matrix) #type:ignore[assignment]
|
123 |
+
points = points.reshape(-1, 2)
|
124 |
+
return points
|
125 |
+
|
126 |
+
|
127 |
+
def transform_bounding_box(bounding_box : BoundingBox, matrix : Matrix) -> BoundingBox:
|
128 |
+
points = numpy.array(
|
129 |
+
[
|
130 |
+
[ bounding_box[0], bounding_box[1] ],
|
131 |
+
[ bounding_box[2], bounding_box[1] ],
|
132 |
+
[ bounding_box[2], bounding_box[3] ],
|
133 |
+
[ bounding_box[0], bounding_box[3] ]
|
134 |
+
])
|
135 |
+
points = transform_points(points, matrix)
|
136 |
+
x1, y1 = numpy.min(points, axis = 0)
|
137 |
+
x2, y2 = numpy.max(points, axis = 0)
|
138 |
+
return normalize_bounding_box(numpy.array([ x1, y1, x2, y2 ]))
|
139 |
+
|
140 |
+
|
141 |
+
def distance_to_bounding_box(points : Points, distance : Distance) -> BoundingBox:
|
142 |
+
x1 = points[:, 0] - distance[:, 0]
|
143 |
+
y1 = points[:, 1] - distance[:, 1]
|
144 |
+
x2 = points[:, 0] + distance[:, 2]
|
145 |
+
y2 = points[:, 1] + distance[:, 3]
|
146 |
+
bounding_box = numpy.column_stack([ x1, y1, x2, y2 ])
|
147 |
+
return bounding_box
|
148 |
+
|
149 |
+
|
150 |
+
def distance_to_face_landmark_5(points : Points, distance : Distance) -> FaceLandmark5:
|
151 |
+
x = points[:, 0::2] + distance[:, 0::2]
|
152 |
+
y = points[:, 1::2] + distance[:, 1::2]
|
153 |
+
face_landmark_5 = numpy.stack((x, y), axis = -1)
|
154 |
+
return face_landmark_5
|
155 |
+
|
156 |
+
|
157 |
+
def scale_face_landmark_5(face_landmark_5 : FaceLandmark5, scale : Scale) -> FaceLandmark5:
|
158 |
+
face_landmark_5_scale = face_landmark_5 - face_landmark_5[2]
|
159 |
+
face_landmark_5_scale *= scale
|
160 |
+
face_landmark_5_scale += face_landmark_5[2]
|
161 |
+
return face_landmark_5_scale
|
162 |
+
|
163 |
+
|
164 |
+
def convert_to_face_landmark_5(face_landmark_68 : FaceLandmark68) -> FaceLandmark5:
|
165 |
+
face_landmark_5 = numpy.array(
|
166 |
+
[
|
167 |
+
numpy.mean(face_landmark_68[36:42], axis = 0),
|
168 |
+
numpy.mean(face_landmark_68[42:48], axis = 0),
|
169 |
+
face_landmark_68[30],
|
170 |
+
face_landmark_68[48],
|
171 |
+
face_landmark_68[54]
|
172 |
+
])
|
173 |
+
return face_landmark_5
|
174 |
+
|
175 |
+
|
176 |
+
def estimate_face_angle(face_landmark_68 : FaceLandmark68) -> Angle:
|
177 |
+
x1, y1 = face_landmark_68[0]
|
178 |
+
x2, y2 = face_landmark_68[16]
|
179 |
+
theta = numpy.arctan2(y2 - y1, x2 - x1)
|
180 |
+
theta = numpy.degrees(theta) % 360
|
181 |
+
angles = numpy.linspace(0, 360, 5)
|
182 |
+
index = numpy.argmin(numpy.abs(angles - theta))
|
183 |
+
face_angle = int(angles[index] % 360)
|
184 |
+
return face_angle
|
185 |
+
|
186 |
+
|
187 |
+
def apply_nms(bounding_boxes : List[BoundingBox], face_scores : List[Score], score_threshold : float, nms_threshold : float) -> Sequence[int]:
|
188 |
+
normed_bounding_boxes = [ (x1, y1, x2 - x1, y2 - y1) for (x1, y1, x2, y2) in bounding_boxes ]
|
189 |
+
keep_indices = cv2.dnn.NMSBoxes(normed_bounding_boxes, face_scores, score_threshold = score_threshold, nms_threshold = nms_threshold)
|
190 |
+
return keep_indices
|
191 |
+
|
192 |
+
|
193 |
+
def get_nms_threshold(face_detector_model : FaceDetectorModel, face_detector_angles : List[Angle]) -> float:
|
194 |
+
if face_detector_model == 'many':
|
195 |
+
return 0.1
|
196 |
+
if len(face_detector_angles) == 2:
|
197 |
+
return 0.3
|
198 |
+
if len(face_detector_angles) == 3:
|
199 |
+
return 0.2
|
200 |
+
if len(face_detector_angles) == 4:
|
201 |
+
return 0.1
|
202 |
+
return 0.4
|
203 |
+
|
204 |
+
|
205 |
+
def merge_matrix(matrices : List[Matrix]) -> Matrix:
|
206 |
+
merged_matrix = numpy.vstack([ matrices[0], [ 0, 0, 1 ] ])
|
207 |
+
for matrix in matrices[1:]:
|
208 |
+
matrix = numpy.vstack([ matrix, [ 0, 0, 1 ] ])
|
209 |
+
merged_matrix = numpy.dot(merged_matrix, matrix)
|
210 |
+
return merged_matrix[:2, :]
|
facefusion/face_landmarker.py
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Tuple
|
2 |
+
|
3 |
+
import cv2
|
4 |
+
import numpy
|
5 |
+
|
6 |
+
from facefusion import inference_manager, state_manager
|
7 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
8 |
+
from facefusion.face_helper import create_rotated_matrix_and_size, estimate_matrix_by_face_landmark_5, transform_points, warp_face_by_translation
|
9 |
+
from facefusion.filesystem import resolve_relative_path
|
10 |
+
from facefusion.thread_helper import conditional_thread_semaphore
|
11 |
+
from facefusion.typing import Angle, BoundingBox, DownloadSet, FaceLandmark5, FaceLandmark68, InferencePool, ModelSet, Prediction, Score, VisionFrame
|
12 |
+
|
13 |
+
MODEL_SET : ModelSet =\
|
14 |
+
{
|
15 |
+
'2dfan4':
|
16 |
+
{
|
17 |
+
'hashes':
|
18 |
+
{
|
19 |
+
'2dfan4':
|
20 |
+
{
|
21 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/2dfan4.hash',
|
22 |
+
'path': resolve_relative_path('../.assets/models/2dfan4.hash')
|
23 |
+
}
|
24 |
+
},
|
25 |
+
'sources':
|
26 |
+
{
|
27 |
+
'2dfan4':
|
28 |
+
{
|
29 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/2dfan4.onnx',
|
30 |
+
'path': resolve_relative_path('../.assets/models/2dfan4.onnx')
|
31 |
+
}
|
32 |
+
},
|
33 |
+
'size': (256, 256)
|
34 |
+
},
|
35 |
+
'peppa_wutz':
|
36 |
+
{
|
37 |
+
'hashes':
|
38 |
+
{
|
39 |
+
'peppa_wutz':
|
40 |
+
{
|
41 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/peppa_wutz.hash',
|
42 |
+
'path': resolve_relative_path('../.assets/models/peppa_wutz.hash')
|
43 |
+
}
|
44 |
+
},
|
45 |
+
'sources':
|
46 |
+
{
|
47 |
+
'peppa_wutz':
|
48 |
+
{
|
49 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/peppa_wutz.onnx',
|
50 |
+
'path': resolve_relative_path('../.assets/models/peppa_wutz.onnx')
|
51 |
+
}
|
52 |
+
},
|
53 |
+
'size': (256, 256)
|
54 |
+
},
|
55 |
+
'fan_68_5':
|
56 |
+
{
|
57 |
+
'hashes':
|
58 |
+
{
|
59 |
+
'fan_68_5':
|
60 |
+
{
|
61 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/fan_68_5.hash',
|
62 |
+
'path': resolve_relative_path('../.assets/models/fan_68_5.hash')
|
63 |
+
}
|
64 |
+
},
|
65 |
+
'sources':
|
66 |
+
{
|
67 |
+
'fan_68_5':
|
68 |
+
{
|
69 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/fan_68_5.onnx',
|
70 |
+
'path': resolve_relative_path('../.assets/models/fan_68_5.onnx')
|
71 |
+
}
|
72 |
+
}
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
+
|
77 |
+
def get_inference_pool() -> InferencePool:
|
78 |
+
_, model_sources = collect_model_downloads()
|
79 |
+
model_context = __name__ + '.' + state_manager.get_item('face_landmarker_model')
|
80 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
81 |
+
|
82 |
+
|
83 |
+
def clear_inference_pool() -> None:
|
84 |
+
model_context = __name__ + '.' + state_manager.get_item('face_landmarker_model')
|
85 |
+
inference_manager.clear_inference_pool(model_context)
|
86 |
+
|
87 |
+
|
88 |
+
def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]:
|
89 |
+
model_hashes =\
|
90 |
+
{
|
91 |
+
'fan_68_5': MODEL_SET.get('fan_68_5').get('hashes').get('fan_68_5')
|
92 |
+
}
|
93 |
+
model_sources =\
|
94 |
+
{
|
95 |
+
'fan_68_5': MODEL_SET.get('fan_68_5').get('sources').get('fan_68_5')
|
96 |
+
}
|
97 |
+
|
98 |
+
if state_manager.get_item('face_landmarker_model') in [ 'many', '2dfan4' ]:
|
99 |
+
model_hashes['2dfan4'] = MODEL_SET.get('2dfan4').get('hashes').get('2dfan4')
|
100 |
+
model_sources['2dfan4'] = MODEL_SET.get('2dfan4').get('sources').get('2dfan4')
|
101 |
+
if state_manager.get_item('face_landmarker_model') in [ 'many', 'peppa_wutz' ]:
|
102 |
+
model_hashes['peppa_wutz'] = MODEL_SET.get('peppa_wutz').get('hashes').get('peppa_wutz')
|
103 |
+
model_sources['peppa_wutz'] = MODEL_SET.get('peppa_wutz').get('sources').get('peppa_wutz')
|
104 |
+
return model_hashes, model_sources
|
105 |
+
|
106 |
+
|
107 |
+
def pre_check() -> bool:
|
108 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
109 |
+
model_hashes, model_sources = collect_model_downloads()
|
110 |
+
|
111 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
112 |
+
|
113 |
+
|
114 |
+
def detect_face_landmarks(vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]:
|
115 |
+
face_landmark_2dfan4 = None
|
116 |
+
face_landmark_peppa_wutz = None
|
117 |
+
face_landmark_score_2dfan4 = 0.0
|
118 |
+
face_landmark_score_peppa_wutz = 0.0
|
119 |
+
|
120 |
+
if state_manager.get_item('face_landmarker_model') in [ 'many', '2dfan4' ]:
|
121 |
+
face_landmark_2dfan4, face_landmark_score_2dfan4 = detect_with_2dfan4(vision_frame, bounding_box, face_angle)
|
122 |
+
if state_manager.get_item('face_landmarker_model') in [ 'many', 'peppa_wutz' ]:
|
123 |
+
face_landmark_peppa_wutz, face_landmark_score_peppa_wutz = detect_with_peppa_wutz(vision_frame, bounding_box, face_angle)
|
124 |
+
|
125 |
+
if face_landmark_score_2dfan4 > face_landmark_score_peppa_wutz - 0.2:
|
126 |
+
return face_landmark_2dfan4, face_landmark_score_2dfan4
|
127 |
+
return face_landmark_peppa_wutz, face_landmark_score_peppa_wutz
|
128 |
+
|
129 |
+
|
130 |
+
def detect_with_2dfan4(temp_vision_frame: VisionFrame, bounding_box: BoundingBox, face_angle: Angle) -> Tuple[FaceLandmark68, Score]:
|
131 |
+
model_size = MODEL_SET.get('2dfan4').get('size')
|
132 |
+
scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None)
|
133 |
+
translation = (model_size[0] - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5
|
134 |
+
rotated_matrix, rotated_size = create_rotated_matrix_and_size(face_angle, model_size)
|
135 |
+
crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, model_size)
|
136 |
+
crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotated_matrix, rotated_size)
|
137 |
+
crop_vision_frame = conditional_optimize_contrast(crop_vision_frame)
|
138 |
+
crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0
|
139 |
+
face_landmark_68, face_heatmap = forward_with_2dfan4(crop_vision_frame)
|
140 |
+
face_landmark_68 = face_landmark_68[:, :, :2][0] / 64 * 256
|
141 |
+
face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotated_matrix))
|
142 |
+
face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix))
|
143 |
+
face_landmark_score_68 = numpy.amax(face_heatmap, axis = (2, 3))
|
144 |
+
face_landmark_score_68 = numpy.mean(face_landmark_score_68)
|
145 |
+
face_landmark_score_68 = numpy.interp(face_landmark_score_68, [ 0, 0.9 ], [ 0, 1 ])
|
146 |
+
return face_landmark_68, face_landmark_score_68
|
147 |
+
|
148 |
+
|
149 |
+
def detect_with_peppa_wutz(temp_vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]:
|
150 |
+
model_size = MODEL_SET.get('peppa_wutz').get('size')
|
151 |
+
scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None)
|
152 |
+
translation = (model_size[0] - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5
|
153 |
+
rotated_matrix, rotated_size = create_rotated_matrix_and_size(face_angle, model_size)
|
154 |
+
crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, model_size)
|
155 |
+
crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotated_matrix, rotated_size)
|
156 |
+
crop_vision_frame = conditional_optimize_contrast(crop_vision_frame)
|
157 |
+
crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0
|
158 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0)
|
159 |
+
prediction = forward_with_peppa_wutz(crop_vision_frame)
|
160 |
+
face_landmark_68 = prediction.reshape(-1, 3)[:, :2] / 64 * model_size[0]
|
161 |
+
face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotated_matrix))
|
162 |
+
face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix))
|
163 |
+
face_landmark_score_68 = prediction.reshape(-1, 3)[:, 2].mean()
|
164 |
+
face_landmark_score_68 = numpy.interp(face_landmark_score_68, [ 0, 0.95 ], [ 0, 1 ])
|
165 |
+
return face_landmark_68, face_landmark_score_68
|
166 |
+
|
167 |
+
|
168 |
+
def conditional_optimize_contrast(crop_vision_frame : VisionFrame) -> VisionFrame:
|
169 |
+
crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_RGB2Lab)
|
170 |
+
if numpy.mean(crop_vision_frame[:, :, 0]) < 30: # type:ignore[arg-type]
|
171 |
+
crop_vision_frame[:, :, 0] = cv2.createCLAHE(clipLimit = 2).apply(crop_vision_frame[:, :, 0])
|
172 |
+
crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_Lab2RGB)
|
173 |
+
return crop_vision_frame
|
174 |
+
|
175 |
+
|
176 |
+
def estimate_face_landmark_68_5(face_landmark_5 : FaceLandmark5) -> FaceLandmark68:
|
177 |
+
affine_matrix = estimate_matrix_by_face_landmark_5(face_landmark_5, 'ffhq_512', (1, 1))
|
178 |
+
face_landmark_5 = cv2.transform(face_landmark_5.reshape(1, -1, 2), affine_matrix).reshape(-1, 2)
|
179 |
+
face_landmark_68_5 = forward_fan_68_5(face_landmark_5)
|
180 |
+
face_landmark_68_5 = cv2.transform(face_landmark_68_5.reshape(1, -1, 2), cv2.invertAffineTransform(affine_matrix)).reshape(-1, 2)
|
181 |
+
return face_landmark_68_5
|
182 |
+
|
183 |
+
|
184 |
+
def forward_with_2dfan4(crop_vision_frame : VisionFrame) -> Tuple[Prediction, Prediction]:
|
185 |
+
face_landmarker = get_inference_pool().get('2dfan4')
|
186 |
+
|
187 |
+
with conditional_thread_semaphore():
|
188 |
+
prediction = face_landmarker.run(None,
|
189 |
+
{
|
190 |
+
'input': [ crop_vision_frame ]
|
191 |
+
})
|
192 |
+
|
193 |
+
return prediction
|
194 |
+
|
195 |
+
|
196 |
+
def forward_with_peppa_wutz(crop_vision_frame : VisionFrame) -> Prediction:
|
197 |
+
face_landmarker = get_inference_pool().get('peppa_wutz')
|
198 |
+
|
199 |
+
with conditional_thread_semaphore():
|
200 |
+
prediction = face_landmarker.run(None,
|
201 |
+
{
|
202 |
+
'input': crop_vision_frame
|
203 |
+
})[0]
|
204 |
+
|
205 |
+
return prediction
|
206 |
+
|
207 |
+
|
208 |
+
def forward_fan_68_5(face_landmark_5 : FaceLandmark5) -> FaceLandmark68:
|
209 |
+
face_landmarker = get_inference_pool().get('fan_68_5')
|
210 |
+
|
211 |
+
with conditional_thread_semaphore():
|
212 |
+
face_landmark_68_5 = face_landmarker.run(None,
|
213 |
+
{
|
214 |
+
'input': [ face_landmark_5 ]
|
215 |
+
})[0][0]
|
216 |
+
|
217 |
+
return face_landmark_68_5
|
facefusion/face_masker.py
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
from typing import Dict, List, Tuple
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
from cv2.typing import Size
|
7 |
+
|
8 |
+
from facefusion import inference_manager
|
9 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
10 |
+
from facefusion.filesystem import resolve_relative_path
|
11 |
+
from facefusion.thread_helper import conditional_thread_semaphore
|
12 |
+
from facefusion.typing import DownloadSet, FaceLandmark68, FaceMaskRegion, InferencePool, Mask, ModelSet, Padding, VisionFrame
|
13 |
+
|
14 |
+
MODEL_SET : ModelSet =\
|
15 |
+
{
|
16 |
+
'face_occluder':
|
17 |
+
{
|
18 |
+
'hashes':
|
19 |
+
{
|
20 |
+
'face_occluder':
|
21 |
+
{
|
22 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/dfl_xseg.hash',
|
23 |
+
'path': resolve_relative_path('../.assets/models/dfl_xseg.hash')
|
24 |
+
}
|
25 |
+
},
|
26 |
+
'sources':
|
27 |
+
{
|
28 |
+
'face_occluder':
|
29 |
+
{
|
30 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/dfl_xseg.onnx',
|
31 |
+
'path': resolve_relative_path('../.assets/models/dfl_xseg.onnx')
|
32 |
+
}
|
33 |
+
},
|
34 |
+
'size': (256, 256)
|
35 |
+
},
|
36 |
+
'face_parser':
|
37 |
+
{
|
38 |
+
'hashes':
|
39 |
+
{
|
40 |
+
'face_parser':
|
41 |
+
{
|
42 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/bisenet_resnet_34.hash',
|
43 |
+
'path': resolve_relative_path('../.assets/models/bisenet_resnet_34.hash')
|
44 |
+
}
|
45 |
+
},
|
46 |
+
'sources':
|
47 |
+
{
|
48 |
+
'face_parser':
|
49 |
+
{
|
50 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/bisenet_resnet_34.onnx',
|
51 |
+
'path': resolve_relative_path('../.assets/models/bisenet_resnet_34.onnx')
|
52 |
+
}
|
53 |
+
},
|
54 |
+
'size': (512, 512)
|
55 |
+
}
|
56 |
+
}
|
57 |
+
FACE_MASK_REGIONS : Dict[FaceMaskRegion, int] =\
|
58 |
+
{
|
59 |
+
'skin': 1,
|
60 |
+
'left-eyebrow': 2,
|
61 |
+
'right-eyebrow': 3,
|
62 |
+
'left-eye': 4,
|
63 |
+
'right-eye': 5,
|
64 |
+
'glasses': 6,
|
65 |
+
'nose': 10,
|
66 |
+
'mouth': 11,
|
67 |
+
'upper-lip': 12,
|
68 |
+
'lower-lip': 13
|
69 |
+
}
|
70 |
+
|
71 |
+
|
72 |
+
def get_inference_pool() -> InferencePool:
|
73 |
+
_, model_sources = collect_model_downloads()
|
74 |
+
return inference_manager.get_inference_pool(__name__, model_sources)
|
75 |
+
|
76 |
+
|
77 |
+
def clear_inference_pool() -> None:
|
78 |
+
inference_manager.clear_inference_pool(__name__)
|
79 |
+
|
80 |
+
|
81 |
+
def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]:
|
82 |
+
model_hashes =\
|
83 |
+
{
|
84 |
+
'face_occluder': MODEL_SET.get('face_occluder').get('hashes').get('face_occluder'),
|
85 |
+
'face_parser': MODEL_SET.get('face_parser').get('hashes').get('face_parser')
|
86 |
+
}
|
87 |
+
model_sources =\
|
88 |
+
{
|
89 |
+
'face_occluder': MODEL_SET.get('face_occluder').get('sources').get('face_occluder'),
|
90 |
+
'face_parser': MODEL_SET.get('face_parser').get('sources').get('face_parser')
|
91 |
+
}
|
92 |
+
return model_hashes, model_sources
|
93 |
+
|
94 |
+
|
95 |
+
def pre_check() -> bool:
|
96 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
97 |
+
model_hashes, model_sources = collect_model_downloads()
|
98 |
+
|
99 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
100 |
+
|
101 |
+
|
102 |
+
@lru_cache(maxsize = None)
|
103 |
+
def create_static_box_mask(crop_size : Size, face_mask_blur : float, face_mask_padding : Padding) -> Mask:
|
104 |
+
blur_amount = int(crop_size[0] * 0.5 * face_mask_blur)
|
105 |
+
blur_area = max(blur_amount // 2, 1)
|
106 |
+
box_mask : Mask = numpy.ones(crop_size).astype(numpy.float32)
|
107 |
+
box_mask[:max(blur_area, int(crop_size[1] * face_mask_padding[0] / 100)), :] = 0
|
108 |
+
box_mask[-max(blur_area, int(crop_size[1] * face_mask_padding[2] / 100)):, :] = 0
|
109 |
+
box_mask[:, :max(blur_area, int(crop_size[0] * face_mask_padding[3] / 100))] = 0
|
110 |
+
box_mask[:, -max(blur_area, int(crop_size[0] * face_mask_padding[1] / 100)):] = 0
|
111 |
+
if blur_amount > 0:
|
112 |
+
box_mask = cv2.GaussianBlur(box_mask, (0, 0), blur_amount * 0.25)
|
113 |
+
return box_mask
|
114 |
+
|
115 |
+
|
116 |
+
def create_occlusion_mask(crop_vision_frame : VisionFrame) -> Mask:
|
117 |
+
model_size = MODEL_SET.get('face_occluder').get('size')
|
118 |
+
prepare_vision_frame = cv2.resize(crop_vision_frame, model_size)
|
119 |
+
prepare_vision_frame = numpy.expand_dims(prepare_vision_frame, axis = 0).astype(numpy.float32) / 255
|
120 |
+
prepare_vision_frame = prepare_vision_frame.transpose(0, 1, 2, 3)
|
121 |
+
occlusion_mask = forward_occlude_face(prepare_vision_frame)
|
122 |
+
occlusion_mask = occlusion_mask.transpose(0, 1, 2).clip(0, 1).astype(numpy.float32)
|
123 |
+
occlusion_mask = cv2.resize(occlusion_mask, crop_vision_frame.shape[:2][::-1])
|
124 |
+
occlusion_mask = (cv2.GaussianBlur(occlusion_mask.clip(0, 1), (0, 0), 5).clip(0.5, 1) - 0.5) * 2
|
125 |
+
return occlusion_mask
|
126 |
+
|
127 |
+
|
128 |
+
def create_region_mask(crop_vision_frame : VisionFrame, face_mask_regions : List[FaceMaskRegion]) -> Mask:
|
129 |
+
model_size = MODEL_SET.get('face_parser').get('size')
|
130 |
+
prepare_vision_frame = cv2.resize(crop_vision_frame, model_size)
|
131 |
+
prepare_vision_frame = prepare_vision_frame[:, :, ::-1].astype(numpy.float32) / 255
|
132 |
+
prepare_vision_frame = numpy.subtract(prepare_vision_frame, numpy.array([ 0.485, 0.456, 0.406 ]).astype(numpy.float32))
|
133 |
+
prepare_vision_frame = numpy.divide(prepare_vision_frame, numpy.array([ 0.229, 0.224, 0.225 ]).astype(numpy.float32))
|
134 |
+
prepare_vision_frame = numpy.expand_dims(prepare_vision_frame, axis = 0)
|
135 |
+
prepare_vision_frame = prepare_vision_frame.transpose(0, 3, 1, 2)
|
136 |
+
region_mask = forward_parse_face(prepare_vision_frame)
|
137 |
+
region_mask = numpy.isin(region_mask.argmax(0), [ FACE_MASK_REGIONS[region] for region in face_mask_regions ])
|
138 |
+
region_mask = cv2.resize(region_mask.astype(numpy.float32), crop_vision_frame.shape[:2][::-1])
|
139 |
+
region_mask = (cv2.GaussianBlur(region_mask.clip(0, 1), (0, 0), 5).clip(0.5, 1) - 0.5) * 2
|
140 |
+
return region_mask
|
141 |
+
|
142 |
+
|
143 |
+
def create_mouth_mask(face_landmark_68 : FaceLandmark68) -> Mask:
|
144 |
+
convex_hull = cv2.convexHull(face_landmark_68[numpy.r_[3:14, 31:36]].astype(numpy.int32))
|
145 |
+
mouth_mask : Mask = numpy.zeros((512, 512)).astype(numpy.float32)
|
146 |
+
mouth_mask = cv2.fillConvexPoly(mouth_mask, convex_hull, 1.0) #type:ignore[call-overload]
|
147 |
+
mouth_mask = cv2.erode(mouth_mask.clip(0, 1), numpy.ones((21, 3)))
|
148 |
+
mouth_mask = cv2.GaussianBlur(mouth_mask, (0, 0), sigmaX = 1, sigmaY = 15)
|
149 |
+
return mouth_mask
|
150 |
+
|
151 |
+
|
152 |
+
def forward_occlude_face(prepare_vision_frame : VisionFrame) -> Mask:
|
153 |
+
face_occluder = get_inference_pool().get('face_occluder')
|
154 |
+
|
155 |
+
with conditional_thread_semaphore():
|
156 |
+
occlusion_mask : Mask = face_occluder.run(None,
|
157 |
+
{
|
158 |
+
'input': prepare_vision_frame
|
159 |
+
})[0][0]
|
160 |
+
|
161 |
+
return occlusion_mask
|
162 |
+
|
163 |
+
|
164 |
+
def forward_parse_face(prepare_vision_frame : VisionFrame) -> Mask:
|
165 |
+
face_parser = get_inference_pool().get('face_parser')
|
166 |
+
|
167 |
+
with conditional_thread_semaphore():
|
168 |
+
region_mask : Mask = face_parser.run(None,
|
169 |
+
{
|
170 |
+
'input': prepare_vision_frame
|
171 |
+
})[0][0]
|
172 |
+
|
173 |
+
return region_mask
|
facefusion/face_recognizer.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Tuple
|
2 |
+
|
3 |
+
import numpy
|
4 |
+
|
5 |
+
from facefusion import inference_manager
|
6 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
7 |
+
from facefusion.face_helper import warp_face_by_face_landmark_5
|
8 |
+
from facefusion.filesystem import resolve_relative_path
|
9 |
+
from facefusion.thread_helper import conditional_thread_semaphore
|
10 |
+
from facefusion.typing import Embedding, FaceLandmark5, InferencePool, ModelOptions, ModelSet, VisionFrame
|
11 |
+
|
12 |
+
MODEL_SET : ModelSet =\
|
13 |
+
{
|
14 |
+
'arcface':
|
15 |
+
{
|
16 |
+
'hashes':
|
17 |
+
{
|
18 |
+
'face_recognizer':
|
19 |
+
{
|
20 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.hash',
|
21 |
+
'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash')
|
22 |
+
}
|
23 |
+
},
|
24 |
+
'sources':
|
25 |
+
{
|
26 |
+
'face_recognizer':
|
27 |
+
{
|
28 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx',
|
29 |
+
'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx')
|
30 |
+
}
|
31 |
+
},
|
32 |
+
'template': 'arcface_112_v2',
|
33 |
+
'size': (112, 112)
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
|
38 |
+
def get_inference_pool() -> InferencePool:
|
39 |
+
model_sources = get_model_options().get('sources')
|
40 |
+
return inference_manager.get_inference_pool(__name__, model_sources)
|
41 |
+
|
42 |
+
|
43 |
+
def clear_inference_pool() -> None:
|
44 |
+
inference_manager.clear_inference_pool(__name__)
|
45 |
+
|
46 |
+
|
47 |
+
def get_model_options() -> ModelOptions:
|
48 |
+
return MODEL_SET.get('arcface')
|
49 |
+
|
50 |
+
|
51 |
+
def pre_check() -> bool:
|
52 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
53 |
+
model_hashes = get_model_options().get('hashes')
|
54 |
+
model_sources = get_model_options().get('sources')
|
55 |
+
|
56 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
57 |
+
|
58 |
+
|
59 |
+
def calc_embedding(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5) -> Tuple[Embedding, Embedding]:
|
60 |
+
model_template = get_model_options().get('template')
|
61 |
+
model_size = get_model_options().get('size')
|
62 |
+
crop_vision_frame, matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, model_size)
|
63 |
+
crop_vision_frame = crop_vision_frame / 127.5 - 1
|
64 |
+
crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32)
|
65 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0)
|
66 |
+
embedding = forward(crop_vision_frame)
|
67 |
+
embedding = embedding.ravel()
|
68 |
+
normed_embedding = embedding / numpy.linalg.norm(embedding)
|
69 |
+
return embedding, normed_embedding
|
70 |
+
|
71 |
+
|
72 |
+
def forward(crop_vision_frame : VisionFrame) -> Embedding:
|
73 |
+
face_recognizer = get_inference_pool().get('face_recognizer')
|
74 |
+
|
75 |
+
with conditional_thread_semaphore():
|
76 |
+
embedding = face_recognizer.run(None,
|
77 |
+
{
|
78 |
+
'input': crop_vision_frame
|
79 |
+
})[0]
|
80 |
+
|
81 |
+
return embedding
|
facefusion/face_selector.py
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
|
3 |
+
import numpy
|
4 |
+
|
5 |
+
from facefusion import state_manager
|
6 |
+
from facefusion.typing import Face, FaceSelectorOrder, FaceSet, Gender, Race
|
7 |
+
|
8 |
+
|
9 |
+
def find_similar_faces(faces : List[Face], reference_faces : FaceSet, face_distance : float) -> List[Face]:
|
10 |
+
similar_faces : List[Face] = []
|
11 |
+
|
12 |
+
if faces and reference_faces:
|
13 |
+
for reference_set in reference_faces:
|
14 |
+
if not similar_faces:
|
15 |
+
for reference_face in reference_faces[reference_set]:
|
16 |
+
for face in faces:
|
17 |
+
if compare_faces(face, reference_face, face_distance):
|
18 |
+
similar_faces.append(face)
|
19 |
+
return similar_faces
|
20 |
+
|
21 |
+
|
22 |
+
def compare_faces(face : Face, reference_face : Face, face_distance : float) -> bool:
|
23 |
+
current_face_distance = calc_face_distance(face, reference_face)
|
24 |
+
return current_face_distance < face_distance
|
25 |
+
|
26 |
+
|
27 |
+
def calc_face_distance(face : Face, reference_face : Face) -> float:
|
28 |
+
if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'):
|
29 |
+
return 1 - numpy.dot(face.normed_embedding, reference_face.normed_embedding)
|
30 |
+
return 0
|
31 |
+
|
32 |
+
|
33 |
+
def sort_and_filter_faces(faces : List[Face]) -> List[Face]:
|
34 |
+
if faces:
|
35 |
+
if state_manager.get_item('face_selector_order'):
|
36 |
+
faces = sort_by_order(faces, state_manager.get_item('face_selector_order'))
|
37 |
+
if state_manager.get_item('face_selector_gender'):
|
38 |
+
faces = filter_by_gender(faces, state_manager.get_item('face_selector_gender'))
|
39 |
+
if state_manager.get_item('face_selector_race'):
|
40 |
+
faces = filter_by_race(faces, state_manager.get_item('face_selector_race'))
|
41 |
+
if state_manager.get_item('face_selector_age_start') or state_manager.get_item('face_selector_age_end'):
|
42 |
+
faces = filter_by_age(faces, state_manager.get_item('face_selector_age_start'), state_manager.get_item('face_selector_age_end'))
|
43 |
+
return faces
|
44 |
+
|
45 |
+
|
46 |
+
def sort_by_order(faces : List[Face], order : FaceSelectorOrder) -> List[Face]:
|
47 |
+
if order == 'left-right':
|
48 |
+
return sorted(faces, key = lambda face: face.bounding_box[0])
|
49 |
+
if order == 'right-left':
|
50 |
+
return sorted(faces, key = lambda face: face.bounding_box[0], reverse = True)
|
51 |
+
if order == 'top-bottom':
|
52 |
+
return sorted(faces, key = lambda face: face.bounding_box[1])
|
53 |
+
if order == 'bottom-top':
|
54 |
+
return sorted(faces, key = lambda face: face.bounding_box[1], reverse = True)
|
55 |
+
if order == 'small-large':
|
56 |
+
return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1]))
|
57 |
+
if order == 'large-small':
|
58 |
+
return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1]), reverse = True)
|
59 |
+
if order == 'best-worst':
|
60 |
+
return sorted(faces, key = lambda face: face.score_set.get('detector'), reverse = True)
|
61 |
+
if order == 'worst-best':
|
62 |
+
return sorted(faces, key = lambda face: face.score_set.get('detector'))
|
63 |
+
return faces
|
64 |
+
|
65 |
+
|
66 |
+
def filter_by_gender(faces : List[Face], gender : Gender) -> List[Face]:
|
67 |
+
filter_faces = []
|
68 |
+
|
69 |
+
for face in faces:
|
70 |
+
if face.gender == gender:
|
71 |
+
filter_faces.append(face)
|
72 |
+
return filter_faces
|
73 |
+
|
74 |
+
|
75 |
+
def filter_by_age(faces : List[Face], face_selector_age_start : int, face_selector_age_end : int) -> List[Face]:
|
76 |
+
filter_faces = []
|
77 |
+
age = range(face_selector_age_start, face_selector_age_end)
|
78 |
+
|
79 |
+
for face in faces:
|
80 |
+
if set(face.age) & set(age):
|
81 |
+
filter_faces.append(face)
|
82 |
+
return filter_faces
|
83 |
+
|
84 |
+
|
85 |
+
def filter_by_race(faces : List[Face], race : Race) -> List[Face]:
|
86 |
+
filter_faces = []
|
87 |
+
|
88 |
+
for face in faces:
|
89 |
+
if face.race == race:
|
90 |
+
filter_faces.append(face)
|
91 |
+
return filter_faces
|
facefusion/face_store.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import hashlib
|
2 |
+
from typing import List, Optional
|
3 |
+
|
4 |
+
import numpy
|
5 |
+
|
6 |
+
from facefusion.typing import Face, FaceSet, FaceStore, VisionFrame
|
7 |
+
|
8 |
+
FACE_STORE : FaceStore =\
|
9 |
+
{
|
10 |
+
'static_faces': {},
|
11 |
+
'reference_faces': {}
|
12 |
+
}
|
13 |
+
|
14 |
+
|
15 |
+
def get_face_store() -> FaceStore:
|
16 |
+
return FACE_STORE
|
17 |
+
|
18 |
+
|
19 |
+
def get_static_faces(vision_frame : VisionFrame) -> Optional[List[Face]]:
|
20 |
+
frame_hash = create_frame_hash(vision_frame)
|
21 |
+
if frame_hash in FACE_STORE['static_faces']:
|
22 |
+
return FACE_STORE['static_faces'][frame_hash]
|
23 |
+
return None
|
24 |
+
|
25 |
+
|
26 |
+
def set_static_faces(vision_frame : VisionFrame, faces : List[Face]) -> None:
|
27 |
+
frame_hash = create_frame_hash(vision_frame)
|
28 |
+
if frame_hash:
|
29 |
+
FACE_STORE['static_faces'][frame_hash] = faces
|
30 |
+
|
31 |
+
|
32 |
+
def clear_static_faces() -> None:
|
33 |
+
FACE_STORE['static_faces'] = {}
|
34 |
+
|
35 |
+
|
36 |
+
def create_frame_hash(vision_frame : VisionFrame) -> Optional[str]:
|
37 |
+
return hashlib.sha1(vision_frame.tobytes()).hexdigest() if numpy.any(vision_frame) else None
|
38 |
+
|
39 |
+
|
40 |
+
def get_reference_faces() -> Optional[FaceSet]:
|
41 |
+
if FACE_STORE['reference_faces']:
|
42 |
+
return FACE_STORE['reference_faces']
|
43 |
+
return None
|
44 |
+
|
45 |
+
|
46 |
+
def append_reference_face(name : str, face : Face) -> None:
|
47 |
+
if name not in FACE_STORE['reference_faces']:
|
48 |
+
FACE_STORE['reference_faces'][name] = []
|
49 |
+
FACE_STORE['reference_faces'][name].append(face)
|
50 |
+
|
51 |
+
|
52 |
+
def clear_reference_faces() -> None:
|
53 |
+
FACE_STORE['reference_faces'] = {}
|
facefusion/ffmpeg.py
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
import subprocess
|
4 |
+
import tempfile
|
5 |
+
from typing import List, Optional
|
6 |
+
|
7 |
+
import filetype
|
8 |
+
|
9 |
+
from facefusion import logger, process_manager, state_manager
|
10 |
+
from facefusion.filesystem import remove_file
|
11 |
+
from facefusion.temp_helper import get_temp_file_path, get_temp_frames_pattern
|
12 |
+
from facefusion.typing import AudioBuffer, Fps, OutputVideoPreset
|
13 |
+
from facefusion.vision import restrict_video_fps
|
14 |
+
|
15 |
+
|
16 |
+
def run_ffmpeg(args : List[str]) -> subprocess.Popen[bytes]:
|
17 |
+
commands = [ shutil.which('ffmpeg'), '-hide_banner', '-loglevel', 'error' ]
|
18 |
+
commands.extend(args)
|
19 |
+
process = subprocess.Popen(commands, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
|
20 |
+
|
21 |
+
while process_manager.is_processing():
|
22 |
+
try:
|
23 |
+
if state_manager.get_item('log_level') == 'debug':
|
24 |
+
log_debug(process)
|
25 |
+
process.wait(timeout = 0.5)
|
26 |
+
except subprocess.TimeoutExpired:
|
27 |
+
continue
|
28 |
+
return process
|
29 |
+
|
30 |
+
if process_manager.is_stopping():
|
31 |
+
process.terminate()
|
32 |
+
return process
|
33 |
+
|
34 |
+
|
35 |
+
def open_ffmpeg(args : List[str]) -> subprocess.Popen[bytes]:
|
36 |
+
commands = [ shutil.which('ffmpeg'), '-hide_banner', '-loglevel', 'quiet' ]
|
37 |
+
commands.extend(args)
|
38 |
+
return subprocess.Popen(commands, stdin = subprocess.PIPE, stdout = subprocess.PIPE)
|
39 |
+
|
40 |
+
|
41 |
+
def log_debug(process : subprocess.Popen[bytes]) -> None:
|
42 |
+
_, stderr = process.communicate()
|
43 |
+
errors = stderr.decode().split(os.linesep)
|
44 |
+
|
45 |
+
for error in errors:
|
46 |
+
if error.strip():
|
47 |
+
logger.debug(error.strip(), __name__)
|
48 |
+
|
49 |
+
|
50 |
+
def extract_frames(target_path : str, temp_video_resolution : str, temp_video_fps : Fps) -> bool:
|
51 |
+
trim_frame_start = state_manager.get_item('trim_frame_start')
|
52 |
+
trim_frame_end = state_manager.get_item('trim_frame_end')
|
53 |
+
temp_frames_pattern = get_temp_frames_pattern(target_path, '%08d')
|
54 |
+
commands = [ '-i', target_path, '-s', str(temp_video_resolution), '-q:v', '0' ]
|
55 |
+
|
56 |
+
if isinstance(trim_frame_start, int) and isinstance(trim_frame_end, int):
|
57 |
+
commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ':end_frame=' + str(trim_frame_end) + ',fps=' + str(temp_video_fps) ])
|
58 |
+
elif isinstance(trim_frame_start, int):
|
59 |
+
commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ',fps=' + str(temp_video_fps) ])
|
60 |
+
elif isinstance(trim_frame_end, int):
|
61 |
+
commands.extend([ '-vf', 'trim=end_frame=' + str(trim_frame_end) + ',fps=' + str(temp_video_fps) ])
|
62 |
+
else:
|
63 |
+
commands.extend([ '-vf', 'fps=' + str(temp_video_fps) ])
|
64 |
+
commands.extend([ '-vsync', '0', temp_frames_pattern ])
|
65 |
+
return run_ffmpeg(commands).returncode == 0
|
66 |
+
|
67 |
+
|
68 |
+
def merge_video(target_path : str, output_video_resolution : str, output_video_fps : Fps) -> bool:
|
69 |
+
temp_video_fps = restrict_video_fps(target_path, output_video_fps)
|
70 |
+
temp_file_path = get_temp_file_path(target_path)
|
71 |
+
temp_frames_pattern = get_temp_frames_pattern(target_path, '%08d')
|
72 |
+
commands = [ '-r', str(temp_video_fps), '-i', temp_frames_pattern, '-s', str(output_video_resolution), '-c:v', state_manager.get_item('output_video_encoder') ]
|
73 |
+
|
74 |
+
if state_manager.get_item('output_video_encoder') in [ 'libx264', 'libx265' ]:
|
75 |
+
output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51))
|
76 |
+
commands.extend([ '-crf', str(output_video_compression), '-preset', state_manager.get_item('output_video_preset') ])
|
77 |
+
if state_manager.get_item('output_video_encoder') in [ 'libvpx-vp9' ]:
|
78 |
+
output_video_compression = round(63 - (state_manager.get_item('output_video_quality') * 0.63))
|
79 |
+
commands.extend([ '-crf', str(output_video_compression) ])
|
80 |
+
if state_manager.get_item('output_video_encoder') in [ 'h264_nvenc', 'hevc_nvenc' ]:
|
81 |
+
output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51))
|
82 |
+
commands.extend([ '-cq', str(output_video_compression), '-preset', map_nvenc_preset(state_manager.get_item('output_video_preset')) ])
|
83 |
+
if state_manager.get_item('output_video_encoder') in [ 'h264_amf', 'hevc_amf' ]:
|
84 |
+
output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51))
|
85 |
+
commands.extend([ '-qp_i', str(output_video_compression), '-qp_p', str(output_video_compression), '-quality', map_amf_preset(state_manager.get_item('output_video_preset')) ])
|
86 |
+
if state_manager.get_item('output_video_encoder') in [ 'h264_videotoolbox', 'hevc_videotoolbox' ]:
|
87 |
+
commands.extend([ '-q:v', str(state_manager.get_item('output_video_quality')) ])
|
88 |
+
commands.extend([ '-vf', 'framerate=fps=' + str(output_video_fps), '-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-y', temp_file_path ])
|
89 |
+
return run_ffmpeg(commands).returncode == 0
|
90 |
+
|
91 |
+
|
92 |
+
def concat_video(output_path : str, temp_output_paths : List[str]) -> bool:
|
93 |
+
concat_video_path = tempfile.mktemp()
|
94 |
+
|
95 |
+
with open(concat_video_path, 'w') as concat_video_file:
|
96 |
+
for temp_output_path in temp_output_paths:
|
97 |
+
concat_video_file.write('file \'' + os.path.abspath(temp_output_path) + '\'' + os.linesep)
|
98 |
+
concat_video_file.flush()
|
99 |
+
concat_video_file.close()
|
100 |
+
commands = [ '-f', 'concat', '-safe', '0', '-i', concat_video_file.name, '-c:v', 'copy', '-c:a', state_manager.get_item('output_audio_encoder'), '-y', os.path.abspath(output_path) ]
|
101 |
+
process = run_ffmpeg(commands)
|
102 |
+
process.communicate()
|
103 |
+
remove_file(concat_video_path)
|
104 |
+
return process.returncode == 0
|
105 |
+
|
106 |
+
|
107 |
+
def copy_image(target_path : str, temp_image_resolution : str) -> bool:
|
108 |
+
temp_file_path = get_temp_file_path(target_path)
|
109 |
+
temp_image_compression = calc_image_compression(target_path, 100)
|
110 |
+
commands = [ '-i', target_path, '-s', str(temp_image_resolution), '-q:v', str(temp_image_compression), '-y', temp_file_path ]
|
111 |
+
return run_ffmpeg(commands).returncode == 0
|
112 |
+
|
113 |
+
|
114 |
+
def finalize_image(target_path : str, output_path : str, output_image_resolution : str) -> bool:
|
115 |
+
temp_file_path = get_temp_file_path(target_path)
|
116 |
+
output_image_compression = calc_image_compression(target_path, state_manager.get_item('output_image_quality'))
|
117 |
+
commands = [ '-i', temp_file_path, '-s', str(output_image_resolution), '-q:v', str(output_image_compression), '-y', output_path ]
|
118 |
+
return run_ffmpeg(commands).returncode == 0
|
119 |
+
|
120 |
+
|
121 |
+
def calc_image_compression(image_path : str, image_quality : int) -> int:
|
122 |
+
is_webp = filetype.guess_mime(image_path) == 'image/webp'
|
123 |
+
if is_webp:
|
124 |
+
image_quality = 100 - image_quality
|
125 |
+
return round(31 - (image_quality * 0.31))
|
126 |
+
|
127 |
+
|
128 |
+
def read_audio_buffer(target_path : str, sample_rate : int, channel_total : int) -> Optional[AudioBuffer]:
|
129 |
+
commands = [ '-i', target_path, '-vn', '-f', 's16le', '-acodec', 'pcm_s16le', '-ar', str(sample_rate), '-ac', str(channel_total), '-' ]
|
130 |
+
process = open_ffmpeg(commands)
|
131 |
+
audio_buffer, _ = process.communicate()
|
132 |
+
if process.returncode == 0:
|
133 |
+
return audio_buffer
|
134 |
+
return None
|
135 |
+
|
136 |
+
|
137 |
+
def restore_audio(target_path : str, output_path : str, output_video_fps : Fps) -> bool:
|
138 |
+
trim_frame_start = state_manager.get_item('trim_frame_start')
|
139 |
+
trim_frame_end = state_manager.get_item('trim_frame_end')
|
140 |
+
temp_file_path = get_temp_file_path(target_path)
|
141 |
+
commands = [ '-i', temp_file_path ]
|
142 |
+
|
143 |
+
if isinstance(trim_frame_start, int):
|
144 |
+
start_time = trim_frame_start / output_video_fps
|
145 |
+
commands.extend([ '-ss', str(start_time) ])
|
146 |
+
if isinstance(trim_frame_end, int):
|
147 |
+
end_time = trim_frame_end / output_video_fps
|
148 |
+
commands.extend([ '-to', str(end_time) ])
|
149 |
+
commands.extend([ '-i', target_path, '-c:v', 'copy', '-c:a', state_manager.get_item('output_audio_encoder'), '-map', '0:v:0', '-map', '1:a:0', '-shortest', '-y', output_path ])
|
150 |
+
return run_ffmpeg(commands).returncode == 0
|
151 |
+
|
152 |
+
|
153 |
+
def replace_audio(target_path : str, audio_path : str, output_path : str) -> bool:
|
154 |
+
temp_file_path = get_temp_file_path(target_path)
|
155 |
+
commands = [ '-i', temp_file_path, '-i', audio_path, '-c:a', state_manager.get_item('output_audio_encoder'), '-af', 'apad', '-shortest', '-y', output_path ]
|
156 |
+
return run_ffmpeg(commands).returncode == 0
|
157 |
+
|
158 |
+
|
159 |
+
def map_nvenc_preset(output_video_preset : OutputVideoPreset) -> Optional[str]:
|
160 |
+
if output_video_preset in [ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast' ]:
|
161 |
+
return 'fast'
|
162 |
+
if output_video_preset == 'medium':
|
163 |
+
return 'medium'
|
164 |
+
if output_video_preset in [ 'slow', 'slower', 'veryslow' ]:
|
165 |
+
return 'slow'
|
166 |
+
return None
|
167 |
+
|
168 |
+
|
169 |
+
def map_amf_preset(output_video_preset : OutputVideoPreset) -> Optional[str]:
|
170 |
+
if output_video_preset in [ 'ultrafast', 'superfast', 'veryfast' ]:
|
171 |
+
return 'speed'
|
172 |
+
if output_video_preset in [ 'faster', 'fast', 'medium' ]:
|
173 |
+
return 'balanced'
|
174 |
+
if output_video_preset in [ 'slow', 'slower', 'veryslow' ]:
|
175 |
+
return 'quality'
|
176 |
+
return None
|
facefusion/filesystem.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
from pathlib import Path
|
4 |
+
from typing import List, Optional
|
5 |
+
|
6 |
+
import filetype
|
7 |
+
|
8 |
+
from facefusion.common_helper import is_windows
|
9 |
+
|
10 |
+
if is_windows():
|
11 |
+
import ctypes
|
12 |
+
|
13 |
+
|
14 |
+
def get_file_size(file_path : str) -> int:
|
15 |
+
if is_file(file_path):
|
16 |
+
return os.path.getsize(file_path)
|
17 |
+
return 0
|
18 |
+
|
19 |
+
|
20 |
+
def same_file_extension(file_paths : List[str]) -> bool:
|
21 |
+
file_extensions : List[str] = []
|
22 |
+
|
23 |
+
for file_path in file_paths:
|
24 |
+
_, file_extension = os.path.splitext(file_path.lower())
|
25 |
+
|
26 |
+
if file_extensions and file_extension not in file_extensions:
|
27 |
+
return False
|
28 |
+
file_extensions.append(file_extension)
|
29 |
+
return True
|
30 |
+
|
31 |
+
|
32 |
+
def is_file(file_path : str) -> bool:
|
33 |
+
return bool(file_path and os.path.isfile(file_path))
|
34 |
+
|
35 |
+
|
36 |
+
def is_directory(directory_path : str) -> bool:
|
37 |
+
return bool(directory_path and os.path.isdir(directory_path))
|
38 |
+
|
39 |
+
|
40 |
+
def in_directory(file_path : str) -> bool:
|
41 |
+
if file_path and not is_directory(file_path):
|
42 |
+
return is_directory(os.path.dirname(file_path))
|
43 |
+
return False
|
44 |
+
|
45 |
+
|
46 |
+
def is_audio(audio_path : str) -> bool:
|
47 |
+
return is_file(audio_path) and filetype.helpers.is_audio(audio_path)
|
48 |
+
|
49 |
+
|
50 |
+
def has_audio(audio_paths : List[str]) -> bool:
|
51 |
+
if audio_paths:
|
52 |
+
return any(is_audio(audio_path) for audio_path in audio_paths)
|
53 |
+
return False
|
54 |
+
|
55 |
+
|
56 |
+
def is_image(image_path : str) -> bool:
|
57 |
+
return is_file(image_path) and filetype.helpers.is_image(image_path)
|
58 |
+
|
59 |
+
|
60 |
+
def has_image(image_paths: List[str]) -> bool:
|
61 |
+
if image_paths:
|
62 |
+
return any(is_image(image_path) for image_path in image_paths)
|
63 |
+
return False
|
64 |
+
|
65 |
+
|
66 |
+
def is_video(video_path : str) -> bool:
|
67 |
+
return is_file(video_path) and filetype.helpers.is_video(video_path)
|
68 |
+
|
69 |
+
|
70 |
+
def filter_audio_paths(paths : List[str]) -> List[str]:
|
71 |
+
if paths:
|
72 |
+
return [ path for path in paths if is_audio(path) ]
|
73 |
+
return []
|
74 |
+
|
75 |
+
|
76 |
+
def filter_image_paths(paths : List[str]) -> List[str]:
|
77 |
+
if paths:
|
78 |
+
return [ path for path in paths if is_image(path) ]
|
79 |
+
return []
|
80 |
+
|
81 |
+
|
82 |
+
def resolve_relative_path(path : str) -> str:
|
83 |
+
return os.path.abspath(os.path.join(os.path.dirname(__file__), path))
|
84 |
+
|
85 |
+
|
86 |
+
def sanitize_path_for_windows(full_path : str) -> Optional[str]:
|
87 |
+
buffer_size = 0
|
88 |
+
|
89 |
+
while True:
|
90 |
+
unicode_buffer = ctypes.create_unicode_buffer(buffer_size)
|
91 |
+
buffer_limit = ctypes.windll.kernel32.GetShortPathNameW(full_path, unicode_buffer, buffer_size) #type:ignore[attr-defined]
|
92 |
+
|
93 |
+
if buffer_size > buffer_limit:
|
94 |
+
return unicode_buffer.value
|
95 |
+
if buffer_limit == 0:
|
96 |
+
return None
|
97 |
+
buffer_size = buffer_limit
|
98 |
+
|
99 |
+
|
100 |
+
def copy_file(file_path : str, move_path : str) -> bool:
|
101 |
+
if is_file(file_path):
|
102 |
+
shutil.copy(file_path, move_path)
|
103 |
+
return is_file(move_path)
|
104 |
+
return False
|
105 |
+
|
106 |
+
|
107 |
+
def move_file(file_path : str, move_path : str) -> bool:
|
108 |
+
if is_file(file_path):
|
109 |
+
shutil.move(file_path, move_path)
|
110 |
+
return not is_file(file_path) and is_file(move_path)
|
111 |
+
return False
|
112 |
+
|
113 |
+
|
114 |
+
def remove_file(file_path : str) -> bool:
|
115 |
+
if is_file(file_path):
|
116 |
+
os.remove(file_path)
|
117 |
+
return not is_file(file_path)
|
118 |
+
return False
|
119 |
+
|
120 |
+
|
121 |
+
def create_directory(directory_path : str) -> bool:
|
122 |
+
if directory_path and not is_file(directory_path):
|
123 |
+
Path(directory_path).mkdir(parents = True, exist_ok = True)
|
124 |
+
return is_directory(directory_path)
|
125 |
+
return False
|
126 |
+
|
127 |
+
|
128 |
+
def list_directory(directory_path : str) -> Optional[List[str]]:
|
129 |
+
if is_directory(directory_path):
|
130 |
+
files = os.listdir(directory_path)
|
131 |
+
files = [ Path(file).stem for file in files if not Path(file).stem.startswith(('.', '__')) ]
|
132 |
+
return sorted(files)
|
133 |
+
return None
|
134 |
+
|
135 |
+
|
136 |
+
def remove_directory(directory_path : str) -> bool:
|
137 |
+
if is_directory(directory_path):
|
138 |
+
shutil.rmtree(directory_path, ignore_errors = True)
|
139 |
+
return not is_directory(directory_path)
|
140 |
+
return False
|
facefusion/hash_helper.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import zlib
|
3 |
+
from typing import Optional
|
4 |
+
|
5 |
+
from facefusion.filesystem import is_file
|
6 |
+
|
7 |
+
|
8 |
+
def create_hash(content : bytes) -> str:
|
9 |
+
return format(zlib.crc32(content), '08x')
|
10 |
+
|
11 |
+
|
12 |
+
def validate_hash(validate_path : str) -> bool:
|
13 |
+
hash_path = get_hash_path(validate_path)
|
14 |
+
|
15 |
+
if is_file(hash_path):
|
16 |
+
with open(hash_path, 'r') as hash_file:
|
17 |
+
hash_content = hash_file.read().strip()
|
18 |
+
|
19 |
+
with open(validate_path, 'rb') as validate_file:
|
20 |
+
validate_content = validate_file.read()
|
21 |
+
|
22 |
+
return create_hash(validate_content) == hash_content
|
23 |
+
return False
|
24 |
+
|
25 |
+
|
26 |
+
def get_hash_path(validate_path : str) -> Optional[str]:
|
27 |
+
if is_file(validate_path):
|
28 |
+
validate_directory_path, _ = os.path.split(validate_path)
|
29 |
+
validate_file_name, _ = os.path.splitext(_)
|
30 |
+
|
31 |
+
return os.path.join(validate_directory_path, validate_file_name + '.hash')
|
32 |
+
return None
|
facefusion/inference_manager.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import lru_cache
|
2 |
+
from time import sleep
|
3 |
+
from typing import List
|
4 |
+
|
5 |
+
import onnx
|
6 |
+
from onnxruntime import InferenceSession
|
7 |
+
|
8 |
+
from facefusion import process_manager, state_manager
|
9 |
+
from facefusion.app_context import detect_app_context
|
10 |
+
from facefusion.execution import create_execution_providers, has_execution_provider
|
11 |
+
from facefusion.thread_helper import thread_lock
|
12 |
+
from facefusion.typing import DownloadSet, ExecutionProviderKey, InferencePool, InferencePoolSet, ModelInitializer
|
13 |
+
|
14 |
+
INFERENCE_POOLS : InferencePoolSet =\
|
15 |
+
{
|
16 |
+
'cli': {}, # type:ignore[typeddict-item]
|
17 |
+
'ui': {} # type:ignore[typeddict-item]
|
18 |
+
}
|
19 |
+
|
20 |
+
|
21 |
+
def get_inference_pool(model_context : str, model_sources : DownloadSet) -> InferencePool:
|
22 |
+
global INFERENCE_POOLS
|
23 |
+
|
24 |
+
with thread_lock():
|
25 |
+
while process_manager.is_checking():
|
26 |
+
sleep(0.5)
|
27 |
+
app_context = detect_app_context()
|
28 |
+
inference_context = get_inference_context(model_context)
|
29 |
+
|
30 |
+
if app_context == 'cli' and INFERENCE_POOLS.get('ui').get(inference_context):
|
31 |
+
INFERENCE_POOLS['cli'][inference_context] = INFERENCE_POOLS.get('ui').get(inference_context)
|
32 |
+
if app_context == 'ui' and INFERENCE_POOLS.get('cli').get(inference_context):
|
33 |
+
INFERENCE_POOLS['ui'][inference_context] = INFERENCE_POOLS.get('cli').get(inference_context)
|
34 |
+
if not INFERENCE_POOLS.get(app_context).get(inference_context):
|
35 |
+
execution_provider_keys = resolve_execution_provider_keys(model_context)
|
36 |
+
INFERENCE_POOLS[app_context][inference_context] = create_inference_pool(model_sources, state_manager.get_item('execution_device_id'), execution_provider_keys)
|
37 |
+
|
38 |
+
return INFERENCE_POOLS.get(app_context).get(inference_context)
|
39 |
+
|
40 |
+
|
41 |
+
def create_inference_pool(model_sources : DownloadSet, execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> InferencePool:
|
42 |
+
inference_pool : InferencePool = {}
|
43 |
+
|
44 |
+
for model_name in model_sources.keys():
|
45 |
+
inference_pool[model_name] = create_inference_session(model_sources.get(model_name).get('path'), execution_device_id, execution_provider_keys)
|
46 |
+
return inference_pool
|
47 |
+
|
48 |
+
|
49 |
+
def clear_inference_pool(model_context : str) -> None:
|
50 |
+
global INFERENCE_POOLS
|
51 |
+
|
52 |
+
app_context = detect_app_context()
|
53 |
+
inference_context = get_inference_context(model_context)
|
54 |
+
|
55 |
+
if INFERENCE_POOLS.get(app_context).get(inference_context):
|
56 |
+
del INFERENCE_POOLS[app_context][inference_context]
|
57 |
+
|
58 |
+
|
59 |
+
def create_inference_session(model_path : str, execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> InferenceSession:
|
60 |
+
execution_providers = create_execution_providers(execution_device_id, execution_provider_keys)
|
61 |
+
return InferenceSession(model_path, providers = execution_providers)
|
62 |
+
|
63 |
+
|
64 |
+
@lru_cache(maxsize = None)
|
65 |
+
def get_static_model_initializer(model_path : str) -> ModelInitializer:
|
66 |
+
model = onnx.load(model_path)
|
67 |
+
return onnx.numpy_helper.to_array(model.graph.initializer[-1])
|
68 |
+
|
69 |
+
|
70 |
+
def resolve_execution_provider_keys(model_context : str) -> List[ExecutionProviderKey]:
|
71 |
+
if has_execution_provider('coreml') and (model_context.startswith('facefusion.processors.modules.age_modifier') or model_context.startswith('facefusion.processors.modules.frame_colorizer')):
|
72 |
+
return [ 'cpu' ]
|
73 |
+
return state_manager.get_item('execution_providers')
|
74 |
+
|
75 |
+
|
76 |
+
def get_inference_context(model_context : str) -> str:
|
77 |
+
execution_provider_keys = resolve_execution_provider_keys(model_context)
|
78 |
+
inference_context = model_context + '.' + '_'.join(execution_provider_keys)
|
79 |
+
return inference_context
|
facefusion/installer.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
import signal
|
4 |
+
import subprocess
|
5 |
+
import sys
|
6 |
+
import tempfile
|
7 |
+
from argparse import ArgumentParser, HelpFormatter
|
8 |
+
from typing import Dict, Tuple
|
9 |
+
|
10 |
+
from facefusion import metadata, wording
|
11 |
+
from facefusion.common_helper import is_linux, is_macos, is_windows
|
12 |
+
|
13 |
+
ONNXRUNTIMES : Dict[str, Tuple[str, str]] = {}
|
14 |
+
|
15 |
+
if is_macos():
|
16 |
+
ONNXRUNTIMES['default'] = ('onnxruntime', '1.19.2')
|
17 |
+
else:
|
18 |
+
ONNXRUNTIMES['default'] = ('onnxruntime', '1.19.2')
|
19 |
+
ONNXRUNTIMES['cuda'] = ('onnxruntime-gpu', '1.19.2')
|
20 |
+
ONNXRUNTIMES['openvino'] = ('onnxruntime-openvino', '1.19.0')
|
21 |
+
if is_linux():
|
22 |
+
ONNXRUNTIMES['rocm'] = ('onnxruntime-rocm', '1.18.0')
|
23 |
+
if is_windows():
|
24 |
+
ONNXRUNTIMES['directml'] = ('onnxruntime-directml', '1.17.3')
|
25 |
+
|
26 |
+
|
27 |
+
def cli() -> None:
|
28 |
+
signal.signal(signal.SIGINT, lambda signal_number, frame: sys.exit(0))
|
29 |
+
program = ArgumentParser(formatter_class = lambda prog: HelpFormatter(prog, max_help_position = 50))
|
30 |
+
program.add_argument('--onnxruntime', help = wording.get('help.install_dependency').format(dependency = 'onnxruntime'), choices = ONNXRUNTIMES.keys(), required = True)
|
31 |
+
program.add_argument('--skip-conda', help = wording.get('help.skip_conda'), action = 'store_true')
|
32 |
+
program.add_argument('-v', '--version', version = metadata.get('name') + ' ' + metadata.get('version'), action = 'version')
|
33 |
+
run(program)
|
34 |
+
|
35 |
+
|
36 |
+
def run(program : ArgumentParser) -> None:
|
37 |
+
args = program.parse_args()
|
38 |
+
has_conda = 'CONDA_PREFIX' in os.environ
|
39 |
+
onnxruntime_name, onnxruntime_version = ONNXRUNTIMES.get(args.onnxruntime)
|
40 |
+
|
41 |
+
if not args.skip_conda and not has_conda:
|
42 |
+
sys.stdout.write(wording.get('conda_not_activated') + os.linesep)
|
43 |
+
sys.exit(1)
|
44 |
+
|
45 |
+
subprocess.call([ shutil.which('pip'), 'install', '-r', 'requirements.txt', '--force-reinstall' ])
|
46 |
+
|
47 |
+
if args.onnxruntime == 'rocm':
|
48 |
+
python_id = 'cp' + str(sys.version_info.major) + str(sys.version_info.minor)
|
49 |
+
|
50 |
+
if python_id == 'cp310':
|
51 |
+
wheel_name = 'onnxruntime_rocm-' + onnxruntime_version +'-' + python_id + '-' + python_id + '-linux_x86_64.whl'
|
52 |
+
wheel_path = os.path.join(tempfile.gettempdir(), wheel_name)
|
53 |
+
wheel_url = 'https://repo.radeon.com/rocm/manylinux/rocm-rel-6.2/' + wheel_name
|
54 |
+
subprocess.call([ shutil.which('curl'), '--silent', '--location', '--continue-at', '-', '--output', wheel_path, wheel_url ])
|
55 |
+
subprocess.call([ shutil.which('pip'), 'uninstall', 'onnxruntime', wheel_path, '-y', '-q' ])
|
56 |
+
subprocess.call([ shutil.which('pip'), 'install', wheel_path, '--force-reinstall' ])
|
57 |
+
os.remove(wheel_path)
|
58 |
+
else:
|
59 |
+
subprocess.call([ shutil.which('pip'), 'uninstall', 'onnxruntime', onnxruntime_name, '-y', '-q' ])
|
60 |
+
subprocess.call([ shutil.which('pip'), 'install', onnxruntime_name + '==' + onnxruntime_version, '--force-reinstall' ])
|
61 |
+
|
62 |
+
if args.onnxruntime == 'cuda' and has_conda:
|
63 |
+
library_paths = []
|
64 |
+
|
65 |
+
if is_linux():
|
66 |
+
if os.getenv('LD_LIBRARY_PATH'):
|
67 |
+
library_paths = os.getenv('LD_LIBRARY_PATH').split(os.pathsep)
|
68 |
+
|
69 |
+
python_id = 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
|
70 |
+
library_paths.extend(
|
71 |
+
[
|
72 |
+
os.path.join(os.getenv('CONDA_PREFIX'), 'lib'),
|
73 |
+
os.path.join(os.getenv('CONDA_PREFIX'), 'lib', python_id, 'site-packages', 'tensorrt_libs')
|
74 |
+
])
|
75 |
+
library_paths = [ library_path for library_path in library_paths if os.path.exists(library_path) ]
|
76 |
+
|
77 |
+
subprocess.call([ shutil.which('conda'), 'env', 'config', 'vars', 'set', 'LD_LIBRARY_PATH=' + os.pathsep.join(library_paths) ])
|
78 |
+
|
79 |
+
if is_windows():
|
80 |
+
if os.getenv('PATH'):
|
81 |
+
library_paths = os.getenv('PATH').split(os.pathsep)
|
82 |
+
|
83 |
+
library_paths.extend(
|
84 |
+
[
|
85 |
+
os.path.join(os.getenv('CONDA_PREFIX'), 'Lib'),
|
86 |
+
os.path.join(os.getenv('CONDA_PREFIX'), 'Lib', 'site-packages', 'tensorrt_libs')
|
87 |
+
])
|
88 |
+
library_paths = [ library_path for library_path in library_paths if os.path.exists(library_path) ]
|
89 |
+
|
90 |
+
subprocess.call([ shutil.which('conda'), 'env', 'config', 'vars', 'set', 'PATH=' + os.pathsep.join(library_paths) ])
|
91 |
+
|
92 |
+
if onnxruntime_version < '1.19.0':
|
93 |
+
subprocess.call([ shutil.which('pip'), 'install', 'numpy==1.26.4', '--force-reinstall' ])
|
facefusion/jobs/__init__.py
ADDED
File without changes
|
facefusion/jobs/job_helper.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from datetime import datetime
|
3 |
+
from typing import Optional
|
4 |
+
|
5 |
+
|
6 |
+
def get_step_output_path(job_id : str, step_index : int, output_path : str) -> Optional[str]:
|
7 |
+
if output_path:
|
8 |
+
output_directory_path, _ = os.path.split(output_path)
|
9 |
+
output_file_name, output_file_extension = os.path.splitext(_)
|
10 |
+
return os.path.join(output_directory_path, output_file_name + '-' + job_id + '-' + str(step_index) + output_file_extension)
|
11 |
+
return None
|
12 |
+
|
13 |
+
|
14 |
+
def suggest_job_id(job_prefix : str = 'job') -> str:
|
15 |
+
return job_prefix + '-' + datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
|
facefusion/jobs/job_list.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from typing import Optional, Tuple
|
3 |
+
|
4 |
+
from facefusion.date_helper import describe_time_ago
|
5 |
+
from facefusion.jobs import job_manager
|
6 |
+
from facefusion.typing import JobStatus, TableContents, TableHeaders
|
7 |
+
|
8 |
+
|
9 |
+
def compose_job_list(job_status : JobStatus) -> Tuple[TableHeaders, TableContents]:
|
10 |
+
jobs = job_manager.find_jobs(job_status)
|
11 |
+
job_headers : TableHeaders = [ 'job id', 'steps', 'date created', 'date updated', 'job status' ]
|
12 |
+
job_contents : TableContents = []
|
13 |
+
|
14 |
+
for index, job_id in enumerate(jobs):
|
15 |
+
if job_manager.validate_job(job_id):
|
16 |
+
job = jobs[job_id]
|
17 |
+
step_total = job_manager.count_step_total(job_id)
|
18 |
+
date_created = prepare_describe_datetime(job.get('date_created'))
|
19 |
+
date_updated = prepare_describe_datetime(job.get('date_updated'))
|
20 |
+
job_contents.append(
|
21 |
+
[
|
22 |
+
job_id,
|
23 |
+
step_total,
|
24 |
+
date_created,
|
25 |
+
date_updated,
|
26 |
+
job_status
|
27 |
+
])
|
28 |
+
return job_headers, job_contents
|
29 |
+
|
30 |
+
|
31 |
+
def prepare_describe_datetime(date_time : Optional[str]) -> Optional[str]:
|
32 |
+
if date_time:
|
33 |
+
return describe_time_ago(datetime.fromisoformat(date_time))
|
34 |
+
return None
|
facefusion/jobs/job_manager.py
ADDED
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import glob
|
2 |
+
import os
|
3 |
+
from copy import copy
|
4 |
+
from typing import List, Optional
|
5 |
+
|
6 |
+
from facefusion.choices import job_statuses
|
7 |
+
from facefusion.date_helper import get_current_date_time
|
8 |
+
from facefusion.filesystem import create_directory, is_directory, is_file, move_file, remove_directory, remove_file
|
9 |
+
from facefusion.jobs.job_helper import get_step_output_path
|
10 |
+
from facefusion.json import read_json, write_json
|
11 |
+
from facefusion.temp_helper import create_base_directory
|
12 |
+
from facefusion.typing import Args, Job, JobSet, JobStatus, JobStep, JobStepStatus
|
13 |
+
|
14 |
+
JOBS_PATH : Optional[str] = None
|
15 |
+
|
16 |
+
|
17 |
+
def init_jobs(jobs_path : str) -> bool:
|
18 |
+
global JOBS_PATH
|
19 |
+
|
20 |
+
JOBS_PATH = jobs_path
|
21 |
+
job_status_paths = [ os.path.join(JOBS_PATH, job_status) for job_status in job_statuses ]
|
22 |
+
|
23 |
+
create_base_directory()
|
24 |
+
for job_status_path in job_status_paths:
|
25 |
+
create_directory(job_status_path)
|
26 |
+
return all(is_directory(status_path) for status_path in job_status_paths)
|
27 |
+
|
28 |
+
|
29 |
+
def clear_jobs(jobs_path : str) -> bool:
|
30 |
+
return remove_directory(jobs_path)
|
31 |
+
|
32 |
+
|
33 |
+
def create_job(job_id : str) -> bool:
|
34 |
+
job : Job =\
|
35 |
+
{
|
36 |
+
'version': '1',
|
37 |
+
'date_created': get_current_date_time().isoformat(),
|
38 |
+
'date_updated': None,
|
39 |
+
'steps': []
|
40 |
+
}
|
41 |
+
|
42 |
+
return create_job_file(job_id, job)
|
43 |
+
|
44 |
+
|
45 |
+
def submit_job(job_id : str) -> bool:
|
46 |
+
drafted_job_ids = find_job_ids('drafted')
|
47 |
+
steps = get_steps(job_id)
|
48 |
+
|
49 |
+
if job_id in drafted_job_ids and steps:
|
50 |
+
return set_steps_status(job_id, 'queued') and move_job_file(job_id, 'queued')
|
51 |
+
return False
|
52 |
+
|
53 |
+
|
54 |
+
def submit_jobs() -> bool:
|
55 |
+
drafted_job_ids = find_job_ids('drafted')
|
56 |
+
|
57 |
+
if drafted_job_ids:
|
58 |
+
for job_id in drafted_job_ids:
|
59 |
+
if not submit_job(job_id):
|
60 |
+
return False
|
61 |
+
return True
|
62 |
+
return False
|
63 |
+
|
64 |
+
|
65 |
+
def delete_job(job_id : str) -> bool:
|
66 |
+
return delete_job_file(job_id)
|
67 |
+
|
68 |
+
|
69 |
+
def delete_jobs() -> bool:
|
70 |
+
job_ids = find_job_ids('drafted') + find_job_ids('queued') + find_job_ids('failed') + find_job_ids('completed')
|
71 |
+
|
72 |
+
if job_ids:
|
73 |
+
for job_id in job_ids:
|
74 |
+
if not delete_job(job_id):
|
75 |
+
return False
|
76 |
+
return True
|
77 |
+
return False
|
78 |
+
|
79 |
+
|
80 |
+
def find_jobs(job_status : JobStatus) -> JobSet:
|
81 |
+
job_ids = find_job_ids(job_status)
|
82 |
+
jobs : JobSet = {}
|
83 |
+
|
84 |
+
for job_id in job_ids:
|
85 |
+
jobs[job_id] = read_job_file(job_id)
|
86 |
+
return jobs
|
87 |
+
|
88 |
+
|
89 |
+
def find_job_ids(job_status : JobStatus) -> List[str]:
|
90 |
+
job_pattern = os.path.join(JOBS_PATH, job_status, '*.json')
|
91 |
+
job_files = glob.glob(job_pattern)
|
92 |
+
job_files.sort(key = os.path.getmtime)
|
93 |
+
job_ids = []
|
94 |
+
|
95 |
+
for job_file in job_files:
|
96 |
+
job_id, _ = os.path.splitext(os.path.basename(job_file))
|
97 |
+
job_ids.append(job_id)
|
98 |
+
return job_ids
|
99 |
+
|
100 |
+
|
101 |
+
def validate_job(job_id : str) -> bool:
|
102 |
+
job = read_job_file(job_id)
|
103 |
+
return bool(job and 'version' in job and 'date_created' in job and 'date_updated' in job and 'steps' in job)
|
104 |
+
|
105 |
+
|
106 |
+
def has_step(job_id : str, step_index : int) -> bool:
|
107 |
+
step_total = count_step_total(job_id)
|
108 |
+
return step_index in range(step_total)
|
109 |
+
|
110 |
+
|
111 |
+
def add_step(job_id : str, step_args : Args) -> bool:
|
112 |
+
job = read_job_file(job_id)
|
113 |
+
|
114 |
+
if job:
|
115 |
+
job.get('steps').append(
|
116 |
+
{
|
117 |
+
'args': step_args,
|
118 |
+
'status': 'drafted'
|
119 |
+
})
|
120 |
+
return update_job_file(job_id, job)
|
121 |
+
return False
|
122 |
+
|
123 |
+
|
124 |
+
def remix_step(job_id : str, step_index : int, step_args : Args) -> bool:
|
125 |
+
steps = get_steps(job_id)
|
126 |
+
step_args = copy(step_args)
|
127 |
+
|
128 |
+
if step_index and step_index < 0:
|
129 |
+
step_index = count_step_total(job_id) - 1
|
130 |
+
|
131 |
+
if has_step(job_id, step_index):
|
132 |
+
output_path = steps[step_index].get('args').get('output_path')
|
133 |
+
step_args['target_path'] = get_step_output_path(job_id, step_index, output_path)
|
134 |
+
return add_step(job_id, step_args)
|
135 |
+
return False
|
136 |
+
|
137 |
+
|
138 |
+
def insert_step(job_id : str, step_index : int, step_args : Args) -> bool:
|
139 |
+
job = read_job_file(job_id)
|
140 |
+
step_args = copy(step_args)
|
141 |
+
|
142 |
+
if step_index and step_index < 0:
|
143 |
+
step_index = count_step_total(job_id) - 1
|
144 |
+
|
145 |
+
if job and has_step(job_id, step_index):
|
146 |
+
job.get('steps').insert(step_index,
|
147 |
+
{
|
148 |
+
'args': step_args,
|
149 |
+
'status': 'drafted'
|
150 |
+
})
|
151 |
+
return update_job_file(job_id, job)
|
152 |
+
return False
|
153 |
+
|
154 |
+
|
155 |
+
def remove_step(job_id : str, step_index : int) -> bool:
|
156 |
+
job = read_job_file(job_id)
|
157 |
+
|
158 |
+
if step_index and step_index < 0:
|
159 |
+
step_index = count_step_total(job_id) - 1
|
160 |
+
|
161 |
+
if job and has_step(job_id, step_index):
|
162 |
+
job.get('steps').pop(step_index)
|
163 |
+
return update_job_file(job_id, job)
|
164 |
+
return False
|
165 |
+
|
166 |
+
|
167 |
+
def get_steps(job_id : str) -> List[JobStep]:
|
168 |
+
job = read_job_file(job_id)
|
169 |
+
|
170 |
+
if job:
|
171 |
+
return job.get('steps')
|
172 |
+
return []
|
173 |
+
|
174 |
+
|
175 |
+
def count_step_total(job_id : str) -> int:
|
176 |
+
steps = get_steps(job_id)
|
177 |
+
|
178 |
+
if steps:
|
179 |
+
return len(steps)
|
180 |
+
return 0
|
181 |
+
|
182 |
+
|
183 |
+
def set_step_status(job_id : str, step_index : int, step_status : JobStepStatus) -> bool:
|
184 |
+
job = read_job_file(job_id)
|
185 |
+
|
186 |
+
if job:
|
187 |
+
steps = job.get('steps')
|
188 |
+
|
189 |
+
if has_step(job_id, step_index):
|
190 |
+
steps[step_index]['status'] = step_status
|
191 |
+
return update_job_file(job_id, job)
|
192 |
+
return False
|
193 |
+
|
194 |
+
|
195 |
+
def set_steps_status(job_id : str, step_status : JobStepStatus) -> bool:
|
196 |
+
job = read_job_file(job_id)
|
197 |
+
|
198 |
+
if job:
|
199 |
+
for step in job.get('steps'):
|
200 |
+
step['status'] = step_status
|
201 |
+
return update_job_file(job_id, job)
|
202 |
+
return False
|
203 |
+
|
204 |
+
|
205 |
+
def read_job_file(job_id : str) -> Optional[Job]:
|
206 |
+
job_path = find_job_path(job_id)
|
207 |
+
return read_json(job_path) #type:ignore[return-value]
|
208 |
+
|
209 |
+
|
210 |
+
def create_job_file(job_id : str, job : Job) -> bool:
|
211 |
+
job_path = find_job_path(job_id)
|
212 |
+
|
213 |
+
if not is_file(job_path):
|
214 |
+
job_create_path = suggest_job_path(job_id, 'drafted')
|
215 |
+
return write_json(job_create_path, job) #type:ignore[arg-type]
|
216 |
+
return False
|
217 |
+
|
218 |
+
|
219 |
+
def update_job_file(job_id : str, job : Job) -> bool:
|
220 |
+
job_path = find_job_path(job_id)
|
221 |
+
|
222 |
+
if is_file(job_path):
|
223 |
+
job['date_updated'] = get_current_date_time().isoformat()
|
224 |
+
return write_json(job_path, job) #type:ignore[arg-type]
|
225 |
+
return False
|
226 |
+
|
227 |
+
|
228 |
+
def move_job_file(job_id : str, job_status : JobStatus) -> bool:
|
229 |
+
job_path = find_job_path(job_id)
|
230 |
+
job_move_path = suggest_job_path(job_id, job_status)
|
231 |
+
return move_file(job_path, job_move_path)
|
232 |
+
|
233 |
+
|
234 |
+
def delete_job_file(job_id : str) -> bool:
|
235 |
+
job_path = find_job_path(job_id)
|
236 |
+
return remove_file(job_path)
|
237 |
+
|
238 |
+
|
239 |
+
def suggest_job_path(job_id : str, job_status : JobStatus) -> Optional[str]:
|
240 |
+
job_file_name = get_job_file_name(job_id)
|
241 |
+
|
242 |
+
if job_file_name:
|
243 |
+
return os.path.join(JOBS_PATH, job_status, job_file_name)
|
244 |
+
return None
|
245 |
+
|
246 |
+
|
247 |
+
def find_job_path(job_id : str) -> Optional[str]:
|
248 |
+
job_file_name = get_job_file_name(job_id)
|
249 |
+
|
250 |
+
if job_file_name:
|
251 |
+
for job_status in job_statuses:
|
252 |
+
job_pattern = os.path.join(JOBS_PATH, job_status, job_file_name)
|
253 |
+
job_paths = glob.glob(job_pattern)
|
254 |
+
|
255 |
+
for job_path in job_paths:
|
256 |
+
return job_path
|
257 |
+
return None
|
258 |
+
|
259 |
+
|
260 |
+
def get_job_file_name(job_id : str) -> Optional[str]:
|
261 |
+
if job_id:
|
262 |
+
return job_id + '.json'
|
263 |
+
return None
|
facefusion/jobs/job_runner.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from facefusion.ffmpeg import concat_video
|
2 |
+
from facefusion.filesystem import is_image, is_video, move_file, remove_file
|
3 |
+
from facefusion.jobs import job_helper, job_manager
|
4 |
+
from facefusion.typing import JobOutputSet, JobStep, ProcessStep
|
5 |
+
|
6 |
+
|
7 |
+
def run_job(job_id : str, process_step : ProcessStep) -> bool:
|
8 |
+
queued_job_ids = job_manager.find_job_ids('queued')
|
9 |
+
|
10 |
+
if job_id in queued_job_ids:
|
11 |
+
if run_steps(job_id, process_step) and finalize_steps(job_id):
|
12 |
+
clean_steps(job_id)
|
13 |
+
return job_manager.move_job_file(job_id, 'completed')
|
14 |
+
clean_steps(job_id)
|
15 |
+
job_manager.move_job_file(job_id, 'failed')
|
16 |
+
return False
|
17 |
+
|
18 |
+
|
19 |
+
def run_jobs(process_step : ProcessStep) -> bool:
|
20 |
+
queued_job_ids = job_manager.find_job_ids('queued')
|
21 |
+
|
22 |
+
if queued_job_ids:
|
23 |
+
for job_id in queued_job_ids:
|
24 |
+
if not run_job(job_id, process_step):
|
25 |
+
return False
|
26 |
+
return True
|
27 |
+
return False
|
28 |
+
|
29 |
+
|
30 |
+
def retry_job(job_id : str, process_step : ProcessStep) -> bool:
|
31 |
+
failed_job_ids = job_manager.find_job_ids('failed')
|
32 |
+
|
33 |
+
if job_id in failed_job_ids:
|
34 |
+
return job_manager.set_steps_status(job_id, 'queued') and job_manager.move_job_file(job_id, 'queued') and run_job(job_id, process_step)
|
35 |
+
return False
|
36 |
+
|
37 |
+
|
38 |
+
def retry_jobs(process_step : ProcessStep) -> bool:
|
39 |
+
failed_job_ids = job_manager.find_job_ids('failed')
|
40 |
+
|
41 |
+
if failed_job_ids:
|
42 |
+
for job_id in failed_job_ids:
|
43 |
+
if not retry_job(job_id, process_step):
|
44 |
+
return False
|
45 |
+
return True
|
46 |
+
return False
|
47 |
+
|
48 |
+
|
49 |
+
def run_step(job_id : str, step_index : int, step : JobStep, process_step : ProcessStep) -> bool:
|
50 |
+
step_args = step.get('args')
|
51 |
+
|
52 |
+
if job_manager.set_step_status(job_id, step_index, 'started') and process_step(job_id, step_index, step_args):
|
53 |
+
output_path = step_args.get('output_path')
|
54 |
+
step_output_path = job_helper.get_step_output_path(job_id, step_index, output_path)
|
55 |
+
|
56 |
+
return move_file(output_path, step_output_path) and job_manager.set_step_status(job_id, step_index, 'completed')
|
57 |
+
job_manager.set_step_status(job_id, step_index, 'failed')
|
58 |
+
return False
|
59 |
+
|
60 |
+
|
61 |
+
def run_steps(job_id : str, process_step : ProcessStep) -> bool:
|
62 |
+
steps = job_manager.get_steps(job_id)
|
63 |
+
|
64 |
+
if steps:
|
65 |
+
for index, step in enumerate(steps):
|
66 |
+
if not run_step(job_id, index, step, process_step):
|
67 |
+
return False
|
68 |
+
return True
|
69 |
+
return False
|
70 |
+
|
71 |
+
|
72 |
+
def finalize_steps(job_id : str) -> bool:
|
73 |
+
output_set = collect_output_set(job_id)
|
74 |
+
|
75 |
+
for output_path, temp_output_paths in output_set.items():
|
76 |
+
if all(map(is_video, temp_output_paths)):
|
77 |
+
if not concat_video(output_path, temp_output_paths):
|
78 |
+
return False
|
79 |
+
if any(map(is_image, temp_output_paths)):
|
80 |
+
for temp_output_path in temp_output_paths:
|
81 |
+
if not move_file(temp_output_path, output_path):
|
82 |
+
return False
|
83 |
+
return True
|
84 |
+
|
85 |
+
|
86 |
+
def clean_steps(job_id: str) -> bool:
|
87 |
+
output_set = collect_output_set(job_id)
|
88 |
+
|
89 |
+
for temp_output_paths in output_set.values():
|
90 |
+
for temp_output_path in temp_output_paths:
|
91 |
+
if not remove_file(temp_output_path):
|
92 |
+
return False
|
93 |
+
return True
|
94 |
+
|
95 |
+
|
96 |
+
def collect_output_set(job_id : str) -> JobOutputSet:
|
97 |
+
steps = job_manager.get_steps(job_id)
|
98 |
+
output_set : JobOutputSet = {}
|
99 |
+
|
100 |
+
for index, step in enumerate(steps):
|
101 |
+
output_path = step.get('args').get('output_path')
|
102 |
+
|
103 |
+
if output_path:
|
104 |
+
step_output_path = job_manager.get_step_output_path(job_id, index, output_path)
|
105 |
+
output_set.setdefault(output_path, []).append(step_output_path)
|
106 |
+
return output_set
|
facefusion/jobs/job_store.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
|
3 |
+
from facefusion.typing import JobStore
|
4 |
+
|
5 |
+
JOB_STORE : JobStore =\
|
6 |
+
{
|
7 |
+
'job_keys': [],
|
8 |
+
'step_keys': []
|
9 |
+
}
|
10 |
+
|
11 |
+
|
12 |
+
def get_job_keys() -> List[str]:
|
13 |
+
return JOB_STORE.get('job_keys')
|
14 |
+
|
15 |
+
|
16 |
+
def get_step_keys() -> List[str]:
|
17 |
+
return JOB_STORE.get('step_keys')
|
18 |
+
|
19 |
+
|
20 |
+
def register_job_keys(step_keys : List[str]) -> None:
|
21 |
+
for step_key in step_keys:
|
22 |
+
JOB_STORE['job_keys'].append(step_key)
|
23 |
+
|
24 |
+
|
25 |
+
def register_step_keys(job_keys : List[str]) -> None:
|
26 |
+
for job_key in job_keys:
|
27 |
+
JOB_STORE['step_keys'].append(job_key)
|
facefusion/json.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from json import JSONDecodeError
|
3 |
+
from typing import Optional
|
4 |
+
|
5 |
+
from facefusion.filesystem import is_file
|
6 |
+
from facefusion.typing import Content
|
7 |
+
|
8 |
+
|
9 |
+
def read_json(json_path : str) -> Optional[Content]:
|
10 |
+
if is_file(json_path):
|
11 |
+
try:
|
12 |
+
with open(json_path, 'r') as json_file:
|
13 |
+
return json.load(json_file)
|
14 |
+
except JSONDecodeError:
|
15 |
+
pass
|
16 |
+
return None
|
17 |
+
|
18 |
+
|
19 |
+
def write_json(json_path : str, content : Content) -> bool:
|
20 |
+
with open(json_path, 'w') as json_file:
|
21 |
+
json.dump(content, json_file, indent = 4)
|
22 |
+
return is_file(json_path)
|
facefusion/logger.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from logging import Logger, basicConfig, getLogger
|
2 |
+
from typing import Tuple
|
3 |
+
|
4 |
+
from facefusion.choices import log_level_set
|
5 |
+
from facefusion.common_helper import get_first, get_last
|
6 |
+
from facefusion.typing import LogLevel, TableContents, TableHeaders
|
7 |
+
|
8 |
+
|
9 |
+
def init(log_level : LogLevel) -> None:
|
10 |
+
basicConfig(format = '%(message)s')
|
11 |
+
get_package_logger().setLevel(log_level_set.get(log_level))
|
12 |
+
|
13 |
+
|
14 |
+
def get_package_logger() -> Logger:
|
15 |
+
return getLogger('facefusion')
|
16 |
+
|
17 |
+
|
18 |
+
def debug(message : str, module_name : str) -> None:
|
19 |
+
get_package_logger().debug(create_message(message, module_name))
|
20 |
+
|
21 |
+
|
22 |
+
def info(message : str, module_name : str) -> None:
|
23 |
+
get_package_logger().info(create_message(message, module_name))
|
24 |
+
|
25 |
+
|
26 |
+
def warn(message : str, module_name : str) -> None:
|
27 |
+
get_package_logger().warning(create_message(message, module_name))
|
28 |
+
|
29 |
+
|
30 |
+
def error(message : str, module_name : str) -> None:
|
31 |
+
get_package_logger().error(create_message(message, module_name))
|
32 |
+
|
33 |
+
|
34 |
+
def create_message(message : str, module_name : str) -> str:
|
35 |
+
scopes = module_name.split('.')
|
36 |
+
first_scope = get_first(scopes)
|
37 |
+
last_scope = get_last(scopes)
|
38 |
+
|
39 |
+
if first_scope and last_scope:
|
40 |
+
return '[' + first_scope.upper() + '.' + last_scope.upper() + '] ' + message
|
41 |
+
return message
|
42 |
+
|
43 |
+
|
44 |
+
def table(headers : TableHeaders, contents : TableContents) -> None:
|
45 |
+
package_logger = get_package_logger()
|
46 |
+
table_column, table_separator = create_table_parts(headers, contents)
|
47 |
+
|
48 |
+
package_logger.info(table_separator)
|
49 |
+
package_logger.info(table_column.format(*headers))
|
50 |
+
package_logger.info(table_separator)
|
51 |
+
|
52 |
+
for content in contents:
|
53 |
+
content = [ value if value else '' for value in content ]
|
54 |
+
package_logger.info(table_column.format(*content))
|
55 |
+
|
56 |
+
package_logger.info(table_separator)
|
57 |
+
|
58 |
+
|
59 |
+
def create_table_parts(headers : TableHeaders, contents : TableContents) -> Tuple[str, str]:
|
60 |
+
column_parts = []
|
61 |
+
separator_parts = []
|
62 |
+
widths = [ len(header) for header in headers ]
|
63 |
+
|
64 |
+
for content in contents:
|
65 |
+
for index, value in enumerate(content):
|
66 |
+
widths[index] = max(widths[index], len(str(value)))
|
67 |
+
|
68 |
+
for width in widths:
|
69 |
+
column_parts.append('{:<' + str(width) + '}')
|
70 |
+
separator_parts.append('-' * width)
|
71 |
+
|
72 |
+
return '| ' + ' | '.join(column_parts) + ' |', '+-' + '-+-'.join(separator_parts) + '-+'
|
73 |
+
|
74 |
+
|
75 |
+
def enable() -> None:
|
76 |
+
get_package_logger().disabled = False
|
77 |
+
|
78 |
+
|
79 |
+
def disable() -> None:
|
80 |
+
get_package_logger().disabled = True
|
facefusion/memory.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from facefusion.common_helper import is_macos, is_windows
|
2 |
+
|
3 |
+
if is_windows():
|
4 |
+
import ctypes
|
5 |
+
else:
|
6 |
+
import resource
|
7 |
+
|
8 |
+
|
9 |
+
def limit_system_memory(system_memory_limit : int = 1) -> bool:
|
10 |
+
if is_macos():
|
11 |
+
system_memory_limit = system_memory_limit * (1024 ** 6)
|
12 |
+
else:
|
13 |
+
system_memory_limit = system_memory_limit * (1024 ** 3)
|
14 |
+
try:
|
15 |
+
if is_windows():
|
16 |
+
ctypes.windll.kernel32.SetProcessWorkingSetSize(-1, ctypes.c_size_t(system_memory_limit), ctypes.c_size_t(system_memory_limit)) #type:ignore[attr-defined]
|
17 |
+
else:
|
18 |
+
resource.setrlimit(resource.RLIMIT_DATA, (system_memory_limit, system_memory_limit))
|
19 |
+
return True
|
20 |
+
except Exception:
|
21 |
+
return False
|
facefusion/metadata.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
|
3 |
+
METADATA =\
|
4 |
+
{
|
5 |
+
'name': 'FaceFusion',
|
6 |
+
'description': 'Industry leading face manipulation platform',
|
7 |
+
'version': '3.0.0',
|
8 |
+
'license': 'MIT',
|
9 |
+
'author': 'Henry Ruhs',
|
10 |
+
'url': 'https://facefusion.io'
|
11 |
+
}
|
12 |
+
|
13 |
+
|
14 |
+
def get(key : str) -> Optional[str]:
|
15 |
+
if key in METADATA:
|
16 |
+
return METADATA.get(key)
|
17 |
+
return None
|
facefusion/normalizer.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Optional
|
2 |
+
|
3 |
+
from facefusion.typing import Fps, Padding
|
4 |
+
|
5 |
+
|
6 |
+
def normalize_padding(padding : Optional[List[int]]) -> Optional[Padding]:
|
7 |
+
if padding and len(padding) == 1:
|
8 |
+
return tuple([ padding[0] ] * 4) #type:ignore[return-value]
|
9 |
+
if padding and len(padding) == 2:
|
10 |
+
return tuple([ padding[0], padding[1], padding[0], padding[1] ]) #type:ignore[return-value]
|
11 |
+
if padding and len(padding) == 3:
|
12 |
+
return tuple([ padding[0], padding[1], padding[2], padding[1] ]) #type:ignore[return-value]
|
13 |
+
if padding and len(padding) == 4:
|
14 |
+
return tuple(padding) #type:ignore[return-value]
|
15 |
+
return None
|
16 |
+
|
17 |
+
|
18 |
+
def normalize_fps(fps : Optional[float]) -> Optional[Fps]:
|
19 |
+
if isinstance(fps, (int, float)):
|
20 |
+
return max(1.0, min(fps, 60.0))
|
21 |
+
return None
|
facefusion/process_manager.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Generator, List
|
2 |
+
|
3 |
+
from facefusion.typing import ProcessState, QueuePayload
|
4 |
+
|
5 |
+
PROCESS_STATE : ProcessState = 'pending'
|
6 |
+
|
7 |
+
|
8 |
+
def get_process_state() -> ProcessState:
|
9 |
+
return PROCESS_STATE
|
10 |
+
|
11 |
+
|
12 |
+
def set_process_state(process_state : ProcessState) -> None:
|
13 |
+
global PROCESS_STATE
|
14 |
+
|
15 |
+
PROCESS_STATE = process_state
|
16 |
+
|
17 |
+
|
18 |
+
def is_checking() -> bool:
|
19 |
+
return get_process_state() == 'checking'
|
20 |
+
|
21 |
+
|
22 |
+
def is_processing() -> bool:
|
23 |
+
return get_process_state() == 'processing'
|
24 |
+
|
25 |
+
|
26 |
+
def is_stopping() -> bool:
|
27 |
+
return get_process_state() == 'stopping'
|
28 |
+
|
29 |
+
|
30 |
+
def is_pending() -> bool:
|
31 |
+
return get_process_state() == 'pending'
|
32 |
+
|
33 |
+
|
34 |
+
def check() -> None:
|
35 |
+
set_process_state('checking')
|
36 |
+
|
37 |
+
|
38 |
+
def start() -> None:
|
39 |
+
set_process_state('processing')
|
40 |
+
|
41 |
+
|
42 |
+
def stop() -> None:
|
43 |
+
set_process_state('stopping')
|
44 |
+
|
45 |
+
|
46 |
+
def end() -> None:
|
47 |
+
set_process_state('pending')
|
48 |
+
|
49 |
+
|
50 |
+
def manage(queue_payloads : List[QueuePayload]) -> Generator[QueuePayload, None, None]:
|
51 |
+
for query_payload in queue_payloads:
|
52 |
+
if is_processing():
|
53 |
+
yield query_payload
|
facefusion/processors/__init__.py
ADDED
File without changes
|
facefusion/processors/choices.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Sequence
|
2 |
+
|
3 |
+
from facefusion.common_helper import create_float_range, create_int_range
|
4 |
+
from facefusion.processors.typing import AgeModifierModel, ExpressionRestorerModel, FaceDebuggerItem, FaceEditorModel, FaceEnhancerModel, FaceSwapperSet, FrameColorizerModel, FrameEnhancerModel, LipSyncerModel
|
5 |
+
|
6 |
+
age_modifier_models : List[AgeModifierModel] = [ 'styleganex_age' ]
|
7 |
+
expression_restorer_models : List[ExpressionRestorerModel] = [ 'live_portrait' ]
|
8 |
+
face_debugger_items : List[FaceDebuggerItem] = [ 'bounding-box', 'face-landmark-5', 'face-landmark-5/68', 'face-landmark-68', 'face-landmark-68/5', 'face-mask', 'face-detector-score', 'face-landmarker-score', 'age', 'gender', 'race' ]
|
9 |
+
face_editor_models : List[FaceEditorModel] = [ 'live_portrait' ]
|
10 |
+
face_enhancer_models : List[FaceEnhancerModel] = [ 'codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_256', 'gpen_bfr_512', 'gpen_bfr_1024', 'gpen_bfr_2048', 'restoreformer_plus_plus' ]
|
11 |
+
face_swapper_set : FaceSwapperSet =\
|
12 |
+
{
|
13 |
+
'blendswap_256': [ '256x256', '384x384', '512x512', '768x768', '1024x1024' ],
|
14 |
+
'ghost_1_256': [ '256x256', '512x512', '768x768', '1024x1024' ],
|
15 |
+
'ghost_2_256': [ '256x256', '512x512', '768x768', '1024x1024' ],
|
16 |
+
'ghost_3_256': [ '256x256', '512x512', '768x768', '1024x1024' ],
|
17 |
+
'inswapper_128': [ '128x128', '256x256', '384x384', '512x512', '768x768', '1024x1024' ],
|
18 |
+
'inswapper_128_fp16': [ '128x128', '256x256', '384x384', '512x512', '768x768', '1024x1024' ],
|
19 |
+
'simswap_256': [ '256x256', '512x512', '768x768', '1024x1024' ],
|
20 |
+
'simswap_unofficial_512': [ '512x512', '768x768', '1024x1024' ],
|
21 |
+
'uniface_256': [ '256x256', '512x512', '768x768', '1024x1024' ]
|
22 |
+
}
|
23 |
+
frame_colorizer_models : List[FrameColorizerModel] = [ 'ddcolor', 'ddcolor_artistic', 'deoldify', 'deoldify_artistic', 'deoldify_stable' ]
|
24 |
+
frame_colorizer_sizes : List[str] = [ '192x192', '256x256', '384x384', '512x512' ]
|
25 |
+
frame_enhancer_models : List[FrameEnhancerModel] = [ 'clear_reality_x4', 'lsdir_x4', 'nomos8k_sc_x4', 'real_esrgan_x2', 'real_esrgan_x2_fp16', 'real_esrgan_x4', 'real_esrgan_x4_fp16', 'real_esrgan_x8', 'real_esrgan_x8_fp16', 'real_hatgan_x4', 'span_kendata_x4', 'ultra_sharp_x4' ]
|
26 |
+
lip_syncer_models : List[LipSyncerModel] = [ 'wav2lip_96', 'wav2lip_gan_96' ]
|
27 |
+
|
28 |
+
age_modifier_direction_range : Sequence[int] = create_int_range(-100, 100, 1)
|
29 |
+
expression_restorer_factor_range : Sequence[int] = create_int_range(0, 100, 1)
|
30 |
+
face_editor_eyebrow_direction_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
31 |
+
face_editor_eye_gaze_horizontal_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
32 |
+
face_editor_eye_gaze_vertical_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
33 |
+
face_editor_eye_open_ratio_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
34 |
+
face_editor_lip_open_ratio_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
35 |
+
face_editor_mouth_grim_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
36 |
+
face_editor_mouth_pout_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
37 |
+
face_editor_mouth_purse_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
38 |
+
face_editor_mouth_smile_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
39 |
+
face_editor_mouth_position_horizontal_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
40 |
+
face_editor_mouth_position_vertical_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
41 |
+
face_editor_head_pitch_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
42 |
+
face_editor_head_yaw_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
43 |
+
face_editor_head_roll_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05)
|
44 |
+
face_enhancer_blend_range : Sequence[int] = create_int_range(0, 100, 1)
|
45 |
+
frame_colorizer_blend_range : Sequence[int] = create_int_range(0, 100, 1)
|
46 |
+
frame_enhancer_blend_range : Sequence[int] = create_int_range(0, 100, 1)
|
facefusion/processors/core.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import importlib
|
2 |
+
import os
|
3 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
4 |
+
from queue import Queue
|
5 |
+
from types import ModuleType
|
6 |
+
from typing import Any, List
|
7 |
+
|
8 |
+
from tqdm import tqdm
|
9 |
+
|
10 |
+
from facefusion import logger, state_manager, wording
|
11 |
+
from facefusion.exit_helper import hard_exit
|
12 |
+
from facefusion.typing import ProcessFrames, QueuePayload
|
13 |
+
|
14 |
+
PROCESSORS_METHODS =\
|
15 |
+
[
|
16 |
+
'get_inference_pool',
|
17 |
+
'clear_inference_pool',
|
18 |
+
'register_args',
|
19 |
+
'apply_args',
|
20 |
+
'pre_check',
|
21 |
+
'pre_process',
|
22 |
+
'post_process',
|
23 |
+
'get_reference_frame',
|
24 |
+
'process_frame',
|
25 |
+
'process_frames',
|
26 |
+
'process_image',
|
27 |
+
'process_video'
|
28 |
+
]
|
29 |
+
|
30 |
+
|
31 |
+
def load_processor_module(processor : str) -> Any:
|
32 |
+
try:
|
33 |
+
processor_module = importlib.import_module('facefusion.processors.modules.' + processor)
|
34 |
+
for method_name in PROCESSORS_METHODS:
|
35 |
+
if not hasattr(processor_module, method_name):
|
36 |
+
raise NotImplementedError
|
37 |
+
except ModuleNotFoundError as exception:
|
38 |
+
logger.error(wording.get('processor_not_loaded').format(processor = processor), __name__)
|
39 |
+
logger.debug(exception.msg, __name__)
|
40 |
+
hard_exit(1)
|
41 |
+
except NotImplementedError:
|
42 |
+
logger.error(wording.get('processor_not_implemented').format(processor = processor), __name__)
|
43 |
+
hard_exit(1)
|
44 |
+
return processor_module
|
45 |
+
|
46 |
+
|
47 |
+
def get_processors_modules(processors : List[str]) -> List[ModuleType]:
|
48 |
+
processor_modules = []
|
49 |
+
|
50 |
+
for processor in processors:
|
51 |
+
processor_module = load_processor_module(processor)
|
52 |
+
processor_modules.append(processor_module)
|
53 |
+
return processor_modules
|
54 |
+
|
55 |
+
|
56 |
+
def clear_processors_modules(processors : List[str]) -> None:
|
57 |
+
for processor in processors:
|
58 |
+
processor_module = load_processor_module(processor)
|
59 |
+
processor_module.clear_inference_pool()
|
60 |
+
|
61 |
+
|
62 |
+
def multi_process_frames(source_paths : List[str], temp_frame_paths : List[str], process_frames : ProcessFrames) -> None:
|
63 |
+
queue_payloads = create_queue_payloads(temp_frame_paths)
|
64 |
+
with tqdm(total = len(queue_payloads), desc = wording.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress:
|
65 |
+
progress.set_postfix(
|
66 |
+
{
|
67 |
+
'execution_providers': state_manager.get_item('execution_providers'),
|
68 |
+
'execution_thread_count': state_manager.get_item('execution_thread_count'),
|
69 |
+
'execution_queue_count': state_manager.get_item('execution_queue_count')
|
70 |
+
})
|
71 |
+
with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor:
|
72 |
+
futures = []
|
73 |
+
queue : Queue[QueuePayload] = create_queue(queue_payloads)
|
74 |
+
queue_per_future = max(len(queue_payloads) // state_manager.get_item('execution_thread_count') * state_manager.get_item('execution_queue_count'), 1)
|
75 |
+
|
76 |
+
while not queue.empty():
|
77 |
+
future = executor.submit(process_frames, source_paths, pick_queue(queue, queue_per_future), progress.update)
|
78 |
+
futures.append(future)
|
79 |
+
|
80 |
+
for future_done in as_completed(futures):
|
81 |
+
future_done.result()
|
82 |
+
|
83 |
+
|
84 |
+
def create_queue(queue_payloads : List[QueuePayload]) -> Queue[QueuePayload]:
|
85 |
+
queue : Queue[QueuePayload] = Queue()
|
86 |
+
for queue_payload in queue_payloads:
|
87 |
+
queue.put(queue_payload)
|
88 |
+
return queue
|
89 |
+
|
90 |
+
|
91 |
+
def pick_queue(queue : Queue[QueuePayload], queue_per_future : int) -> List[QueuePayload]:
|
92 |
+
queues = []
|
93 |
+
for _ in range(queue_per_future):
|
94 |
+
if not queue.empty():
|
95 |
+
queues.append(queue.get())
|
96 |
+
return queues
|
97 |
+
|
98 |
+
|
99 |
+
def create_queue_payloads(temp_frame_paths : List[str]) -> List[QueuePayload]:
|
100 |
+
queue_payloads = []
|
101 |
+
temp_frame_paths = sorted(temp_frame_paths, key = os.path.basename)
|
102 |
+
|
103 |
+
for frame_number, frame_path in enumerate(temp_frame_paths):
|
104 |
+
frame_payload : QueuePayload =\
|
105 |
+
{
|
106 |
+
'frame_number': frame_number,
|
107 |
+
'frame_path': frame_path
|
108 |
+
}
|
109 |
+
queue_payloads.append(frame_payload)
|
110 |
+
return queue_payloads
|
facefusion/processors/live_portrait.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Tuple
|
2 |
+
|
3 |
+
import numpy
|
4 |
+
import scipy
|
5 |
+
|
6 |
+
from facefusion.processors.typing import LivePortraitExpression, LivePortraitPitch, LivePortraitRoll, LivePortraitRotation, LivePortraitYaw
|
7 |
+
|
8 |
+
EXPRESSION_MIN = numpy.array(
|
9 |
+
[
|
10 |
+
[
|
11 |
+
[ -2.88067125e-02, -8.12731311e-02, -1.70541159e-03 ],
|
12 |
+
[ -4.88598682e-02, -3.32196616e-02, -1.67431499e-04 ],
|
13 |
+
[ -6.75425082e-02, -4.28681746e-02, -1.98950816e-04 ],
|
14 |
+
[ -7.23103955e-02, -3.28503326e-02, -7.31324719e-04 ],
|
15 |
+
[ -3.87073644e-02, -6.01546466e-02, -5.50269964e-04 ],
|
16 |
+
[ -6.38048723e-02, -2.23840728e-01, -7.13261834e-04 ],
|
17 |
+
[ -3.02710701e-02, -3.93195450e-02, -8.24086510e-06 ],
|
18 |
+
[ -2.95799859e-02, -5.39318882e-02, -1.74219604e-04 ],
|
19 |
+
[ -2.92359516e-02, -1.53050944e-02, -6.30460854e-05 ],
|
20 |
+
[ -5.56493877e-03, -2.34344602e-02, -1.26858242e-04 ],
|
21 |
+
[ -4.37593013e-02, -2.77768299e-02, -2.70503685e-02 ],
|
22 |
+
[ -1.76926646e-02, -1.91676542e-02, -1.15090821e-04 ],
|
23 |
+
[ -8.34268332e-03, -3.99775570e-03, -3.27481248e-05 ],
|
24 |
+
[ -3.40162888e-02, -2.81868968e-02, -1.96679524e-04 ],
|
25 |
+
[ -2.91855410e-02, -3.97511162e-02, -2.81230678e-05 ],
|
26 |
+
[ -1.50395725e-02, -2.49494594e-02, -9.42573533e-05 ],
|
27 |
+
[ -1.67938769e-02, -2.00953931e-02, -4.00750607e-04 ],
|
28 |
+
[ -1.86435618e-02, -2.48535164e-02, -2.74416432e-02 ],
|
29 |
+
[ -4.61211195e-03, -1.21660791e-02, -2.93173041e-04 ],
|
30 |
+
[ -4.10017073e-02, -7.43824020e-02, -4.42762971e-02 ],
|
31 |
+
[ -1.90370996e-02, -3.74363363e-02, -1.34740388e-02 ]
|
32 |
+
]
|
33 |
+
]).astype(numpy.float32)
|
34 |
+
EXPRESSION_MAX = numpy.array(
|
35 |
+
[
|
36 |
+
[
|
37 |
+
[ 4.46682945e-02, 7.08772913e-02, 4.08344204e-04 ],
|
38 |
+
[ 2.14308221e-02, 6.15894832e-02, 4.85319615e-05 ],
|
39 |
+
[ 3.02363783e-02, 4.45043296e-02, 1.28298725e-05 ],
|
40 |
+
[ 3.05869691e-02, 3.79812494e-02, 6.57040102e-04 ],
|
41 |
+
[ 4.45670523e-02, 3.97259220e-02, 7.10966764e-04 ],
|
42 |
+
[ 9.43699256e-02, 9.85926315e-02, 2.02551950e-04 ],
|
43 |
+
[ 1.61131397e-02, 2.92906128e-02, 3.44733417e-06 ],
|
44 |
+
[ 5.23825921e-02, 1.07065082e-01, 6.61510974e-04 ],
|
45 |
+
[ 2.85718683e-03, 8.32320191e-03, 2.39314613e-04 ],
|
46 |
+
[ 2.57947259e-02, 1.60935968e-02, 2.41853559e-05 ],
|
47 |
+
[ 4.90833223e-02, 3.43903080e-02, 3.22353356e-02 ],
|
48 |
+
[ 1.44766076e-02, 3.39248963e-02, 1.42291479e-04 ],
|
49 |
+
[ 8.75749043e-04, 6.82212645e-03, 2.76097053e-05 ],
|
50 |
+
[ 1.86958015e-02, 3.84016186e-02, 7.33085908e-05 ],
|
51 |
+
[ 2.01714113e-02, 4.90544215e-02, 2.34028921e-05 ],
|
52 |
+
[ 2.46518422e-02, 3.29151377e-02, 3.48571630e-05 ],
|
53 |
+
[ 2.22457591e-02, 1.21796541e-02, 1.56396593e-04 ],
|
54 |
+
[ 1.72109623e-02, 3.01626958e-02, 1.36556877e-02 ],
|
55 |
+
[ 1.83460284e-02, 1.61141958e-02, 2.87440169e-04 ],
|
56 |
+
[ 3.57594155e-02, 1.80554688e-01, 2.75554154e-02 ],
|
57 |
+
[ 2.17450950e-02, 8.66811201e-02, 3.34241726e-02 ]
|
58 |
+
]
|
59 |
+
]).astype(numpy.float32)
|
60 |
+
|
61 |
+
|
62 |
+
def limit_expression(expression : LivePortraitExpression) -> LivePortraitExpression:
|
63 |
+
return numpy.clip(expression, EXPRESSION_MIN, EXPRESSION_MAX)
|
64 |
+
|
65 |
+
|
66 |
+
def limit_euler_angles(target_pitch : LivePortraitPitch, target_yaw : LivePortraitYaw, target_roll : LivePortraitRoll, output_pitch : LivePortraitPitch, output_yaw : LivePortraitYaw, output_roll : LivePortraitRoll) -> Tuple[LivePortraitPitch, LivePortraitYaw, LivePortraitRoll]:
|
67 |
+
pitch_min, pitch_max, yaw_min, yaw_max, roll_min, roll_max = calc_euler_limits(target_pitch, target_yaw, target_roll)
|
68 |
+
output_pitch = numpy.clip(output_pitch, pitch_min, pitch_max)
|
69 |
+
output_yaw = numpy.clip(output_yaw, yaw_min, yaw_max)
|
70 |
+
output_roll = numpy.clip(output_roll, roll_min, roll_max)
|
71 |
+
return output_pitch, output_yaw, output_roll
|
72 |
+
|
73 |
+
|
74 |
+
def calc_euler_limits(pitch : LivePortraitPitch, yaw : LivePortraitYaw, roll : LivePortraitRoll) -> Tuple[float, float, float, float, float, float]:
|
75 |
+
pitch_min = -30.0
|
76 |
+
pitch_max = 30.0
|
77 |
+
yaw_min = -60.0
|
78 |
+
yaw_max = 60.0
|
79 |
+
roll_min = -20.0
|
80 |
+
roll_max = 20.0
|
81 |
+
|
82 |
+
if pitch < 0:
|
83 |
+
pitch_min = min(pitch, pitch_min)
|
84 |
+
else:
|
85 |
+
pitch_max = max(pitch, pitch_max)
|
86 |
+
if yaw < 0:
|
87 |
+
yaw_min = min(yaw, yaw_min)
|
88 |
+
else:
|
89 |
+
yaw_max = max(yaw, yaw_max)
|
90 |
+
if roll < 0:
|
91 |
+
roll_min = min(roll, roll_min)
|
92 |
+
else:
|
93 |
+
roll_max = max(roll, roll_max)
|
94 |
+
|
95 |
+
return pitch_min, pitch_max, yaw_min, yaw_max, roll_min, roll_max
|
96 |
+
|
97 |
+
|
98 |
+
def create_rotation(pitch : LivePortraitPitch, yaw : LivePortraitYaw, roll : LivePortraitRoll) -> LivePortraitRotation:
|
99 |
+
rotation = scipy.spatial.transform.Rotation.from_euler('xyz', [ pitch, yaw, roll ], degrees = True).as_matrix()
|
100 |
+
rotation = rotation.astype(numpy.float32)
|
101 |
+
return rotation
|
facefusion/processors/modules/__init__.py
ADDED
File without changes
|
facefusion/processors/modules/age_modifier.py
ADDED
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from argparse import ArgumentParser
|
2 |
+
from typing import Any, List
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
from cv2.typing import Size
|
7 |
+
from numpy.typing import NDArray
|
8 |
+
|
9 |
+
import facefusion.jobs.job_manager
|
10 |
+
import facefusion.jobs.job_store
|
11 |
+
import facefusion.processors.core as processors
|
12 |
+
from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording
|
13 |
+
from facefusion.common_helper import create_int_metavar
|
14 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
15 |
+
from facefusion.face_analyser import get_many_faces, get_one_face
|
16 |
+
from facefusion.face_helper import merge_matrix, paste_back, scale_face_landmark_5, warp_face_by_face_landmark_5
|
17 |
+
from facefusion.face_masker import create_occlusion_mask, create_static_box_mask
|
18 |
+
from facefusion.face_selector import find_similar_faces, sort_and_filter_faces
|
19 |
+
from facefusion.face_store import get_reference_faces
|
20 |
+
from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension
|
21 |
+
from facefusion.processors import choices as processors_choices
|
22 |
+
from facefusion.processors.typing import AgeModifierInputs
|
23 |
+
from facefusion.program_helper import find_argument_group
|
24 |
+
from facefusion.thread_helper import thread_semaphore
|
25 |
+
from facefusion.typing import ApplyStateItem, Args, Face, InferencePool, Mask, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame
|
26 |
+
from facefusion.vision import read_image, read_static_image, write_image
|
27 |
+
|
28 |
+
MODEL_SET : ModelSet =\
|
29 |
+
{
|
30 |
+
'styleganex_age':
|
31 |
+
{
|
32 |
+
'hashes':
|
33 |
+
{
|
34 |
+
'age_modifier':
|
35 |
+
{
|
36 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/styleganex_age.hash',
|
37 |
+
'path': resolve_relative_path('../.assets/models/styleganex_age.hash')
|
38 |
+
}
|
39 |
+
},
|
40 |
+
'sources':
|
41 |
+
{
|
42 |
+
'age_modifier':
|
43 |
+
{
|
44 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/styleganex_age.onnx',
|
45 |
+
'path': resolve_relative_path('../.assets/models/styleganex_age.onnx')
|
46 |
+
}
|
47 |
+
},
|
48 |
+
'template': 'ffhq_512',
|
49 |
+
'size': (512, 512)
|
50 |
+
}
|
51 |
+
}
|
52 |
+
|
53 |
+
|
54 |
+
def get_inference_pool() -> InferencePool:
|
55 |
+
model_sources = get_model_options().get('sources')
|
56 |
+
model_context = __name__ + '.' + state_manager.get_item('age_modifier_model')
|
57 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
58 |
+
|
59 |
+
|
60 |
+
def clear_inference_pool() -> None:
|
61 |
+
model_context = __name__ + '.' + state_manager.get_item('age_modifier_model')
|
62 |
+
inference_manager.clear_inference_pool(model_context)
|
63 |
+
|
64 |
+
|
65 |
+
def get_model_options() -> ModelOptions:
|
66 |
+
age_modifier_model = state_manager.get_item('age_modifier_model')
|
67 |
+
return MODEL_SET.get(age_modifier_model)
|
68 |
+
|
69 |
+
|
70 |
+
def register_args(program : ArgumentParser) -> None:
|
71 |
+
group_processors = find_argument_group(program, 'processors')
|
72 |
+
if group_processors:
|
73 |
+
group_processors.add_argument('--age-modifier-model', help = wording.get('help.age_modifier_model'), default = config.get_str_value('processors.age_modifier_model', 'styleganex_age'), choices = processors_choices.age_modifier_models)
|
74 |
+
group_processors.add_argument('--age-modifier-direction', help = wording.get('help.age_modifier_direction'), type = int, default = config.get_int_value('processors.age_modifier_direction', '0'), choices = processors_choices.age_modifier_direction_range, metavar = create_int_metavar(processors_choices.age_modifier_direction_range))
|
75 |
+
facefusion.jobs.job_store.register_step_keys([ 'age_modifier_model', 'age_modifier_direction' ])
|
76 |
+
|
77 |
+
|
78 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
79 |
+
apply_state_item('age_modifier_model', args.get('age_modifier_model'))
|
80 |
+
apply_state_item('age_modifier_direction', args.get('age_modifier_direction'))
|
81 |
+
|
82 |
+
|
83 |
+
def pre_check() -> bool:
|
84 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
85 |
+
model_hashes = get_model_options().get('hashes')
|
86 |
+
model_sources = get_model_options().get('sources')
|
87 |
+
|
88 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
89 |
+
|
90 |
+
|
91 |
+
def pre_process(mode : ProcessMode) -> bool:
|
92 |
+
if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')):
|
93 |
+
logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__)
|
94 |
+
return False
|
95 |
+
if mode == 'output' and not in_directory(state_manager.get_item('output_path')):
|
96 |
+
logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__)
|
97 |
+
return False
|
98 |
+
if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]):
|
99 |
+
logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__)
|
100 |
+
return False
|
101 |
+
return True
|
102 |
+
|
103 |
+
|
104 |
+
def post_process() -> None:
|
105 |
+
read_static_image.cache_clear()
|
106 |
+
if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]:
|
107 |
+
clear_inference_pool()
|
108 |
+
if state_manager.get_item('video_memory_strategy') == 'strict':
|
109 |
+
content_analyser.clear_inference_pool()
|
110 |
+
face_classifier.clear_inference_pool()
|
111 |
+
face_detector.clear_inference_pool()
|
112 |
+
face_landmarker.clear_inference_pool()
|
113 |
+
face_masker.clear_inference_pool()
|
114 |
+
face_recognizer.clear_inference_pool()
|
115 |
+
|
116 |
+
|
117 |
+
def modify_age(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
118 |
+
model_template = get_model_options().get('template')
|
119 |
+
model_size = get_model_options().get('size')
|
120 |
+
crop_size = (model_size[0] // 2, model_size[1] // 2)
|
121 |
+
face_landmark_5 = target_face.landmark_set.get('5/68').copy()
|
122 |
+
extend_face_landmark_5 = scale_face_landmark_5(face_landmark_5, 2.0)
|
123 |
+
crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, crop_size)
|
124 |
+
extend_vision_frame, extend_affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, extend_face_landmark_5, model_template, model_size)
|
125 |
+
extend_vision_frame_raw = extend_vision_frame.copy()
|
126 |
+
box_mask = create_static_box_mask(model_size, state_manager.get_item('face_mask_blur'), (0, 0, 0, 0))
|
127 |
+
crop_masks =\
|
128 |
+
[
|
129 |
+
box_mask
|
130 |
+
]
|
131 |
+
|
132 |
+
if 'occlusion' in state_manager.get_item('face_mask_types'):
|
133 |
+
occlusion_mask = create_occlusion_mask(crop_vision_frame)
|
134 |
+
combined_matrix = merge_matrix([ extend_affine_matrix, cv2.invertAffineTransform(affine_matrix) ])
|
135 |
+
occlusion_mask = cv2.warpAffine(occlusion_mask, combined_matrix, model_size)
|
136 |
+
crop_masks.append(occlusion_mask)
|
137 |
+
|
138 |
+
crop_vision_frame = prepare_vision_frame(crop_vision_frame)
|
139 |
+
extend_vision_frame = prepare_vision_frame(extend_vision_frame)
|
140 |
+
extend_vision_frame = forward(crop_vision_frame, extend_vision_frame)
|
141 |
+
extend_vision_frame = normalize_extend_frame(extend_vision_frame)
|
142 |
+
extend_vision_frame = fix_color(extend_vision_frame_raw, extend_vision_frame)
|
143 |
+
extend_crop_mask = cv2.pyrUp(numpy.minimum.reduce(crop_masks).clip(0, 1))
|
144 |
+
extend_affine_matrix *= extend_vision_frame.shape[0] / 512
|
145 |
+
paste_vision_frame = paste_back(temp_vision_frame, extend_vision_frame, extend_crop_mask, extend_affine_matrix)
|
146 |
+
return paste_vision_frame
|
147 |
+
|
148 |
+
|
149 |
+
def forward(crop_vision_frame : VisionFrame, extend_vision_frame : VisionFrame) -> VisionFrame:
|
150 |
+
age_modifier = get_inference_pool().get('age_modifier')
|
151 |
+
age_modifier_inputs = {}
|
152 |
+
|
153 |
+
for age_modifier_input in age_modifier.get_inputs():
|
154 |
+
if age_modifier_input.name == 'target':
|
155 |
+
age_modifier_inputs[age_modifier_input.name] = crop_vision_frame
|
156 |
+
if age_modifier_input.name == 'target_with_background':
|
157 |
+
age_modifier_inputs[age_modifier_input.name] = extend_vision_frame
|
158 |
+
if age_modifier_input.name == 'direction':
|
159 |
+
age_modifier_inputs[age_modifier_input.name] = prepare_direction(state_manager.get_item('age_modifier_direction'))
|
160 |
+
|
161 |
+
with thread_semaphore():
|
162 |
+
crop_vision_frame = age_modifier.run(None, age_modifier_inputs)[0][0]
|
163 |
+
|
164 |
+
return crop_vision_frame
|
165 |
+
|
166 |
+
|
167 |
+
def fix_color(extend_vision_frame_raw : VisionFrame, extend_vision_frame : VisionFrame) -> VisionFrame:
|
168 |
+
color_difference = compute_color_difference(extend_vision_frame_raw, extend_vision_frame, (48, 48))
|
169 |
+
color_difference_mask = create_static_box_mask(extend_vision_frame.shape[:2][::-1], 1.0, (0, 0, 0, 0))
|
170 |
+
color_difference_mask = numpy.stack((color_difference_mask, ) * 3, axis = -1)
|
171 |
+
extend_vision_frame = normalize_color_difference(color_difference, color_difference_mask, extend_vision_frame)
|
172 |
+
return extend_vision_frame
|
173 |
+
|
174 |
+
|
175 |
+
def compute_color_difference(extend_vision_frame_raw : VisionFrame, extend_vision_frame : VisionFrame, size : Size) -> VisionFrame:
|
176 |
+
extend_vision_frame_raw = extend_vision_frame_raw.astype(numpy.float32) / 255
|
177 |
+
extend_vision_frame_raw = cv2.resize(extend_vision_frame_raw, size, interpolation = cv2.INTER_AREA)
|
178 |
+
extend_vision_frame = extend_vision_frame.astype(numpy.float32) / 255
|
179 |
+
extend_vision_frame = cv2.resize(extend_vision_frame, size, interpolation = cv2.INTER_AREA)
|
180 |
+
color_difference = extend_vision_frame_raw - extend_vision_frame
|
181 |
+
return color_difference
|
182 |
+
|
183 |
+
|
184 |
+
def normalize_color_difference(color_difference : VisionFrame, color_difference_mask : Mask, extend_vision_frame : VisionFrame) -> VisionFrame:
|
185 |
+
color_difference = cv2.resize(color_difference, extend_vision_frame.shape[:2][::-1], interpolation = cv2.INTER_CUBIC)
|
186 |
+
color_difference_mask = 1 - color_difference_mask.clip(0, 0.75)
|
187 |
+
extend_vision_frame = extend_vision_frame.astype(numpy.float32) / 255
|
188 |
+
extend_vision_frame += color_difference * color_difference_mask
|
189 |
+
extend_vision_frame = extend_vision_frame.clip(0, 1)
|
190 |
+
extend_vision_frame = numpy.multiply(extend_vision_frame, 255).astype(numpy.uint8)
|
191 |
+
return extend_vision_frame
|
192 |
+
|
193 |
+
|
194 |
+
def prepare_direction(direction : int) -> NDArray[Any]:
|
195 |
+
direction = numpy.interp(float(direction), [ -100, 100 ], [ 2.5, -2.5 ]) #type:ignore[assignment]
|
196 |
+
return numpy.array(direction).astype(numpy.float32)
|
197 |
+
|
198 |
+
|
199 |
+
def prepare_vision_frame(vision_frame : VisionFrame) -> VisionFrame:
|
200 |
+
vision_frame = vision_frame[:, :, ::-1] / 255.0
|
201 |
+
vision_frame = (vision_frame - 0.5) / 0.5
|
202 |
+
vision_frame = numpy.expand_dims(vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
|
203 |
+
return vision_frame
|
204 |
+
|
205 |
+
|
206 |
+
def normalize_extend_frame(extend_vision_frame : VisionFrame) -> VisionFrame:
|
207 |
+
extend_vision_frame = numpy.clip(extend_vision_frame, -1, 1)
|
208 |
+
extend_vision_frame = (extend_vision_frame + 1) / 2
|
209 |
+
extend_vision_frame = extend_vision_frame.transpose(1, 2, 0).clip(0, 255)
|
210 |
+
extend_vision_frame = (extend_vision_frame * 255.0)
|
211 |
+
extend_vision_frame = extend_vision_frame.astype(numpy.uint8)[:, :, ::-1]
|
212 |
+
extend_vision_frame = cv2.pyrDown(extend_vision_frame)
|
213 |
+
return extend_vision_frame
|
214 |
+
|
215 |
+
|
216 |
+
def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
217 |
+
return modify_age(target_face, temp_vision_frame)
|
218 |
+
|
219 |
+
|
220 |
+
def process_frame(inputs : AgeModifierInputs) -> VisionFrame:
|
221 |
+
reference_faces = inputs.get('reference_faces')
|
222 |
+
target_vision_frame = inputs.get('target_vision_frame')
|
223 |
+
many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ]))
|
224 |
+
|
225 |
+
if state_manager.get_item('face_selector_mode') == 'many':
|
226 |
+
if many_faces:
|
227 |
+
for target_face in many_faces:
|
228 |
+
target_vision_frame = modify_age(target_face, target_vision_frame)
|
229 |
+
if state_manager.get_item('face_selector_mode') == 'one':
|
230 |
+
target_face = get_one_face(many_faces)
|
231 |
+
if target_face:
|
232 |
+
target_vision_frame = modify_age(target_face, target_vision_frame)
|
233 |
+
if state_manager.get_item('face_selector_mode') == 'reference':
|
234 |
+
similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance'))
|
235 |
+
if similar_faces:
|
236 |
+
for similar_face in similar_faces:
|
237 |
+
target_vision_frame = modify_age(similar_face, target_vision_frame)
|
238 |
+
return target_vision_frame
|
239 |
+
|
240 |
+
|
241 |
+
def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None:
|
242 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
243 |
+
|
244 |
+
for queue_payload in process_manager.manage(queue_payloads):
|
245 |
+
target_vision_path = queue_payload['frame_path']
|
246 |
+
target_vision_frame = read_image(target_vision_path)
|
247 |
+
output_vision_frame = process_frame(
|
248 |
+
{
|
249 |
+
'reference_faces': reference_faces,
|
250 |
+
'target_vision_frame': target_vision_frame
|
251 |
+
})
|
252 |
+
write_image(target_vision_path, output_vision_frame)
|
253 |
+
update_progress(1)
|
254 |
+
|
255 |
+
|
256 |
+
def process_image(source_path : str, target_path : str, output_path : str) -> None:
|
257 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
258 |
+
target_vision_frame = read_static_image(target_path)
|
259 |
+
output_vision_frame = process_frame(
|
260 |
+
{
|
261 |
+
'reference_faces': reference_faces,
|
262 |
+
'target_vision_frame': target_vision_frame
|
263 |
+
})
|
264 |
+
write_image(output_path, output_vision_frame)
|
265 |
+
|
266 |
+
|
267 |
+
def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None:
|
268 |
+
processors.multi_process_frames(None, temp_frame_paths, process_frames)
|
facefusion/processors/modules/expression_restorer.py
ADDED
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from argparse import ArgumentParser
|
2 |
+
from typing import List, Tuple
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
|
7 |
+
import facefusion.jobs.job_manager
|
8 |
+
import facefusion.jobs.job_store
|
9 |
+
import facefusion.processors.core as processors
|
10 |
+
from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording
|
11 |
+
from facefusion.common_helper import create_int_metavar
|
12 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
13 |
+
from facefusion.face_analyser import get_many_faces, get_one_face
|
14 |
+
from facefusion.face_helper import paste_back, warp_face_by_face_landmark_5
|
15 |
+
from facefusion.face_masker import create_occlusion_mask, create_static_box_mask
|
16 |
+
from facefusion.face_selector import find_similar_faces, sort_and_filter_faces
|
17 |
+
from facefusion.face_store import get_reference_faces
|
18 |
+
from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension
|
19 |
+
from facefusion.processors import choices as processors_choices
|
20 |
+
from facefusion.processors.live_portrait import create_rotation, limit_expression
|
21 |
+
from facefusion.processors.typing import ExpressionRestorerInputs
|
22 |
+
from facefusion.processors.typing import LivePortraitExpression, LivePortraitFeatureVolume, LivePortraitMotionPoints, LivePortraitPitch, LivePortraitRoll, LivePortraitScale, LivePortraitTranslation, LivePortraitYaw
|
23 |
+
from facefusion.program_helper import find_argument_group
|
24 |
+
from facefusion.thread_helper import conditional_thread_semaphore, thread_semaphore
|
25 |
+
from facefusion.typing import ApplyStateItem, Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame
|
26 |
+
from facefusion.vision import get_video_frame, read_image, read_static_image, write_image
|
27 |
+
|
28 |
+
MODEL_SET : ModelSet =\
|
29 |
+
{
|
30 |
+
'live_portrait':
|
31 |
+
{
|
32 |
+
'hashes':
|
33 |
+
{
|
34 |
+
'feature_extractor':
|
35 |
+
{
|
36 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.hash',
|
37 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.hash')
|
38 |
+
},
|
39 |
+
'motion_extractor':
|
40 |
+
{
|
41 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.hash',
|
42 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.hash')
|
43 |
+
},
|
44 |
+
'generator':
|
45 |
+
{
|
46 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.hash',
|
47 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_generator.hash')
|
48 |
+
}
|
49 |
+
},
|
50 |
+
'sources':
|
51 |
+
{
|
52 |
+
'feature_extractor':
|
53 |
+
{
|
54 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.onnx',
|
55 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.onnx')
|
56 |
+
},
|
57 |
+
'motion_extractor':
|
58 |
+
{
|
59 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.onnx',
|
60 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.onnx')
|
61 |
+
},
|
62 |
+
'generator':
|
63 |
+
{
|
64 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.onnx',
|
65 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_generator.onnx')
|
66 |
+
}
|
67 |
+
},
|
68 |
+
'template': 'arcface_128_v2',
|
69 |
+
'size': (512, 512)
|
70 |
+
}
|
71 |
+
}
|
72 |
+
|
73 |
+
|
74 |
+
def get_inference_pool() -> InferencePool:
|
75 |
+
model_sources = get_model_options().get('sources')
|
76 |
+
model_context = __name__ + '.' + state_manager.get_item('expression_restorer_model')
|
77 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
78 |
+
|
79 |
+
|
80 |
+
def clear_inference_pool() -> None:
|
81 |
+
inference_manager.clear_inference_pool(__name__)
|
82 |
+
|
83 |
+
|
84 |
+
def get_model_options() -> ModelOptions:
|
85 |
+
expression_restorer_model = state_manager.get_item('expression_restorer_model')
|
86 |
+
return MODEL_SET.get(expression_restorer_model)
|
87 |
+
|
88 |
+
|
89 |
+
def register_args(program : ArgumentParser) -> None:
|
90 |
+
group_processors = find_argument_group(program, 'processors')
|
91 |
+
if group_processors:
|
92 |
+
group_processors.add_argument('--expression-restorer-model', help = wording.get('help.expression_restorer_model'), default = config.get_str_value('processors.expression_restorer_model', 'live_portrait'), choices = processors_choices.expression_restorer_models)
|
93 |
+
group_processors.add_argument('--expression-restorer-factor', help = wording.get('help.expression_restorer_factor'), type = int, default = config.get_int_value('processors.expression_restorer_factor', '80'), choices = processors_choices.expression_restorer_factor_range, metavar = create_int_metavar(processors_choices.expression_restorer_factor_range))
|
94 |
+
facefusion.jobs.job_store.register_step_keys([ 'expression_restorer_model','expression_restorer_factor' ])
|
95 |
+
|
96 |
+
|
97 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
98 |
+
apply_state_item('expression_restorer_model', args.get('expression_restorer_model'))
|
99 |
+
apply_state_item('expression_restorer_factor', args.get('expression_restorer_factor'))
|
100 |
+
|
101 |
+
|
102 |
+
def pre_check() -> bool:
|
103 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
104 |
+
model_hashes = get_model_options().get('hashes')
|
105 |
+
model_sources = get_model_options().get('sources')
|
106 |
+
|
107 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
108 |
+
|
109 |
+
|
110 |
+
def pre_process(mode : ProcessMode) -> bool:
|
111 |
+
if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')):
|
112 |
+
logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__)
|
113 |
+
return False
|
114 |
+
if mode == 'output' and not in_directory(state_manager.get_item('output_path')):
|
115 |
+
logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__)
|
116 |
+
return False
|
117 |
+
if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]):
|
118 |
+
logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__)
|
119 |
+
return False
|
120 |
+
return True
|
121 |
+
|
122 |
+
|
123 |
+
def post_process() -> None:
|
124 |
+
read_static_image.cache_clear()
|
125 |
+
if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]:
|
126 |
+
clear_inference_pool()
|
127 |
+
if state_manager.get_item('video_memory_strategy') == 'strict':
|
128 |
+
content_analyser.clear_inference_pool()
|
129 |
+
face_classifier.clear_inference_pool()
|
130 |
+
face_detector.clear_inference_pool()
|
131 |
+
face_landmarker.clear_inference_pool()
|
132 |
+
face_masker.clear_inference_pool()
|
133 |
+
face_recognizer.clear_inference_pool()
|
134 |
+
|
135 |
+
|
136 |
+
def restore_expression(source_vision_frame : VisionFrame, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
137 |
+
model_template = get_model_options().get('template')
|
138 |
+
model_size = get_model_options().get('size')
|
139 |
+
expression_restorer_factor = float(numpy.interp(float(state_manager.get_item('expression_restorer_factor')), [ 0, 100 ], [ 0, 1.2 ]))
|
140 |
+
source_vision_frame = cv2.resize(source_vision_frame, temp_vision_frame.shape[:2][::-1])
|
141 |
+
source_crop_vision_frame, _ = warp_face_by_face_landmark_5(source_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size)
|
142 |
+
target_crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size)
|
143 |
+
box_mask = create_static_box_mask(target_crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0))
|
144 |
+
crop_masks =\
|
145 |
+
[
|
146 |
+
box_mask
|
147 |
+
]
|
148 |
+
|
149 |
+
if 'occlusion' in state_manager.get_item('face_mask_types'):
|
150 |
+
occlusion_mask = create_occlusion_mask(target_crop_vision_frame)
|
151 |
+
crop_masks.append(occlusion_mask)
|
152 |
+
|
153 |
+
source_crop_vision_frame = prepare_crop_frame(source_crop_vision_frame)
|
154 |
+
target_crop_vision_frame = prepare_crop_frame(target_crop_vision_frame)
|
155 |
+
target_crop_vision_frame = apply_restore(source_crop_vision_frame, target_crop_vision_frame, expression_restorer_factor)
|
156 |
+
target_crop_vision_frame = normalize_crop_frame(target_crop_vision_frame)
|
157 |
+
crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1)
|
158 |
+
temp_vision_frame = paste_back(temp_vision_frame, target_crop_vision_frame, crop_mask, affine_matrix)
|
159 |
+
return temp_vision_frame
|
160 |
+
|
161 |
+
|
162 |
+
def apply_restore(source_crop_vision_frame : VisionFrame, target_crop_vision_frame : VisionFrame, expression_restorer_factor : float) -> VisionFrame:
|
163 |
+
feature_volume = forward_extract_feature(target_crop_vision_frame)
|
164 |
+
source_expression = forward_extract_motion(source_crop_vision_frame)[5]
|
165 |
+
pitch, yaw, roll, scale, translation, target_expression, motion_points = forward_extract_motion(target_crop_vision_frame)
|
166 |
+
rotation = create_rotation(pitch, yaw, roll)
|
167 |
+
source_expression[:, [ 0, 4, 5, 8, 9 ]] = target_expression[:, [ 0, 4, 5, 8, 9 ]]
|
168 |
+
source_expression = source_expression * expression_restorer_factor + target_expression * (1 - expression_restorer_factor)
|
169 |
+
source_expression = limit_expression(source_expression)
|
170 |
+
source_motion_points = scale * (motion_points @ rotation.T + source_expression) + translation
|
171 |
+
target_motion_points = scale * (motion_points @ rotation.T + target_expression) + translation
|
172 |
+
crop_vision_frame = forward_generate_frame(feature_volume, source_motion_points, target_motion_points)
|
173 |
+
return crop_vision_frame
|
174 |
+
|
175 |
+
|
176 |
+
def forward_extract_feature(crop_vision_frame : VisionFrame) -> LivePortraitFeatureVolume:
|
177 |
+
feature_extractor = get_inference_pool().get('feature_extractor')
|
178 |
+
|
179 |
+
with conditional_thread_semaphore():
|
180 |
+
feature_volume = feature_extractor.run(None,
|
181 |
+
{
|
182 |
+
'input': crop_vision_frame
|
183 |
+
})[0]
|
184 |
+
|
185 |
+
return feature_volume
|
186 |
+
|
187 |
+
|
188 |
+
def forward_extract_motion(crop_vision_frame : VisionFrame) -> Tuple[LivePortraitPitch, LivePortraitYaw, LivePortraitRoll, LivePortraitScale, LivePortraitTranslation, LivePortraitExpression, LivePortraitMotionPoints]:
|
189 |
+
motion_extractor = get_inference_pool().get('motion_extractor')
|
190 |
+
|
191 |
+
with conditional_thread_semaphore():
|
192 |
+
pitch, yaw, roll, scale, translation, expression, motion_points = motion_extractor.run(None,
|
193 |
+
{
|
194 |
+
'input': crop_vision_frame
|
195 |
+
})
|
196 |
+
|
197 |
+
return pitch, yaw, roll, scale, translation, expression, motion_points
|
198 |
+
|
199 |
+
|
200 |
+
def forward_generate_frame(feature_volume : LivePortraitFeatureVolume, source_motion_points : LivePortraitMotionPoints, target_motion_points : LivePortraitMotionPoints) -> VisionFrame:
|
201 |
+
generator = get_inference_pool().get('generator')
|
202 |
+
|
203 |
+
with thread_semaphore():
|
204 |
+
crop_vision_frame = generator.run(None,
|
205 |
+
{
|
206 |
+
'feature_volume': feature_volume,
|
207 |
+
'source': source_motion_points,
|
208 |
+
'target': target_motion_points
|
209 |
+
})[0][0]
|
210 |
+
|
211 |
+
return crop_vision_frame
|
212 |
+
|
213 |
+
|
214 |
+
def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
215 |
+
model_size = get_model_options().get('size')
|
216 |
+
prepare_size = (model_size[0] // 2, model_size[1] // 2)
|
217 |
+
crop_vision_frame = cv2.resize(crop_vision_frame, prepare_size, interpolation = cv2.INTER_AREA)
|
218 |
+
crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0
|
219 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
|
220 |
+
return crop_vision_frame
|
221 |
+
|
222 |
+
|
223 |
+
def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
224 |
+
crop_vision_frame = crop_vision_frame.transpose(1, 2, 0).clip(0, 1)
|
225 |
+
crop_vision_frame = (crop_vision_frame * 255.0)
|
226 |
+
crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1]
|
227 |
+
return crop_vision_frame
|
228 |
+
|
229 |
+
|
230 |
+
def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
231 |
+
pass
|
232 |
+
|
233 |
+
|
234 |
+
def process_frame(inputs : ExpressionRestorerInputs) -> VisionFrame:
|
235 |
+
reference_faces = inputs.get('reference_faces')
|
236 |
+
source_vision_frame = inputs.get('source_vision_frame')
|
237 |
+
target_vision_frame = inputs.get('target_vision_frame')
|
238 |
+
many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ]))
|
239 |
+
|
240 |
+
if state_manager.get_item('face_selector_mode') == 'many':
|
241 |
+
if many_faces:
|
242 |
+
for target_face in many_faces:
|
243 |
+
target_vision_frame = restore_expression(source_vision_frame, target_face, target_vision_frame)
|
244 |
+
if state_manager.get_item('face_selector_mode') == 'one':
|
245 |
+
target_face = get_one_face(many_faces)
|
246 |
+
if target_face:
|
247 |
+
target_vision_frame = restore_expression(source_vision_frame, target_face, target_vision_frame)
|
248 |
+
if state_manager.get_item('face_selector_mode') == 'reference':
|
249 |
+
similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance'))
|
250 |
+
if similar_faces:
|
251 |
+
for similar_face in similar_faces:
|
252 |
+
target_vision_frame = restore_expression(source_vision_frame, similar_face, target_vision_frame)
|
253 |
+
return target_vision_frame
|
254 |
+
|
255 |
+
|
256 |
+
def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None:
|
257 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
258 |
+
|
259 |
+
for queue_payload in process_manager.manage(queue_payloads):
|
260 |
+
frame_number = queue_payload.get('frame_number')
|
261 |
+
if state_manager.get_item('trim_frame_start'):
|
262 |
+
frame_number += state_manager.get_item('trim_frame_start')
|
263 |
+
source_vision_frame = get_video_frame(state_manager.get_item('target_path'), frame_number)
|
264 |
+
target_vision_path = queue_payload.get('frame_path')
|
265 |
+
target_vision_frame = read_image(target_vision_path)
|
266 |
+
output_vision_frame = process_frame(
|
267 |
+
{
|
268 |
+
'reference_faces': reference_faces,
|
269 |
+
'source_vision_frame': source_vision_frame,
|
270 |
+
'target_vision_frame': target_vision_frame
|
271 |
+
})
|
272 |
+
write_image(target_vision_path, output_vision_frame)
|
273 |
+
update_progress(1)
|
274 |
+
|
275 |
+
|
276 |
+
def process_image(source_path : str, target_path : str, output_path : str) -> None:
|
277 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
278 |
+
source_vision_frame = read_static_image(state_manager.get_item('target_path'))
|
279 |
+
target_vision_frame = read_static_image(target_path)
|
280 |
+
output_vision_frame = process_frame(
|
281 |
+
{
|
282 |
+
'reference_faces': reference_faces,
|
283 |
+
'source_vision_frame': source_vision_frame,
|
284 |
+
'target_vision_frame': target_vision_frame
|
285 |
+
})
|
286 |
+
write_image(output_path, output_vision_frame)
|
287 |
+
|
288 |
+
|
289 |
+
def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None:
|
290 |
+
processors.multi_process_frames(None, temp_frame_paths, process_frames)
|
facefusion/processors/modules/face_debugger.py
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from argparse import ArgumentParser
|
2 |
+
from typing import List
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
|
7 |
+
import facefusion.jobs.job_manager
|
8 |
+
import facefusion.jobs.job_store
|
9 |
+
import facefusion.processors.core as processors
|
10 |
+
from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, logger, process_manager, state_manager, wording
|
11 |
+
from facefusion.face_analyser import get_many_faces, get_one_face
|
12 |
+
from facefusion.face_helper import warp_face_by_face_landmark_5
|
13 |
+
from facefusion.face_masker import create_occlusion_mask, create_region_mask, create_static_box_mask
|
14 |
+
from facefusion.face_selector import find_similar_faces, sort_and_filter_faces
|
15 |
+
from facefusion.face_store import get_reference_faces
|
16 |
+
from facefusion.filesystem import in_directory, same_file_extension
|
17 |
+
from facefusion.processors import choices as processors_choices
|
18 |
+
from facefusion.processors.typing import FaceDebuggerInputs
|
19 |
+
from facefusion.program_helper import find_argument_group
|
20 |
+
from facefusion.typing import ApplyStateItem, Args, Face, ProcessMode, QueuePayload, UpdateProgress, VisionFrame
|
21 |
+
from facefusion.vision import read_image, read_static_image, write_image
|
22 |
+
|
23 |
+
|
24 |
+
def get_inference_pool() -> None:
|
25 |
+
pass
|
26 |
+
|
27 |
+
|
28 |
+
def clear_inference_pool() -> None:
|
29 |
+
pass
|
30 |
+
|
31 |
+
|
32 |
+
def register_args(program : ArgumentParser) -> None:
|
33 |
+
group_processors = find_argument_group(program, 'processors')
|
34 |
+
if group_processors:
|
35 |
+
group_processors.add_argument('--face-debugger-items', help = wording.get('help.face_debugger_items').format(choices = ', '.join(processors_choices.face_debugger_items)), default = config.get_str_list('processors.face_debugger_items', 'face-landmark-5/68 face-mask'), choices = processors_choices.face_debugger_items, nargs = '+', metavar = 'FACE_DEBUGGER_ITEMS')
|
36 |
+
facefusion.jobs.job_store.register_step_keys([ 'face_debugger_items' ])
|
37 |
+
|
38 |
+
|
39 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
40 |
+
apply_state_item('face_debugger_items', args.get('face_debugger_items'))
|
41 |
+
|
42 |
+
|
43 |
+
def pre_check() -> bool:
|
44 |
+
return True
|
45 |
+
|
46 |
+
|
47 |
+
def pre_process(mode : ProcessMode) -> bool:
|
48 |
+
if mode == 'output' and not in_directory(state_manager.get_item('output_path')):
|
49 |
+
logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__)
|
50 |
+
return False
|
51 |
+
if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]):
|
52 |
+
logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__)
|
53 |
+
return False
|
54 |
+
return True
|
55 |
+
|
56 |
+
|
57 |
+
def post_process() -> None:
|
58 |
+
read_static_image.cache_clear()
|
59 |
+
if state_manager.get_item('video_memory_strategy') == 'strict':
|
60 |
+
content_analyser.clear_inference_pool()
|
61 |
+
face_classifier.clear_inference_pool()
|
62 |
+
face_detector.clear_inference_pool()
|
63 |
+
face_landmarker.clear_inference_pool()
|
64 |
+
face_masker.clear_inference_pool()
|
65 |
+
face_recognizer.clear_inference_pool()
|
66 |
+
|
67 |
+
|
68 |
+
def debug_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
69 |
+
primary_color = (0, 0, 255)
|
70 |
+
primary_light_color = (100, 100, 255)
|
71 |
+
secondary_color = (0, 255, 0)
|
72 |
+
tertiary_color = (255, 255, 0)
|
73 |
+
bounding_box = target_face.bounding_box.astype(numpy.int32)
|
74 |
+
temp_vision_frame = temp_vision_frame.copy()
|
75 |
+
has_face_landmark_5_fallback = numpy.array_equal(target_face.landmark_set.get('5'), target_face.landmark_set.get('5/68'))
|
76 |
+
has_face_landmark_68_fallback = numpy.array_equal(target_face.landmark_set.get('68'), target_face.landmark_set.get('68/5'))
|
77 |
+
face_debugger_items = state_manager.get_item('face_debugger_items')
|
78 |
+
|
79 |
+
if 'bounding-box' in face_debugger_items:
|
80 |
+
x1, y1, x2, y2 = bounding_box
|
81 |
+
cv2.rectangle(temp_vision_frame, (x1, y1), (x2, y2), primary_color, 2)
|
82 |
+
|
83 |
+
if target_face.angle == 0:
|
84 |
+
cv2.line(temp_vision_frame, (x1, y1), (x2, y1), primary_light_color, 3)
|
85 |
+
elif target_face.angle == 180:
|
86 |
+
cv2.line(temp_vision_frame, (x1, y2), (x2, y2), primary_light_color, 3)
|
87 |
+
elif target_face.angle == 90:
|
88 |
+
cv2.line(temp_vision_frame, (x2, y1), (x2, y2), primary_light_color, 3)
|
89 |
+
elif target_face.angle == 270:
|
90 |
+
cv2.line(temp_vision_frame, (x1, y1), (x1, y2), primary_light_color, 3)
|
91 |
+
|
92 |
+
if 'face-mask' in face_debugger_items:
|
93 |
+
crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), 'arcface_128_v2', (512, 512))
|
94 |
+
inverse_matrix = cv2.invertAffineTransform(affine_matrix)
|
95 |
+
temp_size = temp_vision_frame.shape[:2][::-1]
|
96 |
+
crop_masks = []
|
97 |
+
|
98 |
+
if 'box' in state_manager.get_item('face_mask_types'):
|
99 |
+
box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], 0, state_manager.get_item('face_mask_padding'))
|
100 |
+
crop_masks.append(box_mask)
|
101 |
+
|
102 |
+
if 'occlusion' in state_manager.get_item('face_mask_types'):
|
103 |
+
occlusion_mask = create_occlusion_mask(crop_vision_frame)
|
104 |
+
crop_masks.append(occlusion_mask)
|
105 |
+
|
106 |
+
if 'region' in state_manager.get_item('face_mask_types'):
|
107 |
+
region_mask = create_region_mask(crop_vision_frame, state_manager.get_item('face_mask_regions'))
|
108 |
+
crop_masks.append(region_mask)
|
109 |
+
|
110 |
+
crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1)
|
111 |
+
crop_mask = (crop_mask * 255).astype(numpy.uint8)
|
112 |
+
inverse_vision_frame = cv2.warpAffine(crop_mask, inverse_matrix, temp_size)
|
113 |
+
inverse_vision_frame = cv2.threshold(inverse_vision_frame, 100, 255, cv2.THRESH_BINARY)[1]
|
114 |
+
inverse_vision_frame[inverse_vision_frame > 0] = 255 #type:ignore[operator]
|
115 |
+
inverse_contours = cv2.findContours(inverse_vision_frame, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)[0]
|
116 |
+
cv2.drawContours(temp_vision_frame, inverse_contours, -1, tertiary_color if has_face_landmark_5_fallback else secondary_color, 2)
|
117 |
+
|
118 |
+
if 'face-landmark-5' in face_debugger_items and numpy.any(target_face.landmark_set.get('5')):
|
119 |
+
face_landmark_5 = target_face.landmark_set.get('5').astype(numpy.int32)
|
120 |
+
for index in range(face_landmark_5.shape[0]):
|
121 |
+
cv2.circle(temp_vision_frame, (face_landmark_5[index][0], face_landmark_5[index][1]), 3, primary_color, -1)
|
122 |
+
|
123 |
+
if 'face-landmark-5/68' in face_debugger_items and numpy.any(target_face.landmark_set.get('5/68')):
|
124 |
+
face_landmark_5_68 = target_face.landmark_set.get('5/68').astype(numpy.int32)
|
125 |
+
for index in range(face_landmark_5_68.shape[0]):
|
126 |
+
cv2.circle(temp_vision_frame, (face_landmark_5_68[index][0], face_landmark_5_68[index][1]), 3, tertiary_color if has_face_landmark_5_fallback else secondary_color, -1)
|
127 |
+
|
128 |
+
if 'face-landmark-68' in face_debugger_items and numpy.any(target_face.landmark_set.get('68')):
|
129 |
+
face_landmark_68 = target_face.landmark_set.get('68').astype(numpy.int32)
|
130 |
+
for index in range(face_landmark_68.shape[0]):
|
131 |
+
cv2.circle(temp_vision_frame, (face_landmark_68[index][0], face_landmark_68[index][1]), 3, tertiary_color if has_face_landmark_68_fallback else secondary_color, -1)
|
132 |
+
|
133 |
+
if 'face-landmark-68/5' in face_debugger_items and numpy.any(target_face.landmark_set.get('68')):
|
134 |
+
face_landmark_68 = target_face.landmark_set.get('68/5').astype(numpy.int32)
|
135 |
+
for index in range(face_landmark_68.shape[0]):
|
136 |
+
cv2.circle(temp_vision_frame, (face_landmark_68[index][0], face_landmark_68[index][1]), 3, tertiary_color, -1)
|
137 |
+
|
138 |
+
if bounding_box[3] - bounding_box[1] > 50 and bounding_box[2] - bounding_box[0] > 50:
|
139 |
+
top = bounding_box[1]
|
140 |
+
left = bounding_box[0] - 20
|
141 |
+
|
142 |
+
if 'face-detector-score' in face_debugger_items:
|
143 |
+
face_score_text = str(round(target_face.score_set.get('detector'), 2))
|
144 |
+
top = top + 20
|
145 |
+
cv2.putText(temp_vision_frame, face_score_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2)
|
146 |
+
|
147 |
+
if 'face-landmarker-score' in face_debugger_items:
|
148 |
+
face_score_text = str(round(target_face.score_set.get('landmarker'), 2))
|
149 |
+
top = top + 20
|
150 |
+
cv2.putText(temp_vision_frame, face_score_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, tertiary_color if has_face_landmark_5_fallback else secondary_color, 2)
|
151 |
+
|
152 |
+
if 'age' in face_debugger_items:
|
153 |
+
face_age_text = str(target_face.age.start) + '-' + str(target_face.age.stop)
|
154 |
+
top = top + 20
|
155 |
+
cv2.putText(temp_vision_frame, face_age_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2)
|
156 |
+
|
157 |
+
if 'gender' in face_debugger_items:
|
158 |
+
face_gender_text = target_face.gender
|
159 |
+
top = top + 20
|
160 |
+
cv2.putText(temp_vision_frame, face_gender_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2)
|
161 |
+
|
162 |
+
if 'race' in face_debugger_items:
|
163 |
+
face_race_text = target_face.race
|
164 |
+
top = top + 20
|
165 |
+
cv2.putText(temp_vision_frame, face_race_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2)
|
166 |
+
|
167 |
+
return temp_vision_frame
|
168 |
+
|
169 |
+
|
170 |
+
def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
171 |
+
pass
|
172 |
+
|
173 |
+
|
174 |
+
def process_frame(inputs : FaceDebuggerInputs) -> VisionFrame:
|
175 |
+
reference_faces = inputs.get('reference_faces')
|
176 |
+
target_vision_frame = inputs.get('target_vision_frame')
|
177 |
+
many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ]))
|
178 |
+
|
179 |
+
if state_manager.get_item('face_selector_mode') == 'many':
|
180 |
+
if many_faces:
|
181 |
+
for target_face in many_faces:
|
182 |
+
target_vision_frame = debug_face(target_face, target_vision_frame)
|
183 |
+
if state_manager.get_item('face_selector_mode') == 'one':
|
184 |
+
target_face = get_one_face(many_faces)
|
185 |
+
if target_face:
|
186 |
+
target_vision_frame = debug_face(target_face, target_vision_frame)
|
187 |
+
if state_manager.get_item('face_selector_mode') == 'reference':
|
188 |
+
similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance'))
|
189 |
+
if similar_faces:
|
190 |
+
for similar_face in similar_faces:
|
191 |
+
target_vision_frame = debug_face(similar_face, target_vision_frame)
|
192 |
+
return target_vision_frame
|
193 |
+
|
194 |
+
|
195 |
+
def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None:
|
196 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
197 |
+
|
198 |
+
for queue_payload in process_manager.manage(queue_payloads):
|
199 |
+
target_vision_path = queue_payload['frame_path']
|
200 |
+
target_vision_frame = read_image(target_vision_path)
|
201 |
+
output_vision_frame = process_frame(
|
202 |
+
{
|
203 |
+
'reference_faces': reference_faces,
|
204 |
+
'target_vision_frame': target_vision_frame
|
205 |
+
})
|
206 |
+
write_image(target_vision_path, output_vision_frame)
|
207 |
+
update_progress(1)
|
208 |
+
|
209 |
+
|
210 |
+
def process_image(source_paths : List[str], target_path : str, output_path : str) -> None:
|
211 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
212 |
+
target_vision_frame = read_static_image(target_path)
|
213 |
+
output_vision_frame = process_frame(
|
214 |
+
{
|
215 |
+
'reference_faces': reference_faces,
|
216 |
+
'target_vision_frame': target_vision_frame
|
217 |
+
})
|
218 |
+
write_image(output_path, output_vision_frame)
|
219 |
+
|
220 |
+
|
221 |
+
def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None:
|
222 |
+
processors.multi_process_frames(source_paths, temp_frame_paths, process_frames)
|
facefusion/processors/modules/face_editor.py
ADDED
@@ -0,0 +1,528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from argparse import ArgumentParser
|
2 |
+
from typing import List, Tuple
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
|
7 |
+
import facefusion.jobs.job_manager
|
8 |
+
import facefusion.jobs.job_store
|
9 |
+
import facefusion.processors.core as processors
|
10 |
+
from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording
|
11 |
+
from facefusion.common_helper import create_float_metavar
|
12 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
13 |
+
from facefusion.face_analyser import get_many_faces, get_one_face
|
14 |
+
from facefusion.face_helper import paste_back, scale_face_landmark_5, warp_face_by_face_landmark_5
|
15 |
+
from facefusion.face_masker import create_static_box_mask
|
16 |
+
from facefusion.face_selector import find_similar_faces, sort_and_filter_faces
|
17 |
+
from facefusion.face_store import get_reference_faces
|
18 |
+
from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension
|
19 |
+
from facefusion.processors import choices as processors_choices
|
20 |
+
from facefusion.processors.live_portrait import create_rotation, limit_euler_angles, limit_expression
|
21 |
+
from facefusion.processors.typing import FaceEditorInputs, LivePortraitExpression, LivePortraitFeatureVolume, LivePortraitMotionPoints, LivePortraitPitch, LivePortraitRoll, LivePortraitRotation, LivePortraitScale, LivePortraitTranslation, LivePortraitYaw
|
22 |
+
from facefusion.program_helper import find_argument_group
|
23 |
+
from facefusion.thread_helper import conditional_thread_semaphore, thread_semaphore
|
24 |
+
from facefusion.typing import ApplyStateItem, Args, Face, FaceLandmark68, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame
|
25 |
+
from facefusion.vision import read_image, read_static_image, write_image
|
26 |
+
|
27 |
+
MODEL_SET : ModelSet =\
|
28 |
+
{
|
29 |
+
'live_portrait':
|
30 |
+
{
|
31 |
+
'hashes':
|
32 |
+
{
|
33 |
+
'feature_extractor':
|
34 |
+
{
|
35 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.hash',
|
36 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.hash')
|
37 |
+
},
|
38 |
+
'motion_extractor':
|
39 |
+
{
|
40 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.hash',
|
41 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.hash')
|
42 |
+
},
|
43 |
+
'eye_retargeter':
|
44 |
+
{
|
45 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_eye_retargeter.hash',
|
46 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_eye_retargeter.hash')
|
47 |
+
},
|
48 |
+
'lip_retargeter':
|
49 |
+
{
|
50 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_lip_retargeter.hash',
|
51 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_lip_retargeter.hash')
|
52 |
+
},
|
53 |
+
'stitcher':
|
54 |
+
{
|
55 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_stitcher.hash',
|
56 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_stitcher.hash')
|
57 |
+
},
|
58 |
+
'generator':
|
59 |
+
{
|
60 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.hash',
|
61 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_generator.hash')
|
62 |
+
}
|
63 |
+
},
|
64 |
+
'sources':
|
65 |
+
{
|
66 |
+
'feature_extractor':
|
67 |
+
{
|
68 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.onnx',
|
69 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.onnx')
|
70 |
+
},
|
71 |
+
'motion_extractor':
|
72 |
+
{
|
73 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.onnx',
|
74 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.onnx')
|
75 |
+
},
|
76 |
+
'eye_retargeter':
|
77 |
+
{
|
78 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_eye_retargeter.onnx',
|
79 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_eye_retargeter.onnx')
|
80 |
+
},
|
81 |
+
'lip_retargeter':
|
82 |
+
{
|
83 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_lip_retargeter.onnx',
|
84 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_lip_retargeter.onnx')
|
85 |
+
},
|
86 |
+
'stitcher':
|
87 |
+
{
|
88 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_stitcher.onnx',
|
89 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_stitcher.onnx')
|
90 |
+
},
|
91 |
+
'generator':
|
92 |
+
{
|
93 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.onnx',
|
94 |
+
'path': resolve_relative_path('../.assets/models/live_portrait_generator.onnx')
|
95 |
+
}
|
96 |
+
},
|
97 |
+
'template': 'ffhq_512',
|
98 |
+
'size': (512, 512)
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
|
103 |
+
def get_inference_pool() -> InferencePool:
|
104 |
+
model_sources = get_model_options().get('sources')
|
105 |
+
model_context = __name__ + '.' + state_manager.get_item('face_editor_model')
|
106 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
107 |
+
|
108 |
+
|
109 |
+
def clear_inference_pool() -> None:
|
110 |
+
model_context = __name__ + '.' + state_manager.get_item('face_editor_model')
|
111 |
+
inference_manager.clear_inference_pool(model_context)
|
112 |
+
|
113 |
+
|
114 |
+
def get_model_options() -> ModelOptions:
|
115 |
+
face_editor_model = state_manager.get_item('face_editor_model')
|
116 |
+
return MODEL_SET.get(face_editor_model)
|
117 |
+
|
118 |
+
|
119 |
+
def register_args(program : ArgumentParser) -> None:
|
120 |
+
group_processors = find_argument_group(program, 'processors')
|
121 |
+
if group_processors:
|
122 |
+
group_processors.add_argument('--face-editor-model', help = wording.get('help.face_editor_model'), default = config.get_str_value('processors.face_editor_model', 'live_portrait'), choices = processors_choices.face_editor_models)
|
123 |
+
group_processors.add_argument('--face-editor-eyebrow-direction', help = wording.get('help.face_editor_eyebrow_direction'), type = float, default = config.get_float_value('processors.face_editor_eyebrow_direction', '0'), choices = processors_choices.face_editor_eyebrow_direction_range, metavar = create_float_metavar(processors_choices.face_editor_eyebrow_direction_range))
|
124 |
+
group_processors.add_argument('--face-editor-eye-gaze-horizontal', help = wording.get('help.face_editor_eye_gaze_horizontal'), type = float, default = config.get_float_value('processors.face_editor_eye_gaze_horizontal', '0'), choices = processors_choices.face_editor_eye_gaze_horizontal_range, metavar = create_float_metavar(processors_choices.face_editor_eye_gaze_horizontal_range))
|
125 |
+
group_processors.add_argument('--face-editor-eye-gaze-vertical', help = wording.get('help.face_editor_eye_gaze_vertical'), type = float, default = config.get_float_value('processors.face_editor_eye_gaze_vertical', '0'), choices = processors_choices.face_editor_eye_gaze_vertical_range, metavar = create_float_metavar(processors_choices.face_editor_eye_gaze_vertical_range))
|
126 |
+
group_processors.add_argument('--face-editor-eye-open-ratio', help = wording.get('help.face_editor_eye_open_ratio'), type = float, default = config.get_float_value('processors.face_editor_eye_open_ratio', '0'), choices = processors_choices.face_editor_eye_open_ratio_range, metavar = create_float_metavar(processors_choices.face_editor_eye_open_ratio_range))
|
127 |
+
group_processors.add_argument('--face-editor-lip-open-ratio', help = wording.get('help.face_editor_lip_open_ratio'), type = float, default = config.get_float_value('processors.face_editor_lip_open_ratio', '0'), choices = processors_choices.face_editor_lip_open_ratio_range, metavar = create_float_metavar(processors_choices.face_editor_lip_open_ratio_range))
|
128 |
+
group_processors.add_argument('--face-editor-mouth-grim', help = wording.get('help.face_editor_mouth_grim'), type = float, default = config.get_float_value('processors.face_editor_mouth_grim', '0'), choices = processors_choices.face_editor_mouth_grim_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_grim_range))
|
129 |
+
group_processors.add_argument('--face-editor-mouth-pout', help = wording.get('help.face_editor_mouth_pout'), type = float, default = config.get_float_value('processors.face_editor_mouth_pout', '0'), choices = processors_choices.face_editor_mouth_pout_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_pout_range))
|
130 |
+
group_processors.add_argument('--face-editor-mouth-purse', help = wording.get('help.face_editor_mouth_purse'), type = float, default = config.get_float_value('processors.face_editor_mouth_purse', '0'), choices = processors_choices.face_editor_mouth_purse_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_purse_range))
|
131 |
+
group_processors.add_argument('--face-editor-mouth-smile', help = wording.get('help.face_editor_mouth_smile'), type = float, default = config.get_float_value('processors.face_editor_mouth_smile', '0'), choices = processors_choices.face_editor_mouth_smile_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_smile_range))
|
132 |
+
group_processors.add_argument('--face-editor-mouth-position-horizontal', help = wording.get('help.face_editor_mouth_position_horizontal'), type = float, default = config.get_float_value('processors.face_editor_mouth_position_horizontal', '0'), choices = processors_choices.face_editor_mouth_position_horizontal_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_position_horizontal_range))
|
133 |
+
group_processors.add_argument('--face-editor-mouth-position-vertical', help = wording.get('help.face_editor_mouth_position_vertical'), type = float, default = config.get_float_value('processors.face_editor_mouth_position_vertical', '0'), choices = processors_choices.face_editor_mouth_position_vertical_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_position_vertical_range))
|
134 |
+
group_processors.add_argument('--face-editor-head-pitch', help = wording.get('help.face_editor_head_pitch'), type = float, default = config.get_float_value('processors.face_editor_head_pitch', '0'), choices = processors_choices.face_editor_head_pitch_range, metavar = create_float_metavar(processors_choices.face_editor_head_pitch_range))
|
135 |
+
group_processors.add_argument('--face-editor-head-yaw', help = wording.get('help.face_editor_head_yaw'), type = float, default = config.get_float_value('processors.face_editor_head_yaw', '0'), choices = processors_choices.face_editor_head_yaw_range, metavar = create_float_metavar(processors_choices.face_editor_head_yaw_range))
|
136 |
+
group_processors.add_argument('--face-editor-head-roll', help = wording.get('help.face_editor_head_roll'), type = float, default = config.get_float_value('processors.face_editor_head_roll', '0'), choices = processors_choices.face_editor_head_roll_range, metavar = create_float_metavar(processors_choices.face_editor_head_roll_range))
|
137 |
+
facefusion.jobs.job_store.register_step_keys([ 'face_editor_model', 'face_editor_eyebrow_direction', 'face_editor_eye_gaze_horizontal', 'face_editor_eye_gaze_vertical', 'face_editor_eye_open_ratio', 'face_editor_lip_open_ratio', 'face_editor_mouth_grim', 'face_editor_mouth_pout', 'face_editor_mouth_purse', 'face_editor_mouth_smile', 'face_editor_mouth_position_horizontal', 'face_editor_mouth_position_vertical', 'face_editor_head_pitch', 'face_editor_head_yaw', 'face_editor_head_roll' ])
|
138 |
+
|
139 |
+
|
140 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
141 |
+
apply_state_item('face_editor_model', args.get('face_editor_model'))
|
142 |
+
apply_state_item('face_editor_eyebrow_direction', args.get('face_editor_eyebrow_direction'))
|
143 |
+
apply_state_item('face_editor_eye_gaze_horizontal', args.get('face_editor_eye_gaze_horizontal'))
|
144 |
+
apply_state_item('face_editor_eye_gaze_vertical', args.get('face_editor_eye_gaze_vertical'))
|
145 |
+
apply_state_item('face_editor_eye_open_ratio', args.get('face_editor_eye_open_ratio'))
|
146 |
+
apply_state_item('face_editor_lip_open_ratio', args.get('face_editor_lip_open_ratio'))
|
147 |
+
apply_state_item('face_editor_mouth_grim', args.get('face_editor_mouth_grim'))
|
148 |
+
apply_state_item('face_editor_mouth_pout', args.get('face_editor_mouth_pout'))
|
149 |
+
apply_state_item('face_editor_mouth_purse', args.get('face_editor_mouth_purse'))
|
150 |
+
apply_state_item('face_editor_mouth_smile', args.get('face_editor_mouth_smile'))
|
151 |
+
apply_state_item('face_editor_mouth_position_horizontal', args.get('face_editor_mouth_position_horizontal'))
|
152 |
+
apply_state_item('face_editor_mouth_position_vertical', args.get('face_editor_mouth_position_vertical'))
|
153 |
+
apply_state_item('face_editor_head_pitch', args.get('face_editor_head_pitch'))
|
154 |
+
apply_state_item('face_editor_head_yaw', args.get('face_editor_head_yaw'))
|
155 |
+
apply_state_item('face_editor_head_roll', args.get('face_editor_head_roll'))
|
156 |
+
|
157 |
+
|
158 |
+
def pre_check() -> bool:
|
159 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
160 |
+
model_hashes = get_model_options().get('hashes')
|
161 |
+
model_sources = get_model_options().get('sources')
|
162 |
+
|
163 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
164 |
+
|
165 |
+
|
166 |
+
def pre_process(mode : ProcessMode) -> bool:
|
167 |
+
if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')):
|
168 |
+
logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__)
|
169 |
+
return False
|
170 |
+
if mode == 'output' and not in_directory(state_manager.get_item('output_path')):
|
171 |
+
logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__)
|
172 |
+
return False
|
173 |
+
if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]):
|
174 |
+
logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__)
|
175 |
+
return False
|
176 |
+
return True
|
177 |
+
|
178 |
+
|
179 |
+
def post_process() -> None:
|
180 |
+
read_static_image.cache_clear()
|
181 |
+
if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]:
|
182 |
+
clear_inference_pool()
|
183 |
+
if state_manager.get_item('video_memory_strategy') == 'strict':
|
184 |
+
content_analyser.clear_inference_pool()
|
185 |
+
face_classifier.clear_inference_pool()
|
186 |
+
face_detector.clear_inference_pool()
|
187 |
+
face_landmarker.clear_inference_pool()
|
188 |
+
face_masker.clear_inference_pool()
|
189 |
+
face_recognizer.clear_inference_pool()
|
190 |
+
|
191 |
+
|
192 |
+
def edit_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
193 |
+
model_template = get_model_options().get('template')
|
194 |
+
model_size = get_model_options().get('size')
|
195 |
+
face_landmark_5 = scale_face_landmark_5(target_face.landmark_set.get('5/68'), 1.5)
|
196 |
+
crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, model_size)
|
197 |
+
box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0))
|
198 |
+
crop_vision_frame = prepare_crop_frame(crop_vision_frame)
|
199 |
+
crop_vision_frame = apply_edit(crop_vision_frame, target_face.landmark_set.get('68'))
|
200 |
+
crop_vision_frame = normalize_crop_frame(crop_vision_frame)
|
201 |
+
temp_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, box_mask, affine_matrix)
|
202 |
+
return temp_vision_frame
|
203 |
+
|
204 |
+
|
205 |
+
def apply_edit(crop_vision_frame : VisionFrame, face_landmark_68 : FaceLandmark68) -> VisionFrame:
|
206 |
+
feature_volume = forward_extract_feature(crop_vision_frame)
|
207 |
+
pitch, yaw, roll, scale, translation, expression, motion_points = forward_extract_motion(crop_vision_frame)
|
208 |
+
rotation = create_rotation(pitch, yaw, roll)
|
209 |
+
motion_points_target = scale * (motion_points @ rotation.T + expression) + translation
|
210 |
+
expression = edit_eye_gaze(expression)
|
211 |
+
expression = edit_mouth_grim(expression)
|
212 |
+
expression = edit_mouth_position(expression)
|
213 |
+
expression = edit_mouth_pout(expression)
|
214 |
+
expression = edit_mouth_purse(expression)
|
215 |
+
expression = edit_mouth_smile(expression)
|
216 |
+
expression = edit_eyebrow_direction(expression)
|
217 |
+
expression = limit_expression(expression)
|
218 |
+
rotation = edit_head_rotation(pitch, yaw, roll)
|
219 |
+
motion_points_source = motion_points @ rotation.T
|
220 |
+
motion_points_source += expression
|
221 |
+
motion_points_source *= scale
|
222 |
+
motion_points_source += translation
|
223 |
+
motion_points_source += edit_eye_open(motion_points_target, face_landmark_68)
|
224 |
+
motion_points_source += edit_lip_open(motion_points_target, face_landmark_68)
|
225 |
+
motion_points_source = forward_stitch_motion_points(motion_points_source, motion_points_target)
|
226 |
+
crop_vision_frame = forward_generate_frame(feature_volume, motion_points_source, motion_points_target)
|
227 |
+
return crop_vision_frame
|
228 |
+
|
229 |
+
|
230 |
+
def forward_extract_feature(crop_vision_frame : VisionFrame) -> LivePortraitFeatureVolume:
|
231 |
+
feature_extractor = get_inference_pool().get('feature_extractor')
|
232 |
+
|
233 |
+
with conditional_thread_semaphore():
|
234 |
+
feature_volume = feature_extractor.run(None,
|
235 |
+
{
|
236 |
+
'input': crop_vision_frame
|
237 |
+
})[0]
|
238 |
+
|
239 |
+
return feature_volume
|
240 |
+
|
241 |
+
|
242 |
+
def forward_extract_motion(crop_vision_frame : VisionFrame) -> Tuple[LivePortraitPitch, LivePortraitYaw, LivePortraitRoll, LivePortraitScale, LivePortraitTranslation, LivePortraitExpression, LivePortraitMotionPoints]:
|
243 |
+
motion_extractor = get_inference_pool().get('motion_extractor')
|
244 |
+
|
245 |
+
with conditional_thread_semaphore():
|
246 |
+
pitch, yaw, roll, scale, translation, expression, motion_points = motion_extractor.run(None,
|
247 |
+
{
|
248 |
+
'input': crop_vision_frame
|
249 |
+
})
|
250 |
+
|
251 |
+
return pitch, yaw, roll, scale, translation, expression, motion_points
|
252 |
+
|
253 |
+
|
254 |
+
def forward_retarget_eye(eye_motion_points : LivePortraitMotionPoints) -> LivePortraitMotionPoints:
|
255 |
+
eye_retargeter = get_inference_pool().get('eye_retargeter')
|
256 |
+
|
257 |
+
with conditional_thread_semaphore():
|
258 |
+
eye_motion_points = eye_retargeter.run(None,
|
259 |
+
{
|
260 |
+
'input': eye_motion_points
|
261 |
+
})[0]
|
262 |
+
|
263 |
+
return eye_motion_points
|
264 |
+
|
265 |
+
|
266 |
+
def forward_retarget_lip(lip_motion_points : LivePortraitMotionPoints) -> LivePortraitMotionPoints:
|
267 |
+
lip_retargeter = get_inference_pool().get('lip_retargeter')
|
268 |
+
|
269 |
+
with conditional_thread_semaphore():
|
270 |
+
lip_motion_points = lip_retargeter.run(None,
|
271 |
+
{
|
272 |
+
'input': lip_motion_points
|
273 |
+
})[0]
|
274 |
+
|
275 |
+
return lip_motion_points
|
276 |
+
|
277 |
+
|
278 |
+
def forward_stitch_motion_points(source_motion_points : LivePortraitMotionPoints, target_motion_points : LivePortraitMotionPoints) -> LivePortraitMotionPoints:
|
279 |
+
stitcher = get_inference_pool().get('stitcher')
|
280 |
+
|
281 |
+
with thread_semaphore():
|
282 |
+
motion_points = stitcher.run(None,
|
283 |
+
{
|
284 |
+
'source': source_motion_points,
|
285 |
+
'target': target_motion_points
|
286 |
+
})[0]
|
287 |
+
|
288 |
+
return motion_points
|
289 |
+
|
290 |
+
|
291 |
+
def forward_generate_frame(feature_volume : LivePortraitFeatureVolume, source_motion_points : LivePortraitMotionPoints, target_motion_points : LivePortraitMotionPoints) -> VisionFrame:
|
292 |
+
generator = get_inference_pool().get('generator')
|
293 |
+
|
294 |
+
with thread_semaphore():
|
295 |
+
crop_vision_frame = generator.run(None,
|
296 |
+
{
|
297 |
+
'feature_volume': feature_volume,
|
298 |
+
'source': source_motion_points,
|
299 |
+
'target': target_motion_points
|
300 |
+
})[0][0]
|
301 |
+
|
302 |
+
return crop_vision_frame
|
303 |
+
|
304 |
+
|
305 |
+
def edit_eyebrow_direction(expression : LivePortraitExpression) -> LivePortraitExpression:
|
306 |
+
face_editor_eyebrow = state_manager.get_item('face_editor_eyebrow_direction')
|
307 |
+
|
308 |
+
if face_editor_eyebrow > 0:
|
309 |
+
expression[0, 1, 1] += numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.015, 0.015 ])
|
310 |
+
expression[0, 2, 1] -= numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.020, 0.020 ])
|
311 |
+
else:
|
312 |
+
expression[0, 1, 0] -= numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.015, 0.015 ])
|
313 |
+
expression[0, 2, 0] += numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.020, 0.020 ])
|
314 |
+
expression[0, 1, 1] += numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.005, 0.005 ])
|
315 |
+
expression[0, 2, 1] -= numpy.interp(face_editor_eyebrow, [ -1, 1 ], [ -0.005, 0.005 ])
|
316 |
+
return expression
|
317 |
+
|
318 |
+
|
319 |
+
def edit_eye_gaze(expression : LivePortraitExpression) -> LivePortraitExpression:
|
320 |
+
face_editor_eye_gaze_horizontal = state_manager.get_item('face_editor_eye_gaze_horizontal')
|
321 |
+
face_editor_eye_gaze_vertical = state_manager.get_item('face_editor_eye_gaze_vertical')
|
322 |
+
|
323 |
+
if face_editor_eye_gaze_horizontal > 0:
|
324 |
+
expression[0, 11, 0] += numpy.interp(face_editor_eye_gaze_horizontal, [ -1, 1 ], [ -0.015, 0.015 ])
|
325 |
+
expression[0, 15, 0] += numpy.interp(face_editor_eye_gaze_horizontal, [ -1, 1 ], [ -0.020, 0.020 ])
|
326 |
+
else:
|
327 |
+
expression[0, 11, 0] += numpy.interp(face_editor_eye_gaze_horizontal, [ -1, 1 ], [ -0.020, 0.020 ])
|
328 |
+
expression[0, 15, 0] += numpy.interp(face_editor_eye_gaze_horizontal, [ -1, 1 ], [ -0.015, 0.015 ])
|
329 |
+
expression[0, 1, 1] += numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.0025, 0.0025 ])
|
330 |
+
expression[0, 2, 1] -= numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.0025, 0.0025 ])
|
331 |
+
expression[0, 11, 1] -= numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.010, 0.010 ])
|
332 |
+
expression[0, 13, 1] -= numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.005, 0.005 ])
|
333 |
+
expression[0, 15, 1] -= numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.010, 0.010 ])
|
334 |
+
expression[0, 16, 1] -= numpy.interp(face_editor_eye_gaze_vertical, [ -1, 1 ], [ -0.005, 0.005 ])
|
335 |
+
return expression
|
336 |
+
|
337 |
+
|
338 |
+
def edit_eye_open(motion_points : LivePortraitMotionPoints, face_landmark_68 : FaceLandmark68) -> LivePortraitMotionPoints:
|
339 |
+
face_editor_eye_open_ratio = state_manager.get_item('face_editor_eye_open_ratio')
|
340 |
+
left_eye_ratio = calc_distance_ratio(face_landmark_68, 37, 40, 39, 36)
|
341 |
+
right_eye_ratio = calc_distance_ratio(face_landmark_68, 43, 46, 45, 42)
|
342 |
+
|
343 |
+
if face_editor_eye_open_ratio < 0:
|
344 |
+
eye_motion_points = numpy.concatenate([ motion_points.ravel(), [ left_eye_ratio, right_eye_ratio, 0.0 ] ])
|
345 |
+
else:
|
346 |
+
eye_motion_points = numpy.concatenate([ motion_points.ravel(), [ left_eye_ratio, right_eye_ratio, 0.6 ] ])
|
347 |
+
eye_motion_points = eye_motion_points.reshape(1, -1).astype(numpy.float32)
|
348 |
+
eye_motion_points = forward_retarget_eye(eye_motion_points) * numpy.abs(face_editor_eye_open_ratio)
|
349 |
+
eye_motion_points = eye_motion_points.reshape(-1, 21, 3)
|
350 |
+
return eye_motion_points
|
351 |
+
|
352 |
+
|
353 |
+
def edit_lip_open(motion_points : LivePortraitMotionPoints, face_landmark_68 : FaceLandmark68) -> LivePortraitMotionPoints:
|
354 |
+
face_editor_lip_open_ratio = state_manager.get_item('face_editor_lip_open_ratio')
|
355 |
+
lip_ratio = calc_distance_ratio(face_landmark_68, 62, 66, 54, 48)
|
356 |
+
|
357 |
+
if face_editor_lip_open_ratio < 0:
|
358 |
+
lip_motion_points = numpy.concatenate([ motion_points.ravel(), [ lip_ratio, 0.0 ] ])
|
359 |
+
else:
|
360 |
+
lip_motion_points = numpy.concatenate([ motion_points.ravel(), [ lip_ratio, 1.0 ] ])
|
361 |
+
lip_motion_points = lip_motion_points.reshape(1, -1).astype(numpy.float32)
|
362 |
+
lip_motion_points = forward_retarget_lip(lip_motion_points) * numpy.abs(face_editor_lip_open_ratio)
|
363 |
+
lip_motion_points = lip_motion_points.reshape(-1, 21, 3)
|
364 |
+
return lip_motion_points
|
365 |
+
|
366 |
+
|
367 |
+
def edit_mouth_grim(expression : LivePortraitExpression) -> LivePortraitExpression:
|
368 |
+
face_editor_mouth_grim = state_manager.get_item('face_editor_mouth_grim')
|
369 |
+
if face_editor_mouth_grim > 0:
|
370 |
+
expression[0, 17, 2] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.005, 0.005 ])
|
371 |
+
expression[0, 19, 2] += numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.01, 0.01 ])
|
372 |
+
expression[0, 20, 1] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.06, 0.06 ])
|
373 |
+
expression[0, 20, 2] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.03, 0.03 ])
|
374 |
+
else:
|
375 |
+
expression[0, 19, 1] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.05, 0.05 ])
|
376 |
+
expression[0, 19, 2] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.02, 0.02 ])
|
377 |
+
expression[0, 20, 2] -= numpy.interp(face_editor_mouth_grim, [ -1, 1 ], [ -0.03, 0.03 ])
|
378 |
+
return expression
|
379 |
+
|
380 |
+
|
381 |
+
def edit_mouth_position(expression : LivePortraitExpression) -> LivePortraitExpression:
|
382 |
+
face_editor_mouth_position_horizontal = state_manager.get_item('face_editor_mouth_position_horizontal')
|
383 |
+
face_editor_mouth_position_vertical = state_manager.get_item('face_editor_mouth_position_vertical')
|
384 |
+
expression[0, 19, 0] += numpy.interp(face_editor_mouth_position_horizontal, [ -1, 1 ], [ -0.05, 0.05 ])
|
385 |
+
expression[0, 20, 0] += numpy.interp(face_editor_mouth_position_horizontal, [ -1, 1 ], [ -0.04, 0.04 ])
|
386 |
+
if face_editor_mouth_position_vertical > 0:
|
387 |
+
expression[0, 19, 1] -= numpy.interp(face_editor_mouth_position_vertical, [ -1, 1 ], [ -0.04, 0.04 ])
|
388 |
+
expression[0, 20, 1] -= numpy.interp(face_editor_mouth_position_vertical, [ -1, 1 ], [ -0.02, 0.02 ])
|
389 |
+
else:
|
390 |
+
expression[0, 19, 1] -= numpy.interp(face_editor_mouth_position_vertical, [ -1, 1 ], [ -0.05, 0.05 ])
|
391 |
+
expression[0, 20, 1] -= numpy.interp(face_editor_mouth_position_vertical, [ -1, 1 ], [ -0.04, 0.04 ])
|
392 |
+
return expression
|
393 |
+
|
394 |
+
|
395 |
+
def edit_mouth_pout(expression : LivePortraitExpression) -> LivePortraitExpression:
|
396 |
+
face_editor_mouth_pout = state_manager.get_item('face_editor_mouth_pout')
|
397 |
+
if face_editor_mouth_pout > 0:
|
398 |
+
expression[0, 19, 1] -= numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.022, 0.022 ])
|
399 |
+
expression[0, 19, 2] += numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.025, 0.025 ])
|
400 |
+
expression[0, 20, 2] -= numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.002, 0.002 ])
|
401 |
+
else:
|
402 |
+
expression[0, 19, 1] += numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.022, 0.022 ])
|
403 |
+
expression[0, 19, 2] += numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.025, 0.025 ])
|
404 |
+
expression[0, 20, 2] -= numpy.interp(face_editor_mouth_pout, [ -1, 1 ], [ -0.002, 0.002 ])
|
405 |
+
return expression
|
406 |
+
|
407 |
+
|
408 |
+
def edit_mouth_purse(expression : LivePortraitExpression) -> LivePortraitExpression:
|
409 |
+
face_editor_mouth_purse = state_manager.get_item('face_editor_mouth_purse')
|
410 |
+
if face_editor_mouth_purse > 0:
|
411 |
+
expression[0, 19, 1] -= numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.04, 0.04 ])
|
412 |
+
expression[0, 19, 2] -= numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.02, 0.02 ])
|
413 |
+
else:
|
414 |
+
expression[0, 14, 1] -= numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.02, 0.02 ])
|
415 |
+
expression[0, 17, 2] += numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.01, 0.01 ])
|
416 |
+
expression[0, 19, 2] -= numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.015, 0.015 ])
|
417 |
+
expression[0, 20, 2] -= numpy.interp(face_editor_mouth_purse, [ -1, 1 ], [ -0.002, 0.002 ])
|
418 |
+
return expression
|
419 |
+
|
420 |
+
|
421 |
+
def edit_mouth_smile(expression : LivePortraitExpression) -> LivePortraitExpression:
|
422 |
+
face_editor_mouth_smile = state_manager.get_item('face_editor_mouth_smile')
|
423 |
+
if face_editor_mouth_smile > 0:
|
424 |
+
expression[0, 20, 1] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.015, 0.015 ])
|
425 |
+
expression[0, 14, 1] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.025, 0.025 ])
|
426 |
+
expression[0, 17, 1] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.01, 0.01 ])
|
427 |
+
expression[0, 17, 2] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.004, 0.004 ])
|
428 |
+
expression[0, 3, 1] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.0045, 0.0045 ])
|
429 |
+
expression[0, 7, 1] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.0045, 0.0045 ])
|
430 |
+
else:
|
431 |
+
expression[0, 14, 1] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.02, 0.02 ])
|
432 |
+
expression[0, 17, 1] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.003, 0.003 ])
|
433 |
+
expression[0, 19, 1] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.02, 0.02 ])
|
434 |
+
expression[0, 19, 2] -= numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.005, 0.005 ])
|
435 |
+
expression[0, 20, 2] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.01, 0.01 ])
|
436 |
+
expression[0, 3, 1] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.0045, 0.0045 ])
|
437 |
+
expression[0, 7, 1] += numpy.interp(face_editor_mouth_smile, [ -1, 1 ], [ -0.0045, 0.0045 ])
|
438 |
+
return expression
|
439 |
+
|
440 |
+
|
441 |
+
def edit_head_rotation(pitch : LivePortraitPitch, yaw : LivePortraitYaw, roll : LivePortraitRoll) -> LivePortraitRotation:
|
442 |
+
face_editor_head_pitch = state_manager.get_item('face_editor_head_pitch')
|
443 |
+
face_editor_head_yaw = state_manager.get_item('face_editor_head_yaw')
|
444 |
+
face_editor_head_roll = state_manager.get_item('face_editor_head_roll')
|
445 |
+
edit_pitch = pitch + float(numpy.interp(face_editor_head_pitch, [ -1, 1 ], [ 20, -20 ]))
|
446 |
+
edit_yaw = yaw + float(numpy.interp(face_editor_head_yaw, [ -1, 1 ], [ 60, -60 ]))
|
447 |
+
edit_roll = roll + float(numpy.interp(face_editor_head_roll, [ -1, 1 ], [ -15, 15 ]))
|
448 |
+
edit_pitch, edit_yaw, edit_roll = limit_euler_angles(pitch, yaw, roll, edit_pitch, edit_yaw, edit_roll)
|
449 |
+
rotation = create_rotation(edit_pitch, edit_yaw, edit_roll)
|
450 |
+
return rotation
|
451 |
+
|
452 |
+
|
453 |
+
def calc_distance_ratio(face_landmark_68 : FaceLandmark68, top_index : int, bottom_index : int, left_index : int, right_index : int) -> float:
|
454 |
+
vertical_direction = face_landmark_68[top_index] - face_landmark_68[bottom_index]
|
455 |
+
horizontal_direction = face_landmark_68[left_index] - face_landmark_68[right_index]
|
456 |
+
distance_ratio = float(numpy.linalg.norm(vertical_direction) / (numpy.linalg.norm(horizontal_direction) + 1e-6))
|
457 |
+
return distance_ratio
|
458 |
+
|
459 |
+
|
460 |
+
def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
461 |
+
model_size = get_model_options().get('size')
|
462 |
+
prepare_size = (model_size[0] // 2, model_size[1] // 2)
|
463 |
+
crop_vision_frame = cv2.resize(crop_vision_frame, prepare_size, interpolation = cv2.INTER_AREA)
|
464 |
+
crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0
|
465 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
|
466 |
+
return crop_vision_frame
|
467 |
+
|
468 |
+
|
469 |
+
def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
470 |
+
crop_vision_frame = crop_vision_frame.transpose(1, 2, 0).clip(0, 1)
|
471 |
+
crop_vision_frame = (crop_vision_frame * 255.0)
|
472 |
+
crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1]
|
473 |
+
return crop_vision_frame
|
474 |
+
|
475 |
+
|
476 |
+
def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
477 |
+
pass
|
478 |
+
|
479 |
+
|
480 |
+
def process_frame(inputs : FaceEditorInputs) -> VisionFrame:
|
481 |
+
reference_faces = inputs.get('reference_faces')
|
482 |
+
target_vision_frame = inputs.get('target_vision_frame')
|
483 |
+
many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ]))
|
484 |
+
|
485 |
+
if state_manager.get_item('face_selector_mode') == 'many':
|
486 |
+
if many_faces:
|
487 |
+
for target_face in many_faces:
|
488 |
+
target_vision_frame = edit_face(target_face, target_vision_frame)
|
489 |
+
if state_manager.get_item('face_selector_mode') == 'one':
|
490 |
+
target_face = get_one_face(many_faces)
|
491 |
+
if target_face:
|
492 |
+
target_vision_frame = edit_face(target_face, target_vision_frame)
|
493 |
+
if state_manager.get_item('face_selector_mode') == 'reference':
|
494 |
+
similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance'))
|
495 |
+
if similar_faces:
|
496 |
+
for similar_face in similar_faces:
|
497 |
+
target_vision_frame = edit_face(similar_face, target_vision_frame)
|
498 |
+
return target_vision_frame
|
499 |
+
|
500 |
+
|
501 |
+
def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None:
|
502 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
503 |
+
|
504 |
+
for queue_payload in process_manager.manage(queue_payloads):
|
505 |
+
target_vision_path = queue_payload['frame_path']
|
506 |
+
target_vision_frame = read_image(target_vision_path)
|
507 |
+
output_vision_frame = process_frame(
|
508 |
+
{
|
509 |
+
'reference_faces': reference_faces,
|
510 |
+
'target_vision_frame': target_vision_frame
|
511 |
+
})
|
512 |
+
write_image(target_vision_path, output_vision_frame)
|
513 |
+
update_progress(1)
|
514 |
+
|
515 |
+
|
516 |
+
def process_image(source_path : str, target_path : str, output_path : str) -> None:
|
517 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
518 |
+
target_vision_frame = read_static_image(target_path)
|
519 |
+
output_vision_frame = process_frame(
|
520 |
+
{
|
521 |
+
'reference_faces': reference_faces,
|
522 |
+
'target_vision_frame': target_vision_frame
|
523 |
+
})
|
524 |
+
write_image(output_path, output_vision_frame)
|
525 |
+
|
526 |
+
|
527 |
+
def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None:
|
528 |
+
processors.multi_process_frames(None, temp_frame_paths, process_frames)
|
facefusion/processors/modules/face_enhancer.py
ADDED
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from argparse import ArgumentParser
|
2 |
+
from typing import List
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import numpy
|
6 |
+
|
7 |
+
import facefusion.jobs.job_manager
|
8 |
+
import facefusion.jobs.job_store
|
9 |
+
import facefusion.processors.core as processors
|
10 |
+
from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording
|
11 |
+
from facefusion.common_helper import create_int_metavar
|
12 |
+
from facefusion.download import conditional_download_hashes, conditional_download_sources
|
13 |
+
from facefusion.face_analyser import get_many_faces, get_one_face
|
14 |
+
from facefusion.face_helper import paste_back, warp_face_by_face_landmark_5
|
15 |
+
from facefusion.face_masker import create_occlusion_mask, create_static_box_mask
|
16 |
+
from facefusion.face_selector import find_similar_faces, sort_and_filter_faces
|
17 |
+
from facefusion.face_store import get_reference_faces
|
18 |
+
from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension
|
19 |
+
from facefusion.processors import choices as processors_choices
|
20 |
+
from facefusion.processors.typing import FaceEnhancerInputs
|
21 |
+
from facefusion.program_helper import find_argument_group
|
22 |
+
from facefusion.thread_helper import thread_semaphore
|
23 |
+
from facefusion.typing import ApplyStateItem, Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame
|
24 |
+
from facefusion.vision import read_image, read_static_image, write_image
|
25 |
+
|
26 |
+
MODEL_SET : ModelSet =\
|
27 |
+
{
|
28 |
+
'codeformer':
|
29 |
+
{
|
30 |
+
'hashes':
|
31 |
+
{
|
32 |
+
'face_enhancer':
|
33 |
+
{
|
34 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/codeformer.hash',
|
35 |
+
'path': resolve_relative_path('../.assets/models/codeformer.hash')
|
36 |
+
}
|
37 |
+
},
|
38 |
+
'sources':
|
39 |
+
{
|
40 |
+
'face_enhancer':
|
41 |
+
{
|
42 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/codeformer.onnx',
|
43 |
+
'path': resolve_relative_path('../.assets/models/codeformer.onnx')
|
44 |
+
}
|
45 |
+
},
|
46 |
+
'template': 'ffhq_512',
|
47 |
+
'size': (512, 512)
|
48 |
+
},
|
49 |
+
'gfpgan_1.2':
|
50 |
+
{
|
51 |
+
'hashes':
|
52 |
+
{
|
53 |
+
'face_enhancer':
|
54 |
+
{
|
55 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.2.hash',
|
56 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.2.hash')
|
57 |
+
}
|
58 |
+
},
|
59 |
+
'sources':
|
60 |
+
{
|
61 |
+
'face_enhancer':
|
62 |
+
{
|
63 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.2.onnx',
|
64 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.2.onnx')
|
65 |
+
}
|
66 |
+
},
|
67 |
+
'template': 'ffhq_512',
|
68 |
+
'size': (512, 512)
|
69 |
+
},
|
70 |
+
'gfpgan_1.3':
|
71 |
+
{
|
72 |
+
'hashes':
|
73 |
+
{
|
74 |
+
'face_enhancer':
|
75 |
+
{
|
76 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.3.hash',
|
77 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.3.hash')
|
78 |
+
}
|
79 |
+
},
|
80 |
+
'sources':
|
81 |
+
{
|
82 |
+
'face_enhancer':
|
83 |
+
{
|
84 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.3.onnx',
|
85 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.3.onnx')
|
86 |
+
}
|
87 |
+
},
|
88 |
+
'template': 'ffhq_512',
|
89 |
+
'size': (512, 512)
|
90 |
+
},
|
91 |
+
'gfpgan_1.4':
|
92 |
+
{
|
93 |
+
'hashes':
|
94 |
+
{
|
95 |
+
'face_enhancer':
|
96 |
+
{
|
97 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.4.hash',
|
98 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.4.hash')
|
99 |
+
}
|
100 |
+
},
|
101 |
+
'sources':
|
102 |
+
{
|
103 |
+
'face_enhancer':
|
104 |
+
{
|
105 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.4.onnx',
|
106 |
+
'path': resolve_relative_path('../.assets/models/gfpgan_1.4.onnx')
|
107 |
+
}
|
108 |
+
},
|
109 |
+
'template': 'ffhq_512',
|
110 |
+
'size': (512, 512)
|
111 |
+
},
|
112 |
+
'gpen_bfr_256':
|
113 |
+
{
|
114 |
+
'hashes':
|
115 |
+
{
|
116 |
+
'face_enhancer':
|
117 |
+
{
|
118 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_256.hash',
|
119 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_256.hash')
|
120 |
+
}
|
121 |
+
},
|
122 |
+
'sources':
|
123 |
+
{
|
124 |
+
'face_enhancer':
|
125 |
+
{
|
126 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_256.onnx',
|
127 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_256.onnx')
|
128 |
+
}
|
129 |
+
},
|
130 |
+
'template': 'arcface_128_v2',
|
131 |
+
'size': (256, 256)
|
132 |
+
},
|
133 |
+
'gpen_bfr_512':
|
134 |
+
{
|
135 |
+
'hashes':
|
136 |
+
{
|
137 |
+
'face_enhancer':
|
138 |
+
{
|
139 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_512.hash',
|
140 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_512.hash')
|
141 |
+
}
|
142 |
+
},
|
143 |
+
'sources':
|
144 |
+
{
|
145 |
+
'face_enhancer':
|
146 |
+
{
|
147 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_512.onnx',
|
148 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_512.onnx')
|
149 |
+
}
|
150 |
+
},
|
151 |
+
'template': 'ffhq_512',
|
152 |
+
'size': (512, 512)
|
153 |
+
},
|
154 |
+
'gpen_bfr_1024':
|
155 |
+
{
|
156 |
+
'hashes':
|
157 |
+
{
|
158 |
+
'face_enhancer':
|
159 |
+
{
|
160 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_1024.hash',
|
161 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_1024.hash')
|
162 |
+
}
|
163 |
+
},
|
164 |
+
'sources':
|
165 |
+
{
|
166 |
+
'face_enhancer':
|
167 |
+
{
|
168 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_1024.onnx',
|
169 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_1024.onnx')
|
170 |
+
}
|
171 |
+
},
|
172 |
+
'template': 'ffhq_512',
|
173 |
+
'size': (1024, 1024)
|
174 |
+
},
|
175 |
+
'gpen_bfr_2048':
|
176 |
+
{
|
177 |
+
'hashes':
|
178 |
+
{
|
179 |
+
'face_enhancer':
|
180 |
+
{
|
181 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_2048.hash',
|
182 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_2048.hash')
|
183 |
+
}
|
184 |
+
},
|
185 |
+
'sources':
|
186 |
+
{
|
187 |
+
'face_enhancer':
|
188 |
+
{
|
189 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_2048.onnx',
|
190 |
+
'path': resolve_relative_path('../.assets/models/gpen_bfr_2048.onnx')
|
191 |
+
}
|
192 |
+
},
|
193 |
+
'template': 'ffhq_512',
|
194 |
+
'size': (2048, 2048)
|
195 |
+
},
|
196 |
+
'restoreformer_plus_plus':
|
197 |
+
{
|
198 |
+
'hashes':
|
199 |
+
{
|
200 |
+
'face_enhancer':
|
201 |
+
{
|
202 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/restoreformer_plus_plus.hash',
|
203 |
+
'path': resolve_relative_path('../.assets/models/restoreformer_plus_plus.hash')
|
204 |
+
}
|
205 |
+
},
|
206 |
+
'sources':
|
207 |
+
{
|
208 |
+
'face_enhancer':
|
209 |
+
{
|
210 |
+
'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/restoreformer_plus_plus.onnx',
|
211 |
+
'path': resolve_relative_path('../.assets/models/restoreformer_plus_plus.onnx')
|
212 |
+
}
|
213 |
+
},
|
214 |
+
'template': 'ffhq_512',
|
215 |
+
'size': (512, 512)
|
216 |
+
}
|
217 |
+
}
|
218 |
+
|
219 |
+
|
220 |
+
def get_inference_pool() -> InferencePool:
|
221 |
+
model_sources = get_model_options().get('sources')
|
222 |
+
model_context = __name__ + '.' + state_manager.get_item('face_enhancer_model')
|
223 |
+
return inference_manager.get_inference_pool(model_context, model_sources)
|
224 |
+
|
225 |
+
|
226 |
+
def clear_inference_pool() -> None:
|
227 |
+
model_context = __name__ + '.' + state_manager.get_item('face_enhancer_model')
|
228 |
+
inference_manager.clear_inference_pool(model_context)
|
229 |
+
|
230 |
+
|
231 |
+
def get_model_options() -> ModelOptions:
|
232 |
+
face_enhancer_model = state_manager.get_item('face_enhancer_model')
|
233 |
+
return MODEL_SET.get(face_enhancer_model)
|
234 |
+
|
235 |
+
|
236 |
+
def register_args(program : ArgumentParser) -> None:
|
237 |
+
group_processors = find_argument_group(program, 'processors')
|
238 |
+
if group_processors:
|
239 |
+
group_processors.add_argument('--face-enhancer-model', help = wording.get('help.face_enhancer_model'), default = config.get_str_value('processors.face_enhancer_model', 'gfpgan_1.4'), choices = processors_choices.face_enhancer_models)
|
240 |
+
group_processors.add_argument('--face-enhancer-blend', help = wording.get('help.face_enhancer_blend'), type = int, default = config.get_int_value('processors.face_enhancer_blend', '80'), choices = processors_choices.face_enhancer_blend_range, metavar = create_int_metavar(processors_choices.face_enhancer_blend_range))
|
241 |
+
facefusion.jobs.job_store.register_step_keys([ 'face_enhancer_model', 'face_enhancer_blend' ])
|
242 |
+
|
243 |
+
|
244 |
+
def apply_args(args : Args, apply_state_item : ApplyStateItem) -> None:
|
245 |
+
apply_state_item('face_enhancer_model', args.get('face_enhancer_model'))
|
246 |
+
apply_state_item('face_enhancer_blend', args.get('face_enhancer_blend'))
|
247 |
+
|
248 |
+
|
249 |
+
def pre_check() -> bool:
|
250 |
+
download_directory_path = resolve_relative_path('../.assets/models')
|
251 |
+
model_hashes = get_model_options().get('hashes')
|
252 |
+
model_sources = get_model_options().get('sources')
|
253 |
+
|
254 |
+
return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources)
|
255 |
+
|
256 |
+
|
257 |
+
def pre_process(mode : ProcessMode) -> bool:
|
258 |
+
if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')):
|
259 |
+
logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__)
|
260 |
+
return False
|
261 |
+
if mode == 'output' and not in_directory(state_manager.get_item('output_path')):
|
262 |
+
logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__)
|
263 |
+
return False
|
264 |
+
if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]):
|
265 |
+
logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__)
|
266 |
+
return False
|
267 |
+
return True
|
268 |
+
|
269 |
+
|
270 |
+
def post_process() -> None:
|
271 |
+
read_static_image.cache_clear()
|
272 |
+
if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]:
|
273 |
+
clear_inference_pool()
|
274 |
+
if state_manager.get_item('video_memory_strategy') == 'strict':
|
275 |
+
content_analyser.clear_inference_pool()
|
276 |
+
face_classifier.clear_inference_pool()
|
277 |
+
face_detector.clear_inference_pool()
|
278 |
+
face_landmarker.clear_inference_pool()
|
279 |
+
face_masker.clear_inference_pool()
|
280 |
+
face_recognizer.clear_inference_pool()
|
281 |
+
|
282 |
+
|
283 |
+
def enhance_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
284 |
+
model_template = get_model_options().get('template')
|
285 |
+
model_size = get_model_options().get('size')
|
286 |
+
crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size)
|
287 |
+
box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0))
|
288 |
+
crop_masks =\
|
289 |
+
[
|
290 |
+
box_mask
|
291 |
+
]
|
292 |
+
|
293 |
+
if 'occlusion' in state_manager.get_item('face_mask_types'):
|
294 |
+
occlusion_mask = create_occlusion_mask(crop_vision_frame)
|
295 |
+
crop_masks.append(occlusion_mask)
|
296 |
+
|
297 |
+
crop_vision_frame = prepare_crop_frame(crop_vision_frame)
|
298 |
+
crop_vision_frame = forward(crop_vision_frame)
|
299 |
+
crop_vision_frame = normalize_crop_frame(crop_vision_frame)
|
300 |
+
crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1)
|
301 |
+
paste_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, crop_mask, affine_matrix)
|
302 |
+
temp_vision_frame = blend_frame(temp_vision_frame, paste_vision_frame)
|
303 |
+
return temp_vision_frame
|
304 |
+
|
305 |
+
|
306 |
+
def forward(crop_vision_frame : VisionFrame) -> VisionFrame:
|
307 |
+
face_enhancer = get_inference_pool().get('face_enhancer')
|
308 |
+
face_enhancer_inputs = {}
|
309 |
+
|
310 |
+
for face_enhancer_input in face_enhancer.get_inputs():
|
311 |
+
if face_enhancer_input.name == 'input':
|
312 |
+
face_enhancer_inputs[face_enhancer_input.name] = crop_vision_frame
|
313 |
+
if face_enhancer_input.name == 'weight':
|
314 |
+
weight = numpy.array([ 1 ]).astype(numpy.double)
|
315 |
+
face_enhancer_inputs[face_enhancer_input.name] = weight
|
316 |
+
|
317 |
+
with thread_semaphore():
|
318 |
+
crop_vision_frame = face_enhancer.run(None, face_enhancer_inputs)[0][0]
|
319 |
+
|
320 |
+
return crop_vision_frame
|
321 |
+
|
322 |
+
|
323 |
+
def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
324 |
+
crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0
|
325 |
+
crop_vision_frame = (crop_vision_frame - 0.5) / 0.5
|
326 |
+
crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32)
|
327 |
+
return crop_vision_frame
|
328 |
+
|
329 |
+
|
330 |
+
def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame:
|
331 |
+
crop_vision_frame = numpy.clip(crop_vision_frame, -1, 1)
|
332 |
+
crop_vision_frame = (crop_vision_frame + 1) / 2
|
333 |
+
crop_vision_frame = crop_vision_frame.transpose(1, 2, 0)
|
334 |
+
crop_vision_frame = (crop_vision_frame * 255.0).round()
|
335 |
+
crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1]
|
336 |
+
return crop_vision_frame
|
337 |
+
|
338 |
+
|
339 |
+
def blend_frame(temp_vision_frame : VisionFrame, paste_vision_frame : VisionFrame) -> VisionFrame:
|
340 |
+
face_enhancer_blend = 1 - (state_manager.get_item('face_enhancer_blend') / 100)
|
341 |
+
temp_vision_frame = cv2.addWeighted(temp_vision_frame, face_enhancer_blend, paste_vision_frame, 1 - face_enhancer_blend, 0)
|
342 |
+
return temp_vision_frame
|
343 |
+
|
344 |
+
|
345 |
+
def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame:
|
346 |
+
return enhance_face(target_face, temp_vision_frame)
|
347 |
+
|
348 |
+
|
349 |
+
def process_frame(inputs : FaceEnhancerInputs) -> VisionFrame:
|
350 |
+
reference_faces = inputs.get('reference_faces')
|
351 |
+
target_vision_frame = inputs.get('target_vision_frame')
|
352 |
+
many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ]))
|
353 |
+
|
354 |
+
if state_manager.get_item('face_selector_mode') == 'many':
|
355 |
+
if many_faces:
|
356 |
+
for target_face in many_faces:
|
357 |
+
target_vision_frame = enhance_face(target_face, target_vision_frame)
|
358 |
+
if state_manager.get_item('face_selector_mode') == 'one':
|
359 |
+
target_face = get_one_face(many_faces)
|
360 |
+
if target_face:
|
361 |
+
target_vision_frame = enhance_face(target_face, target_vision_frame)
|
362 |
+
if state_manager.get_item('face_selector_mode') == 'reference':
|
363 |
+
similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance'))
|
364 |
+
if similar_faces:
|
365 |
+
for similar_face in similar_faces:
|
366 |
+
target_vision_frame = enhance_face(similar_face, target_vision_frame)
|
367 |
+
return target_vision_frame
|
368 |
+
|
369 |
+
|
370 |
+
def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None:
|
371 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
372 |
+
|
373 |
+
for queue_payload in process_manager.manage(queue_payloads):
|
374 |
+
target_vision_path = queue_payload['frame_path']
|
375 |
+
target_vision_frame = read_image(target_vision_path)
|
376 |
+
output_vision_frame = process_frame(
|
377 |
+
{
|
378 |
+
'reference_faces': reference_faces,
|
379 |
+
'target_vision_frame': target_vision_frame
|
380 |
+
})
|
381 |
+
write_image(target_vision_path, output_vision_frame)
|
382 |
+
update_progress(1)
|
383 |
+
|
384 |
+
|
385 |
+
def process_image(source_path : str, target_path : str, output_path : str) -> None:
|
386 |
+
reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None
|
387 |
+
target_vision_frame = read_static_image(target_path)
|
388 |
+
output_vision_frame = process_frame(
|
389 |
+
{
|
390 |
+
'reference_faces': reference_faces,
|
391 |
+
'target_vision_frame': target_vision_frame
|
392 |
+
})
|
393 |
+
write_image(output_path, output_vision_frame)
|
394 |
+
|
395 |
+
|
396 |
+
def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None:
|
397 |
+
processors.multi_process_frames(None, temp_frame_paths, process_frames)
|