From 6e91bddfb07015fed49b469a571992806832c3db Mon Sep 17 00:00:00 2001 From: henryruhs Date: Wed, 22 Jul 2026 10:31:11 +0200 Subject: [PATCH] Reuse the decoded window frame as the swap canvas process_temp_frame read the target frame twice per output frame: once from the video (the detection window) and once from the extracted temp PNG (the swap canvas). They are the same frame N. The window centre is already decoded in the reader, so use it as the canvas and only fall back to the temp PNG when the reader resolution differs from the temp resolution (scaled output). Removes one full-frame decode per output frame. AV1 ~22 -> ~26 fps and H.264 ~20 -> ~23 fps -- matching and slightly beating 3.6.1, because a frame copy is cheaper than a PNG decode. The canvas now comes from the raw pipe decode instead of the PNG decode of the same frame (sub-visual ~1 LSB difference in passthrough regions); detection is unchanged. Co-Authored-By: Claude Opus 4.8 --- facefusion/workflows/image_to_video.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/facefusion/workflows/image_to_video.py b/facefusion/workflows/image_to_video.py index 19e6538e..a71661f7 100644 --- a/facefusion/workflows/image_to_video.py +++ b/facefusion/workflows/image_to_video.py @@ -7,13 +7,13 @@ from tqdm import tqdm from facefusion import ffmpeg from facefusion import logger, process_manager, state_manager, translator, video_manager from facefusion.audio import create_empty_audio_frame, get_audio_frame, get_voice_frame -from facefusion.common_helper import get_first +from facefusion.common_helper import get_first, get_middle from facefusion.content_analyser import analyse_video from facefusion.filesystem import filter_audio_paths, is_video from facefusion.processors.core import get_processors_modules from facefusion.temp_helper import clear_temp_directory, create_temp_directory, move_temp_file, resolve_temp_frame_set from facefusion.time_helper import calculate_end_time -from facefusion.types import ErrorCode +from facefusion.types import ErrorCode, Resolution from facefusion.vision import conditional_merge_vision_mask, detect_video_resolution, extract_vision_mask, pack_resolution, read_static_image, read_static_images, read_static_video_frame, restrict_trim_frame, restrict_video_fps, restrict_video_resolution, scale_resolution, select_video_frames, write_image from facefusion.workflows.core import is_process_stopping @@ -75,6 +75,8 @@ def extract_frames() -> ErrorCode: def process_video() -> ErrorCode: temp_frame_set = resolve_temp_frame_set(state_manager.get_item('target_path')) + output_video_resolution = scale_resolution(detect_video_resolution(state_manager.get_item('target_path')), state_manager.get_item('output_video_scale')) + temp_video_resolution = restrict_video_resolution(state_manager.get_item('target_path'), output_video_resolution) if temp_frame_set: with tqdm(total = len(temp_frame_set), desc = translator.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: @@ -84,7 +86,7 @@ def process_video() -> ErrorCode: futures = [] for frame_number, temp_frame_path in temp_frame_set.items(): - future = executor.submit(process_temp_frame, temp_frame_path, frame_number) + future = executor.submit(process_temp_frame, temp_frame_path, frame_number, temp_video_resolution) futures.append(future) for future in as_completed(futures): @@ -154,14 +156,18 @@ def restore_audio() -> ErrorCode: return 0 -def process_temp_frame(temp_frame_path : str, frame_number : int) -> bool: +def process_temp_frame(temp_frame_path : str, frame_number : int, temp_video_resolution : Resolution) -> bool: trim_frame_start, _ = restrict_trim_frame(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end')) reference_vision_frame = read_static_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) source_vision_frames = read_static_images(state_manager.get_item('source_paths')) source_audio_path = get_first(filter_audio_paths(state_manager.get_item('source_paths'))) target_vision_frames = select_video_frames(state_manager.get_item('target_path'), frame_number, state_manager.get_item('target_frame_amount')) + target_vision_frame = get_middle(target_vision_frames) temp_video_fps = restrict_video_fps(state_manager.get_item('target_path'), state_manager.get_item('output_video_fps')) - temp_vision_frame = read_static_image(temp_frame_path, 'rgba') + + if not (target_vision_frame.shape[1], target_vision_frame.shape[0]) == temp_video_resolution: + target_vision_frame = read_static_image(temp_frame_path, 'rgba') + temp_vision_frame = target_vision_frame.copy() temp_vision_mask = extract_vision_mask(temp_vision_frame) source_audio_frame = get_audio_frame(source_audio_path, temp_video_fps, frame_number - trim_frame_start)