""" 文件路径: FaceSwap/app.py 创建时间: 未知 上次修改时间: 未知 开发者: aidaox 项目说明: 这是一个基于Gradio的人脸交换(Face Swapping)Web应用程序。 项目使用insightface库进行人脸检测和交换,支持多种使用场景: 1. 单张照片的人脸交换 2. 一个源人脸对应多个目标照片 3. 多个源人脸对应一个目标照片 4. 多个源人脸对应多个目标照片 5. 自定义人脸映射(指定哪个源人脸替换目标照片中的哪个人脸) 6. 视频中的人脸交换(支持单个人脸、所有人脸、自定义映射) 7. 一个源人脸对应多个视频 技术栈: - Gradio: 用于构建Web界面 - OpenCV (cv2): 图像和视频处理 - NumPy: 数值计算和数组操作 - insightface: 人脸检测和交换的核心库 - FFmpeg: 视频音频处理(通过subprocess调用) """ # ==================== 导入必要的库 ==================== import gradio as gr # Gradio库,用于快速构建Web界面 import os # 操作系统接口,用于文件路径操作 import cv2 # OpenCV库,用于图像和视频处理 import numpy as np # NumPy库,用于数组操作和数值计算 import shutil # 文件操作库,用于复制、删除文件 import subprocess # 子进程管理,用于调用外部命令(如FFmpeg) import time # 时间库,用于计算处理耗时 from SinglePhoto import FaceSwapper # 导入人脸交换核心类 import argparse # 命令行参数解析库 # ==================== 全局变量 ==================== # 欢迎消息:柔和 indigo 渐变,低饱和、低对比阴影,与整体暖白背景协调 wellcomingMessage = """

Face Swapping Free

Photos, videos, batch — one tool

""" # 创建全局的人脸交换器实例,所有函数共享使用 # 这样可以避免重复初始化,提高性能 swapper = FaceSwapper() # ==================== 照片交换功能函数 ==================== def swap_single_photo(src_img, src_idx, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)): """ 功能: 单张照片的人脸交换 这是最基础的功能,将源照片中的一个人脸替换到目标照片中的一个人脸上 参数说明: src_img: numpy数组,源图像(包含要提取的人脸) src_idx: 整数,源图像中要使用的人脸索引(从1开始,1表示第一张脸) dst_img: numpy数组,目标图像(要被替换人脸的图像) dst_idx: 整数,目标图像中要被替换的人脸索引(从1开始) progress: Gradio进度条对象,用于显示处理进度 返回值: output_path: 字符串,交换后图像的保存路径 log: 字符串,处理过程的日志信息 工作流程: 1. 保存源图像和目标图像到临时文件 2. 调用FaceSwapper进行人脸交换 3. 保存结果图像 4. 清理临时文件 5. 返回结果路径和日志 """ log = "" # 初始化日志字符串,用于记录处理过程 start_time = time.time() # 记录开始时间,用于计算总耗时 try: # 步骤1: 准备文件路径和目录 progress(0, desc="Preparing files") # 更新进度条,显示"准备文件" src_path = "SinglePhoto/data_src.jpg" # 源图像临时保存路径 dst_path = "SinglePhoto/data_dst.jpg" # 目标图像临时保存路径 output_path = "SinglePhoto/output_swapped.jpg" # 输出图像保存路径 # 确保目录存在,exist_ok=True表示如果目录已存在则不报错 os.makedirs(os.path.dirname(src_path), exist_ok=True) os.makedirs(os.path.dirname(dst_path), exist_ok=True) os.makedirs(os.path.dirname(output_path), exist_ok=True) # 步骤2: 转换颜色空间并保存图像 # Gradio返回的图像是RGB格式,OpenCV使用BGR格式,需要转换 src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) # 保存源图像 cv2.imwrite(dst_path, dst_img_bgr) # 保存目标图像 log += f"Saved source to {src_path}, destination to {dst_path}\n" # 步骤3: 执行人脸交换 progress(0.5, desc="Swapping faces") # 更新进度条到50% # 调用FaceSwapper的swap_faces方法进行人脸交换 result = swapper.swap_faces(src_path, int(src_idx), dst_path, int(dst_idx)) cv2.imwrite(output_path, result) # 保存交换后的结果 log += f"Swapped and saved result to {output_path}\n" # 步骤4: 清理临时文件 progress(0.8, desc="Cleaning up") # 更新进度条到80% try: if os.path.exists(src_path): os.remove(src_path) # 删除源图像临时文件 if os.path.exists(dst_path): os.remove(dst_path) # 删除目标图像临时文件 log += "Cleaned up temp files.\n" except Exception as cleanup_error: log += f"Cleanup error: {cleanup_error}\n" # 记录清理错误但不中断流程 # 步骤5: 完成处理 progress(1, desc="Done") # 更新进度条到100% elapsed = time.time() - start_time # 计算总耗时 log += f"Elapsed time: {elapsed:.2f} seconds\n" return output_path, log # 返回结果路径和日志 except Exception as e: # 错误处理:如果处理过程中出现任何异常,记录错误并返回 log = f"[FAILED] Processing failed. See details below.\n" + log log += f"Error: {e}\n" progress(1, desc="Error") # 更新进度条显示错误状态 elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return None, log # 返回None表示失败,同时返回错误日志 def swap_single_src_multi_dst(src_img, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)): """ 功能: 一个源人脸对应多个目标照片 将同一个源照片中的人脸,分别替换到多个目标照片中 参数说明: src_img: numpy数组,源图像(包含要提取的人脸) dst_imgs: 图像列表,多个目标图像(Gradio Gallery组件返回的列表) dst_indices: 字符串或列表,每个目标图像中要替换的人脸索引 例如 "1,1,2" 表示第一个目标用索引1,第二个用索引1,第三个用索引2 progress: Gradio进度条对象 返回值: results: 列表,所有交换后图像的路径列表 log: 字符串,处理过程的日志信息 注意: 这个函数在代码中出现了两次(第59行和第205行),可能是重复定义,建议删除其中一个 """ log = "" # 初始化日志 results = [] # 初始化结果列表,用于存储所有输出图像的路径 # 定义工作目录 src_dir = "SingleSrcMultiDst/src" # 源图像目录 dst_dir = "SingleSrcMultiDst/dst" # 目标图像目录 output_dir = "SingleSrcMultiDst/output" # 输出图像目录 # 创建必要的目录 os.makedirs(src_dir, exist_ok=True) os.makedirs(dst_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) # 处理源图像(Gradio可能返回元组格式,需要提取实际图像) if isinstance(src_img, tuple): src_img = src_img[0] # 如果是元组,取第一个元素(实际图像) # 保存源图像 src_path = os.path.join(src_dir, "data_src.jpg") src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) log += f"Saved source image to {src_path}\n" progress(0.05, desc="Saved source image") # 解析目标人脸索引(支持字符串和列表两种格式) if isinstance(dst_indices, str): # 如果是字符串,按逗号分割并转换为整数列表 dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] else: # 如果是列表,直接转换为整数列表 dst_indices_list = [int(idx) for idx in dst_indices] # 遍历每个目标图像,进行人脸交换 for j, dst_img in enumerate(dst_imgs): if isinstance(dst_img, tuple): dst_img = dst_img[0] # 处理元组格式 # 为每个目标图像创建保存路径 dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg") output_path = os.path.join(output_dir, f"output_swapped_{j}.jpg") # 转换颜色空间并保存目标图像 dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) cv2.imwrite(dst_path, dst_img_bgr) log += f"Saved destination image {j} to {dst_path}\n" try: # 获取当前目标图像对应的人脸索引(如果索引不够,默认使用1) dst_idx = dst_indices_list[j] if j < len(dst_indices_list) else 1 # 执行人脸交换(源图像使用第1个人脸,目标图像使用指定的索引) result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) cv2.imwrite(output_path, result) # 保存结果 results.append(output_path) # 添加到结果列表 log += f"Swapped and saved result to {output_path}\n" except Exception as e: # 如果交换失败,记录错误但继续处理下一个 results.append(f"Error: {e}") log += f"Error swapping with destination {j}: {e}\n" # 更新进度条 progress((j + 1) / len(dst_imgs), desc=f"Swapping destination {j+1}/{len(dst_imgs)}") progress(1, desc="Done") return results, log def swap_multi_src_single_dst(src_imgs, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)): """ 功能: 多个源人脸对应一个目标照片 将多个源照片中的人脸,分别替换到同一个目标照片中的同一个人脸上 每次交换都是独立的,生成多个结果图像 参数说明: src_imgs: 图像列表,多个源图像(Gradio Gallery组件返回的列表) dst_img: numpy数组,目标图像(要被替换人脸的图像) dst_idx: 整数,目标图像中要被替换的人脸索引(从1开始) progress: Gradio进度条对象 返回值: results: 列表,所有交换后图像的路径列表 log: 字符串,处理过程的日志信息 使用场景: 例如,想看看不同人的脸放在同一个目标照片上的效果 """ log = "" # 初始化日志 results = [] # 初始化结果列表 # 定义工作目录 src_dir = "MultiSrcSingleDst/src" # 源图像目录 dst_dir = "MultiSrcSingleDst/dst" # 目标图像目录 output_dir = "MultiSrcSingleDst/output" # 输出图像目录 # 创建必要的目录 os.makedirs(src_dir, exist_ok=True) os.makedirs(dst_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) # 处理并保存目标图像 if isinstance(dst_img, tuple): dst_img = dst_img[0] # 处理元组格式 dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) dst_path = os.path.join(dst_dir, "data_dst.jpg") cv2.imwrite(dst_path, dst_img_bgr) log += f"Saved destination image to {dst_path}\n" progress(0.05, desc="Saved destination image") # 遍历每个源图像,分别与目标图像进行交换 for i, src_img in enumerate(src_imgs): if isinstance(src_img, tuple): src_img = src_img[0] # 处理元组格式 # 为每个源图像创建保存路径 src_path = os.path.join(src_dir, f"data_src_{i}.jpg") output_path = os.path.join(output_dir, f"output_swapped_{i}.jpg") # 转换颜色空间并保存源图像 src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) log += f"Saved source image {i} to {src_path}\n" try: # 执行人脸交换(每个源图像使用第1个人脸,目标图像使用指定的索引) result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) cv2.imwrite(output_path, result) # 保存结果 results.append(output_path) # 添加到结果列表 log += f"Swapped and saved result to {output_path}\n" except Exception as e: # 如果交换失败,记录错误但继续处理下一个 results.append(f"Error: {e}") log += f"Error swapping source {i}: {e}\n" # 更新进度条 progress((i + 1) / len(src_imgs), desc=f"Swapping source {i+1}/{len(src_imgs)}") progress(1, desc="Done") return results, log def swap_multi_src_multi_dst(src_imgs, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)): """ 功能: 多个源人脸对应多个目标照片(笛卡尔积) 将每个源图像中的人脸,分别替换到每个目标图像中 如果有3个源图像和2个目标图像,将生成3×2=6个结果图像 参数说明: src_imgs: 图像列表,多个源图像 dst_imgs: 图像列表,多个目标图像 dst_indices: 字符串或列表,每个目标图像中要替换的人脸索引 例如 "1,2" 表示第一个目标用索引1,第二个用索引2 progress: Gradio进度条对象 返回值: results: 列表,所有交换后图像的路径列表 log: 字符串,处理过程的日志信息 使用场景: 批量处理,例如有多个人的照片,想分别替换到多个场景中 """ log = "" # 初始化日志 results = [] # 初始化结果列表 # 定义工作目录 src_dir = "MultiSrcMultiDst/src" # 源图像目录 dst_dir = "MultiSrcMultiDst/dst" # 目标图像目录 output_dir = "MultiSrcMultiDst/output" # 输出图像目录 # 创建必要的目录 os.makedirs(src_dir, exist_ok=True) os.makedirs(dst_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) # 解析目标人脸索引 if isinstance(dst_indices, str): # 如果是字符串,按逗号分割并转换为整数列表 dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] else: # 如果是列表,直接转换为整数列表 dst_indices_list = [int(idx) for idx in dst_indices] # 计算总任务数(用于进度条) total = max(1, len(src_imgs) * len(dst_imgs)) count = 0 # 已完成任务计数 # 外层循环:遍历每个源图像 for i, src_img in enumerate(src_imgs): if isinstance(src_img, tuple): src_img = src_img[0] # 处理元组格式 # 验证源图像有效性 if src_img is None: results.append(f"Error: Source image at index {i} is None") log += f"Source image at index {i} is None\n" continue # 跳过无效的源图像 # 保存源图像(支持numpy数组和文件路径两种格式) src_path = os.path.join(src_dir, f"data_src_{i}.jpg") if isinstance(src_img, np.ndarray): # 如果是numpy数组,转换颜色空间并保存 src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) log += f"Saved source image {i} to {src_path}\n" elif isinstance(src_img, str) and os.path.exists(src_img): # 如果是文件路径,直接复制 shutil.copy(src_img, src_path) log += f"Copied source image {i} from {src_img} to {src_path}\n" else: # 无效的源图像 results.append(f"Error: Invalid source image at index {i}") log += f"Invalid source image at index {i}\n" continue # 跳过无效的源图像 # 内层循环:遍历每个目标图像 for j, dst_img in enumerate(dst_imgs): if isinstance(dst_img, tuple): dst_img = dst_img[0] # 处理元组格式 # 验证目标图像有效性 if dst_img is None: results.append(f"Error: Destination image at index {j} is None") log += f"Destination image at index {j} is None\n" continue # 跳过无效的目标图像 # 创建输出路径(使用i_j格式区分不同的组合) dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg") output_path = os.path.join(output_dir, f"output_swapped_{i}_{j}.jpg") # 保存目标图像(支持numpy数组和文件路径两种格式) if isinstance(dst_img, np.ndarray): dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) cv2.imwrite(dst_path, dst_img_bgr) log += f"Saved destination image {j} to {dst_path}\n" elif isinstance(dst_img, str) and os.path.exists(dst_img): shutil.copy(dst_img, dst_path) log += f"Copied destination image {j} from {dst_img} to {dst_path}\n" else: results.append(f"Error: Invalid destination image at index {j}") log += f"Invalid destination image at index {j}\n" continue # 跳过无效的目标图像 # 执行人脸交换 try: # 获取当前目标图像对应的人脸索引(如果索引不够,默认使用1) dst_idx = dst_indices_list[j] if j < len(dst_indices_list) else 1 # 执行交换(源图像使用第1个人脸,目标图像使用指定的索引) result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) cv2.imwrite(output_path, result) # 保存结果 results.append(output_path) # 添加到结果列表 log += f"Swapped src {i} with dst {j} and saved to {output_path}\n" except Exception as e: # 如果交换失败,记录错误但继续处理 results.append(f"Error: {e}") log += f"Error swapping src {i} with dst {j}: {e}\n" # 更新进度条 count += 1 progress(count / total, desc=f"Swapping ({count}/{total})") progress(1, desc="Done") return results, log def swap_faces_custom(src_imgs, dst_img, mapping_str, progress=gr.Progress(track_tqdm=True)): """ 功能: 自定义人脸映射(一张目标照片,多个源人脸,自定义替换规则) 这是最灵活的功能,可以指定目标照片中的每个人脸分别用哪个源人脸替换 参数说明: src_imgs: 图像列表,多个源图像(Gradio Gallery组件返回的列表) dst_img: numpy数组,目标图像(要被替换人脸的图像) mapping_str: 字符串,逗号分隔的映射关系 例如 "2,1,3" 表示: - 目标照片中的第1个人脸用源图像2中的人脸替换 - 目标照片中的第2个人脸用源图像1中的人脸替换 - 目标照片中的第3个人脸用源图像3中的人脸替换 progress: Gradio进度条对象 返回值: output_path: 字符串,交换后图像的保存路径 log: 字符串,处理过程的日志信息 使用场景: 例如目标照片中有3个人,想分别用3个不同源照片中的人脸替换 """ log = "" # 初始化日志 start_time = time.time() # 记录开始时间 # 定义工作目录和文件路径 dst_path = "CustomSwap/data_dst.jpg" # 目标图像路径 output_path = "CustomSwap/output_swapped.jpg" # 输出图像路径 src_dir = "CustomSwap/src" # 源图像目录 temp_dir = "CustomSwap/temp" # 临时文件目录(用于迭代交换) # 创建必要的目录 os.makedirs(src_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True) os.makedirs(os.path.dirname(dst_path), exist_ok=True) os.makedirs(os.path.dirname(output_path), exist_ok=True) # 步骤1: 保存目标图像 dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) cv2.imwrite(dst_path, dst_img_bgr) log += f"Saved destination image to {dst_path}\n" # 步骤2: 保存所有源图像 src_paths = [] # 存储所有源图像的路径 for i, src_img in enumerate(src_imgs): src_path = os.path.join(src_dir, f"data_src_{i+1}.jpg") # 索引从1开始(用户友好) if isinstance(src_img, tuple): src_img = src_img[0] # 处理元组格式 # 验证源图像有效性 if src_img is None: log += f"Source image {i+1} is None, skipping.\n" continue # 跳过None值 # 保存源图像(支持numpy数组和文件路径两种格式) if isinstance(src_img, np.ndarray): src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) src_paths.append(src_path) # 添加到路径列表 log += f"Saved source image {i+1} to {src_path}\n" elif isinstance(src_img, str) and os.path.exists(src_img): shutil.copy(src_img, src_path) src_paths.append(src_path) log += f"Copied source image {i+1} from {src_img} to {src_path}\n" else: log += f"Source image {i+1} is not a valid image, skipping.\n" # 步骤3: 解析映射字符串 try: # 将字符串 "2,1,3" 解析为整数列表 [2, 1, 3] mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()] except Exception as e: log += f"Error parsing mapping: {e}\n" elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return None, log # 解析失败,返回错误 # 步骤4: 使用临时文件进行迭代交换 # 因为目标图像中可能有多个脸需要替换,需要逐个替换 # 每次替换后,结果作为下一次替换的输入 temp_dst_path = os.path.join(temp_dir, "temp_dst.jpg") shutil.copy(dst_path, temp_dst_path) # 复制目标图像到临时文件 # 步骤5: 按照映射关系逐个替换人脸 # enumerate(mapping, start=1) 表示从1开始计数(face_idx从1开始) for face_idx, src_idx in enumerate(mapping, start=1): # face_idx: 目标图像中的人脸索引(第1个、第2个...) # src_idx: 要使用的源图像索引(在src_paths中的位置) # 验证源图像索引有效性 if src_idx < 1 or src_idx > len(src_paths): log += f"Invalid source index {src_idx} for face {face_idx}, skipping.\n" continue # 跳过无效索引 try: # 执行人脸交换 # src_paths[src_idx-1]: 因为src_paths索引从0开始,而src_idx从1开始,所以减1 # 1: 源图像中的人脸索引(使用第1个人脸) # temp_dst_path: 临时目标图像(每次交换后更新) # face_idx: 目标图像中要替换的人脸索引 swapped_img = swapper.swap_faces(src_paths[src_idx-1], 1, temp_dst_path, face_idx) cv2.imwrite(temp_dst_path, swapped_img) # 保存交换结果,作为下次的输入 log += f"Swapped face {face_idx} in destination with source {src_idx}\n" except Exception as e: log += f"Failed to swap face {face_idx} with source {src_idx}: {e}\n" # 步骤6: 复制最终结果到输出路径 shutil.copy(temp_dst_path, output_path) log += f"Saved swapped image to {output_path}\n" # 清理临时文件 if os.path.exists(temp_dst_path): os.remove(temp_dst_path) elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return output_path, log # ==================== 视频交换功能函数 ==================== def add_audio_to_video(original_video_path, video_no_audio_path, output_path): """ 功能: 将原始视频的音频添加到没有音频的视频中 视频处理过程中,我们只处理视频帧(图像),生成的视频没有音频 这个函数使用FFmpeg将原始视频的音频合并到新视频中 参数说明: original_video_path: 字符串,原始视频路径(包含音频) video_no_audio_path: 字符串,没有音频的视频路径(处理后的视频) output_path: 字符串,最终输出视频路径(包含音频) 返回值: (True, "") 或 (False, error_message): 元组,第一个元素表示是否成功,第二个是错误信息 技术说明: 使用FFmpeg的-map选项选择视频流和音频流进行合并 """ # 构建FFmpeg命令 cmd = [ "ffmpeg", # FFmpeg可执行文件 "-y", # 自动覆盖输出文件(不询问) "-i", video_no_audio_path, # 输入1: 没有音频的视频(处理后的视频) "-i", original_video_path, # 输入2: 原始视频(包含音频) "-c:v", "copy", # 视频编码器:直接复制(不重新编码,速度快) "-c:a", "aac", # 音频编码器:使用AAC格式 "-map", "0:v:0", # 映射:使用输入1的第0个视频流 "-map", "1:a:0?", # 映射:使用输入2的第0个音频流(?表示可选,如果没有音频也不报错) "-shortest", # 以最短的流为准(如果视频和音频长度不同) output_path # 输出文件路径 ] try: # 执行FFmpeg命令 # check=True: 如果命令失败(返回非0)则抛出异常 # stdout=subprocess.PIPE: 捕获标准输出(这里不需要,所以丢弃) # stderr=subprocess.PIPE: 捕获错误输出 subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return True, "" # 成功返回 except subprocess.CalledProcessError as e: # 如果FFmpeg执行失败,返回错误信息 return False, e.stderr.decode() # 将字节错误信息解码为字符串 def swap_video(src_img, src_idx, video, dst_idx, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): """ 功能: 视频中单个人脸交换 将源照片中的人脸替换到视频中的指定人脸上 参数说明: src_img: numpy数组,源图像(包含要提取的人脸) src_idx: 整数,源图像中要使用的人脸索引(从1开始) video: 字符串或视频对象,目标视频路径 dst_idx: 整数,目标视频中要被替换的人脸索引(从1开始) delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录(节省空间) add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 copy_to_drive: 布尔值,是否复制到Google Drive(用于Colab环境,本地环境可忽略) progress: Gradio进度条对象 返回值: final_output_path: 字符串,最终输出视频路径 log: 字符串,处理过程的日志信息 工作流程: 1. 提取视频的所有帧 2. 对每一帧进行人脸交换 3. 将交换后的帧重新组合成视频 4. 可选:添加原始视频的音频 5. 可选:清理临时文件 """ log = "" # 初始化日志 start_time = time.time() # 记录开始时间 # 定义工作目录和文件路径 src_path = "VideoSwapping/data_src.jpg" # 源图像保存路径 dst_video_path = "VideoSwapping/data_dst.mp4" # 目标视频保存路径 frames_dir = "VideoSwapping/video_frames" # 提取的视频帧目录 swapped_dir = "VideoSwapping/swapped_frames" # 交换后的帧目录 output_video_path = "VideoSwapping/output_tmp_output_video.mp4" # 临时输出视频(无音频) final_output_path = "VideoSwapping/output_with_audio.mp4" # 最终输出视频(有音频) # 创建必要的目录 os.makedirs(os.path.dirname(src_path), exist_ok=True) os.makedirs(os.path.dirname(dst_video_path), exist_ok=True) os.makedirs(frames_dir, exist_ok=True) os.makedirs(swapped_dir, exist_ok=True) # 步骤1: 保存源图像 src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) # 转换颜色空间 cv2.imwrite(src_path, src_img_bgr) # 保存源图像 log += f"Saved source image to {src_path}\n" progress(0.05, desc="Saved source image") # 更新进度条到5% # 步骤2: 处理目标视频(可能是文件路径或Gradio返回的对象) if isinstance(video, str) and os.path.exists(video): # 如果是文件路径且文件存在,复制到工作目录 shutil.copy(video, dst_video_path) log += f"Copied video to {dst_video_path}\n" else: # 否则直接使用(可能是Gradio返回的临时路径) dst_video_path = video # 导入视频处理函数(从VideoSwapping模块) from VideoSwapping import extract_frames, frames_to_video # 步骤3: 提取视频的所有帧 frame_paths = extract_frames(dst_video_path, frames_dir) # 提取帧,返回帧路径列表 log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" progress(0.15, desc="Extracted frames") # 更新进度条到15% # 步骤4: 对每一帧进行人脸交换 swapped_files = set(os.listdir(swapped_dir)) # 获取已交换的帧文件名集合(用于断点续传) total_frames = len(frame_paths) # 总帧数 start_loop_time = time.time() # 记录循环开始时间(用于计算剩余时间) for idx, frame_path in enumerate(frame_paths): swapped_name = f"swapped_{idx:05d}.jpg" # 交换后的帧文件名(5位数字,如00000.jpg) out_path = os.path.join(swapped_dir, swapped_name) # 检查是否已经处理过(断点续传功能) if swapped_name in swapped_files and os.path.exists(out_path): log += f"Frame {idx}: already swapped, skipping.\n" # 计算剩余时间并更新进度条 elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 # 平均每帧处理时间 remaining = avg_time * (total_frames - (idx + 1)) # 剩余时间(秒) mins, secs = divmod(int(remaining), 60) # 转换为分钟和秒 progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") continue # 跳过已处理的帧 try: try: # 尝试使用指定的目标人脸索引进行交换 swapped = swapper.swap_faces(src_path, int(src_idx), frame_path, int(dst_idx)) except ValueError as ve: # 如果指定的目标人脸索引不存在,尝试使用索引1(第一个人脸) if int(dst_idx) != 1 and "Target image contains" in str(ve): swapped = swapper.swap_faces(src_path, int(src_idx), frame_path, 1) log += f"Frame {idx}: dst_idx {dst_idx} not found, used 1 instead.\n" else: raise ve # 其他错误继续抛出 cv2.imwrite(out_path, swapped) # 保存交换后的帧 log += f"Swapped frame {idx} and saved to {out_path}\n" except Exception as e: # 如果交换失败,保存原始帧(保证视频完整性) cv2.imwrite(out_path, cv2.imread(frame_path)) log += f"Failed to swap frame {idx}: {e}\n" # 计算剩余时间并更新进度条 elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 remaining = avg_time * (total_frames - (idx + 1)) mins, secs = divmod(int(remaining), 60) progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") # 步骤5: 将交换后的帧重新组合成视频 cap = cv2.VideoCapture(dst_video_path) # 打开原始视频以获取帧率 fps = cap.get(cv2.CAP_PROP_FPS) # 获取帧率(Frames Per Second) cap.release() # 释放视频对象 frames_to_video(swapped_dir, output_video_path, fps) # 将帧组合成视频 log += f"Combined swapped frames into video {output_video_path}\n" progress(0.8, desc="Muxing audio") # 更新进度条到80% # 步骤6: 可选 - 复制到Google Drive(用于Colab环境) if copy_to_drive: drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) try: shutil.copy(output_video_path, drive_path) log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" except Exception as e: log += f"Failed to copy to Google Drive: {e}\n" # 步骤7: 可选 - 添加原始视频的音频 if add_audio: ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) if ok: log += f"Added audio to {final_output_path}\n" else: log += f"Audio muxing failed: {audio_log}\n" final_output_path = output_video_path # 如果添加音频失败,使用无音频版本 else: final_output_path = output_video_path log += "Audio was not added as per user request.\n" # 步骤8: 清理临时文件 try: if os.path.exists(src_path): os.remove(src_path) # 删除源图像 if os.path.exists(dst_video_path): os.remove(dst_video_path) # 删除复制的目标视频 if delete_frames_dir and os.path.exists(frames_dir): shutil.rmtree(frames_dir) # 删除提取的帧目录 log += "Deleted video_frames directory.\n" elif not delete_frames_dir: log += "Kept video_frames directory as requested.\n" if os.path.exists(swapped_dir): shutil.rmtree(swapped_dir) # 删除交换后的帧目录 log += "Cleaned up temp files and folders.\n" except Exception as cleanup_error: log += f"Cleanup error: {cleanup_error}\n" progress(1, desc="Done") # 更新进度条到100% elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return final_output_path, log def swap_video_all_faces(src_img, video, num_faces_to_swap, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): """ 功能: 视频中所有人脸交换 将源照片中的人脸替换到视频中的前N个人脸上(N由num_faces_to_swap指定) 参数说明: src_img: numpy数组,源图像(包含要提取的人脸) video: 字符串或视频对象,目标视频路径 num_faces_to_swap: 整数,要交换的人脸数量(从第1个到第N个) delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录 add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 copy_to_drive: 布尔值,是否复制到Google Drive progress: Gradio进度条对象 返回值: final_output_path: 字符串,最终输出视频路径 log: 字符串,处理过程的日志信息 使用场景: 例如视频中有多个人,想用同一个源人脸替换所有人 """ log = "" start_time = time.time() src_path = "VideoSwappingAllFaces/data_src.jpg" dst_video_path = "VideoSwappingAllFaces/data_dst.mp4" frames_dir = "VideoSwappingAllFaces/video_frames" swapped_dir = "VideoSwappingAllFaces/swapped_frames" output_video_path = "VideoSwappingAllFaces/output_tmp_output_video.mp4" final_output_path = "VideoSwappingAllFaces/output_with_audio.mp4" os.makedirs(os.path.dirname(src_path), exist_ok=True) os.makedirs(os.path.dirname(dst_video_path), exist_ok=True) os.makedirs(frames_dir, exist_ok=True) os.makedirs(swapped_dir, exist_ok=True) src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) log += f"Saved source image to {src_path}\n" progress(0.05, desc="Saved source image") if isinstance(video, str) and os.path.exists(video): shutil.copy(video, dst_video_path) log += f"Copied video to {dst_video_path}\n" else: dst_video_path = video from VideoSwapping import extract_frames, frames_to_video frame_paths = extract_frames(dst_video_path, frames_dir) log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" progress(0.15, desc="Extracted frames") swapped_files = set(os.listdir(swapped_dir)) temp_dir = os.path.join(swapped_dir, "temp_swap") os.makedirs(temp_dir, exist_ok=True) total_frames = len(frame_paths) start_loop_time = time.time() for idx, frame_path in enumerate(frame_paths): swapped_name = f"swapped_{idx:05d}.jpg" out_path = os.path.join(swapped_dir, swapped_name) temp_frame_path = os.path.join(temp_dir, "temp.jpg") if swapped_name in swapped_files and os.path.exists(out_path): log += f"Frame {idx}: already swapped, skipping.\n" elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 remaining = avg_time * (total_frames - (idx + 1)) mins, secs = divmod(int(remaining), 60) progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") continue try: # 复制原始帧到临时文件(用于迭代交换) shutil.copy(frame_path, temp_frame_path) # 对每一帧,依次替换前N个人脸(使用同一个源人脸) for face_idx in range(1, int(num_faces_to_swap) + 1): try: # 执行人脸交换(源图像使用第1个人脸,目标帧使用face_idx指定的人脸) # 每次交换后,结果保存回temp_frame_path,作为下次交换的输入 swapped_img = swapper.swap_faces(src_path, 1, temp_frame_path, face_idx) cv2.imwrite(temp_frame_path, swapped_img) # 保存交换结果 except Exception as e: log += f"Failed to swap face {face_idx} in frame {idx}: {e}\n" # 所有人脸交换完成后,复制最终结果到输出路径 shutil.copy(temp_frame_path, out_path) log += f"Swapped all faces in frame {idx} and saved to {out_path}\n" # 清理临时文件 if os.path.exists(temp_frame_path): os.remove(temp_frame_path) except Exception as e: cv2.imwrite(out_path, cv2.imread(frame_path)) log += f"Failed to swap frame {idx}: {e}\n" elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 remaining = avg_time * (total_frames - (idx + 1)) mins, secs = divmod(int(remaining), 60) progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") if os.path.exists(temp_dir): shutil.rmtree(temp_dir) cap = cv2.VideoCapture(dst_video_path) fps = cap.get(cv2.CAP_PROP_FPS) cap.release() frames_to_video(swapped_dir, output_video_path, fps) log += f"Combined swapped frames into video {output_video_path}\n" progress(0.8, desc="Muxing audio") # 如果请求,复制到 Google Drive if copy_to_drive: drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) try: shutil.copy(output_video_path, drive_path) log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" except Exception as e: log += f"Failed to copy to Google Drive: {e}\n" if add_audio: ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) if ok: log += f"Added audio to {final_output_path}\n" else: log += f"Audio muxing failed: {audio_log}\n" final_output_path = output_video_path else: final_output_path = output_video_path log += "Audio was not added as per user request.\n" try: if os.path.exists(src_path): os.remove(src_path) if os.path.exists(dst_video_path): os.remove(dst_video_path) if delete_frames_dir and os.path.exists(frames_dir): shutil.rmtree(frames_dir) log += "Deleted video_frames directory.\n" elif not delete_frames_dir: log += "Kept video_frames directory as requested.\n" if os.path.exists(swapped_dir): shutil.rmtree(swapped_dir) log += "Cleaned up temp files and folders.\n" except Exception as cleanup_error: log += f"Cleanup error: {cleanup_error}\n" progress(1, desc="Done") elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return final_output_path, log def swap_video_custom_mapping(src_imgs, video, mapping_str, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): log = "" start_time = time.time() src_dir = "CustomVideoSwap/src" temp_dir = "CustomVideoSwap/temp" frames_dir = "CustomVideoSwap/frames" swapped_dir = "CustomVideoSwap/swapped_frames" output_video_path = "CustomVideoSwap/output_tmp_output_video.mp4" final_output_path = "CustomVideoSwap/output_with_audio.mp4" dst_video_path = "CustomVideoSwap/data_dst.mp4" os.makedirs(src_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True) os.makedirs(frames_dir, exist_ok=True) os.makedirs(swapped_dir, exist_ok=True) # 保存所有源图像 src_paths = [] for i, src_img in enumerate(src_imgs): src_path = os.path.join(src_dir, f"data_src_{i+1}.jpg") if isinstance(src_img, tuple): src_img = src_img[0] if src_img is None: log += f"Source image {i+1} is None, skipping.\n" continue if isinstance(src_img, np.ndarray): src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) src_paths.append(src_path) log += f"Saved source image {i+1} to {src_path}\n" elif isinstance(src_img, str) and os.path.exists(src_img): shutil.copy(src_img, src_path) src_paths.append(src_path) log += f"Copied source image {i+1} from {src_img} to {src_path}\n" else: log += f"Source image {i+1} is not a valid image, skipping.\n" # 解析映射关系 try: mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()] except Exception as e: log += f"Error parsing mapping: {e}\n" elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return None, log # 准备视频 if isinstance(video, str) and os.path.exists(video): shutil.copy(video, dst_video_path) log += f"Copied video to {dst_video_path}\n" else: dst_video_path = video from VideoSwapping import extract_frames, frames_to_video frame_paths = extract_frames(dst_video_path, frames_dir) log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" progress(0.1, desc="Extracted frames") swapped_files = set(os.listdir(swapped_dir)) temp_frame_path = os.path.join(temp_dir, "temp.jpg") total_frames = len(frame_paths) start_loop_time = time.time() for idx, frame_path in enumerate(frame_paths): swapped_name = f"swapped_{idx:05d}.jpg" out_path = os.path.join(swapped_dir, swapped_name) if swapped_name in swapped_files and os.path.exists(out_path): log += f"Frame {idx}: already swapped, skipping.\n" elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 remaining = avg_time * (total_frames - (idx + 1)) mins, secs = divmod(int(remaining), 60) progress(0.1 + 0.7 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") continue try: # 复制原始帧到临时文件(用于迭代交换) shutil.copy(frame_path, temp_frame_path) # 按照映射关系,依次替换视频帧中的每个人脸 # enumerate(mapping, start=1) 表示face_idx从1开始(第1个、第2个人脸...) for face_idx, src_idx in enumerate(mapping, start=1): # face_idx: 目标视频帧中的人脸索引(第1个、第2个...) # src_idx: 要使用的源图像索引(在src_paths中的位置,从1开始) # 验证源图像索引有效性 if src_idx < 1 or src_idx > len(src_paths): log += f"Invalid source index {src_idx} for face {face_idx} in frame {idx}, skipping.\n" continue # 跳过无效索引 try: # 执行人脸交换 # src_paths[src_idx-1]: 因为src_paths索引从0开始,而src_idx从1开始,所以减1 # 1: 源图像中的人脸索引(使用第1个人脸) # temp_frame_path: 临时帧文件(每次交换后更新) # face_idx: 目标帧中要替换的人脸索引 swapped_img = swapper.swap_faces(src_paths[src_idx-1], 1, temp_frame_path, face_idx) cv2.imwrite(temp_frame_path, swapped_img) # 保存交换结果,作为下次的输入 log += f"Frame {idx}: Swapped face {face_idx} with source {src_idx}\n" except Exception as e: log += f"Frame {idx}: Failed to swap face {face_idx} with source {src_idx}: {e}\n" # 所有人脸交换完成后,复制最终结果到输出路径 shutil.copy(temp_frame_path, out_path) log += f"Swapped all faces in frame {idx} and saved to {out_path}\n" # 清理临时文件 if os.path.exists(temp_frame_path): os.remove(temp_frame_path) except Exception as e: cv2.imwrite(out_path, cv2.imread(frame_path)) log += f"Failed to swap frame {idx}: {e}\n" elapsed = time.time() - start_loop_time avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 remaining = avg_time * (total_frames - (idx + 1)) mins, secs = divmod(int(remaining), 60) progress(0.1 + 0.7 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") if os.path.exists(temp_dir): shutil.rmtree(temp_dir) cap = cv2.VideoCapture(dst_video_path) fps = cap.get(cv2.CAP_PROP_FPS) cap.release() frames_to_video(swapped_dir, output_video_path, fps) log += f"Combined swapped frames into video {output_video_path}\n" progress(0.9, desc="Muxing audio") if add_audio: ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) if ok: log += f"Added audio to {final_output_path}\n" else: log += f"Audio muxing failed: {audio_log}\n" final_output_path = output_video_path else: final_output_path = output_video_path log += "Audio was not added as per user request.\n" try: if os.path.exists(dst_video_path): os.remove(dst_video_path) if delete_frames_dir and os.path.exists(frames_dir): shutil.rmtree(frames_dir) log += "Deleted video_frames directory.\n" elif not delete_frames_dir: log += "Kept video_frames directory as requested.\n" if os.path.exists(swapped_dir): shutil.rmtree(swapped_dir) log += "Cleaned up temp files and folders.\n" except Exception as cleanup_error: log += f"Cleanup error: {cleanup_error}\n" progress(1, desc="Done") elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return final_output_path, log def swap_single_src_multi_video(src_img, dst_videos, dst_indices, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress(track_tqdm=True)): """ 功能: 一个源人脸对应多个视频 将同一个源照片中的人脸,分别替换到多个视频中的指定人脸上 每个视频依次处理,生成多个输出视频 参数说明: src_img: numpy数组,源图像(包含要提取的人脸) dst_videos: 视频列表,多个目标视频(Gradio File组件返回的列表) dst_indices: 字符串或列表,每个视频中要替换的人脸索引 例如 "1,2,1" 表示第一个视频用索引1,第二个用索引2,第三个用索引1 delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录 add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 copy_to_drive: 布尔值,是否复制到Google Drive progress: Gradio进度条对象 返回值: results: 列表,所有输出视频的路径列表 log: 字符串,处理过程的日志信息 使用场景: 例如有一个人的照片,想替换到多个不同的视频中 """ log = "" results = [] start_time = time.time() base_dir = "SingleSrcMultiVideo" dst_dir = os.path.join(base_dir, "dst") frames_dir = os.path.join(base_dir, "video_frames") swapped_dir = os.path.join(base_dir, "swap_frames") output_dir = os.path.join(base_dir, "output") os.makedirs(dst_dir, exist_ok=True) os.makedirs(frames_dir, exist_ok=True) os.makedirs(swapped_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) # 解析索引 if isinstance(dst_indices, str): dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] else: dst_indices_list = [int(idx) for idx in dst_indices] # 保存源图像 src_path = os.path.join(base_dir, "data_src.jpg") src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) cv2.imwrite(src_path, src_img_bgr) log += f"Saved source image to {src_path}\n" from VideoSwapping import extract_frames, frames_to_video for i, video in enumerate(dst_videos): dst_idx = dst_indices_list[i] if i < len(dst_indices_list) else 1 video_name = f"video_{i}.mp4" dst_video_path = os.path.join(dst_dir, video_name) output_video_path = os.path.join(output_dir, f"output_{i}.mp4") output_video_with_audio = os.path.join(output_dir, f"output_with_audio_{i}.mp4") # 复制视频到目标目录 if isinstance(video, str) and os.path.exists(video): shutil.copy(video, dst_video_path) log += f"Copied video {video} to {dst_video_path}\n" else: dst_video_path = video # 提取帧 frame_paths = extract_frames(dst_video_path, frames_dir) log += f"Extracted {len(frame_paths)} frames from {dst_video_path} to {frames_dir}\n" progress(i / len(dst_videos), desc=f"Processing video {i+1}/{len(dst_videos)}") swapped_files = set(os.listdir(swapped_dir)) total_frames = len(frame_paths) temp_frame_path = os.path.join(swapped_dir, "temp.jpg") start_loop_time = time.time() for idx, frame_path in enumerate(frame_paths): swapped_name = f"swapped_{idx:05d}.jpg" out_path = os.path.join(swapped_dir, swapped_name) if swapped_name in swapped_files and os.path.exists(out_path): continue try: shutil.copy(frame_path, temp_frame_path) try: swapped_img = swapper.swap_faces(src_path, 1, temp_frame_path, int(dst_idx)) cv2.imwrite(temp_frame_path, swapped_img) except Exception as e: log += f"Failed to swap face in frame {idx} of video {i}: {e}\n" shutil.copy(temp_frame_path, out_path) if os.path.exists(temp_frame_path): os.remove(temp_frame_path) except Exception as e: cv2.imwrite(out_path, cv2.imread(frame_path)) log += f"Failed to swap frame {idx} of video {i}: {e}\n" # 将帧组合成视频 cap = cv2.VideoCapture(dst_video_path) fps = cap.get(cv2.CAP_PROP_FPS) cap.release() frames_to_video(swapped_dir, output_video_path, fps) log += f"Combined swapped frames into video {output_video_path}\n" # 如果请求,复制到 Google Drive if copy_to_drive: drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) try: shutil.copy(output_video_path, drive_path) log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" except Exception as e: log += f"Failed to copy to Google Drive: {e}\n" # 如果请求,合并音频 if add_audio: ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, output_video_with_audio) if ok: log += f"Added audio to {output_video_with_audio}\n" results.append(output_video_with_audio) else: log += f"Audio muxing failed for video {i}: {audio_log}\n" results.append(output_video_path) else: results.append(output_video_path) log += f"Audio was not added for video {i} as per user request.\n" # 清理帧以便处理下一个视频 if delete_frames_dir and os.path.exists(frames_dir): shutil.rmtree(frames_dir) os.makedirs(frames_dir, exist_ok=True) log += f"Deleted video_frames directory after video {i}.\n" elif not delete_frames_dir: log += f"Kept video_frames directory after video {i} as requested.\n" if os.path.exists(swapped_dir): shutil.rmtree(swapped_dir) os.makedirs(swapped_dir, exist_ok=True) log += f"Cleared swap_frames directory after video {i}.\n" elapsed = time.time() - start_time log += f"Elapsed time: {elapsed:.2f} seconds\n" return results, log # Gradio UI:自定义 CSS(隐藏页脚 + 容器宽度/留白 + 输入输出区卡片感 + 按钮/焦点微调) custom_css = """ footer {display: none !important;} .gradio-container footer {display: none !important;} #footer {display: none !important;} .gradio-footer {display: none !important;} footer.absolute {display: none !important;} [data-testid="footer"] {display: none !important;} .gradio-container { margin: 0 auto; padding: 1.25rem 1.5rem; box-sizing: border-box; background: #fafaf9; min-height: 100vh; } .gradio-container, .gradio-container > div { width: 100%; max-width: 100%; box-sizing: border-box; } .gr-block { border-radius: 14px; box-sizing: border-box; } .input-panel, .output-panel { background: #ffffff; border-radius: 14px; padding: 1.25rem; border: 1px solid #e7e5e4; box-shadow: 0 1px 2px rgba(0,0,0,0.04); } .gr-row { gap: 1.5rem !important; } button.primary { border-radius: 10px; font-weight: 600; box-shadow: 0 1px 2px rgba(79,70,229,0.15); } button.primary:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(79,70,229,0.2); } .small-text { font-size: 0.875rem; color: #57534e; } label, .label { color: #44403c !important; } button[aria-selected="true"], .tabs button.selected { border-bottom: 2px solid #4f46e5; font-weight: 600; } @media (max-width: 768px) { .input-panel, .output-panel { padding: 1rem; } button.primary { min-height: 44px; } } @media (min-width: 1220px) { .gradio-container { width: 1200px !important; max-width: 1200px !important; min-width: 1200px !important; } .gradio-container > div { width: 100% !important; min-width: 100% !important; } } """ _clarity_script = '' with gr.Blocks(title="Face Swapping Free", css=custom_css, theme=gr.themes.Soft(primary_hue="indigo")) as demo: gr.HTML(_clarity_script) gr.Markdown(wellcomingMessage) # ==================== 标签页1: 单张照片交换(最常用功能) ==================== with gr.Tab("📷 Single Photo Swapping", id="single_photo"): with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" - **Source Image**: Image containing the face to extract - **Source Face Index**: Which face to use if multiple (from 1) - **Destination Image**: Image where the face will be replaced - **Destination Face Index**: Which face in destination to replace (from 1) - Click **Start Swapping** to run """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") gr.Markdown("*Upload images with clear, front-facing faces for best results.*", elem_classes=["small-text"]) src_img = gr.Image(label="Source Image", type="numpy", height=280) src_idx = gr.Number(value=1, label="Source Face Index", minimum=1, precision=0, info="If the image contains multiple faces, specify which one to use (starting from 1)") dst_img = gr.Image(label="Destination Image", type="numpy", height=300) dst_idx = gr.Number(value=1, label="Destination Face Index", minimum=1, precision=0, info="Specify which face in the destination image to replace") gr.Markdown("*Upload both images first, then click Start. Face index starts at 1 (use 1 for single face).*", elem_classes=["small-text"]) submit_btn = gr.Button("🚀 Start Swapping", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") gr.Markdown("*Result will appear here after processing. You can download directly from the preview.*", elem_classes=["small-text"]) output_img = gr.Image(label="Swapped Image", height=360) log_output = gr.Textbox(label="Log Output", lines=6, interactive=False, placeholder="Processing logs will appear here...") submit_btn.click( fn=swap_single_photo, inputs=[src_img, src_idx, dst_img, dst_idx], outputs=[output_img, log_output] ) # ==================== 标签页2: 一个源对应多个目标 ==================== with gr.Tab("🖼️ Single Source Multi-Destination", id="single_src_multi_dst"): with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Replace the same source face into multiple destination images. Perfect for batch processing. - **Source Image**: Image containing the face to extract - **Destination Images**: Upload multiple images (Gallery supports multiple selection) - **Destination Face Indices**: Comma-separated, e.g. "1,1,2" = index 1 for 1st & 2nd image, index 2 for 3rd """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_img_multi = gr.Image(label="Source Image", type="numpy", height=250) dst_imgs = gr.Gallery(label="Destination Images", type="numpy", columns=2, height=300, show_label=True) dst_indices_multi = gr.Textbox( label="Destination Face Indices", placeholder="e.g., 1,1,2", info="Comma-separated values, corresponding to each destination image's face index" ) submit_multi_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_gallery = gr.Gallery(label="Swapped Images", columns=2, height=400) log_multi = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_multi_btn.click( fn=swap_single_src_multi_dst, inputs=[src_img_multi, dst_imgs, dst_indices_multi], outputs=[output_gallery, log_multi] ) # ==================== 标签页3: 多个源对应一个目标 ==================== with gr.Tab("👥 Multi-Source Single Destination", id="multi_src_single_dst"): with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Replace different faces from multiple sources into the same destination image. - **Source Images**: Upload multiple images with different faces - **Destination Image**: One destination image - **Destination Face Index**: Which face in destination to replace (from 1) """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_imgs_multi = gr.Gallery(label="Source Images", type="numpy", columns=2, height=250) dst_img_single = gr.Image(label="Destination Image", type="numpy", height=250) dst_idx_single = gr.Number(value=1, label="Destination Face Index", minimum=1, precision=0) submit_multi_src_btn = gr.Button("🚀 Start Processing", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_gallery_multi = gr.Gallery(label="Swapped Images", columns=2, height=400) log_multi_src = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_multi_src_btn.click( fn=swap_multi_src_single_dst, inputs=[src_imgs_multi, dst_img_single, dst_idx_single], outputs=[output_gallery_multi, log_multi_src] ) # ==================== 标签页4: 多个源对应多个目标 ==================== with gr.Tab("🔄 Multi-Source Multi-Destination", id="multi_src_multi_dst"): with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Batch: each source face × each destination image (Cartesian product). E.g. 3 sources × 2 destinations = 6 results. - **Source Images** / **Destination Images**: Upload multiple - **Destination Face Indices**: Comma-separated, one index per destination image """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_imgs_cartesian = gr.Gallery(label="Source Images", type="numpy", columns=2, height=200) dst_imgs_cartesian = gr.Gallery(label="Destination Images", type="numpy", columns=2, height=200) dst_indices_cartesian = gr.Textbox( label="Destination Face Indices", placeholder="e.g., 1,1,2", info="Comma-separated values, corresponding to each destination image" ) submit_cartesian_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_gallery_cartesian = gr.Gallery(label="Swapped Images", columns=3, height=400) log_cartesian = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_cartesian_btn.click( fn=swap_multi_src_multi_dst, inputs=[src_imgs_cartesian, dst_imgs_cartesian, dst_indices_cartesian], outputs=[output_gallery_cartesian, log_cartesian] ) # ==================== 标签页5: 自定义人脸映射 ==================== with gr.Tab("🎯 Custom Face Mapping", id="custom_mapping"): with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Specify which source face replaces each face in the destination. E.g. mapping "2,1,3": 1st dest face → source 2, 2nd → source 1, 3rd → source 3. - **Source Images** / **Destination Image**: Upload; destination can have multiple faces - **Mapping**: Comma-separated, one number per face in destination (numbers = source image index from 1) """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_imgs_custom = gr.Gallery(label="Source Images", type="numpy", columns=2, height=250) dst_img_custom = gr.Image(label="Destination Image", type="numpy", height=250) mapping_str = gr.Textbox( label="Mapping", placeholder="e.g., 2,1,3", info="Comma-separated values, numbers correspond to source image indices (starting from 1)" ) submit_custom_btn = gr.Button("🚀 Start Processing", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_img_custom = gr.Image(label="Swapped Image", height=400) log_custom = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_custom_btn.click( fn=swap_faces_custom, inputs=[src_imgs_custom, dst_img_custom, mapping_str], outputs=[output_img_custom, log_custom] ) # ==================== 标签页6: 视频单个人脸交换 ==================== with gr.Tab("🎬 Video Face Swapping", id="video_swapping"): gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Replace the source face into a specific face in the target video. - **Source Image** / **Target Video**: Upload; **Face Indices**: which face in source and in video (from 1) - **Processing Options**: Keep audio, delete temp frames, etc. """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_img_video = gr.Image(label="Source Image", type="numpy", height=200) src_idx_video = gr.Number(value=1, label="Source Face Index", minimum=1, precision=0) video_input = gr.Video(label="Target Video", height=200) dst_idx_video = gr.Number(value=1, label="Destination Face Index", minimum=1, precision=0) gr.Markdown("### ⚙️ Processing Options") delete_frames = gr.Checkbox(label="Delete temporary frame files after processing", value=True) add_audio = gr.Checkbox(label="Keep original video audio", value=True) copy_to_drive = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) submit_video_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_video = gr.Video(label="Swapped Video", height=400) log_video = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_video_btn.click( fn=swap_video, inputs=[src_img_video, src_idx_video, video_input, dst_idx_video, delete_frames, add_audio, copy_to_drive], outputs=[output_video, log_video] ) # ==================== 标签页7: 视频所有人脸交换 ==================== with gr.Tab("👥 Video All Faces Swapping", id="video_all_faces"): gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Replace the same source face into the first N faces in the video. - **Source Image** / **Target Video**: Upload - **Number of Faces**: How many faces to replace (from 1st face) """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_img_all = gr.Image(label="Source Image", type="numpy", height=200) video_all = gr.Video(label="Target Video", height=200) num_faces = gr.Number(value=1, label="Number of Faces to Swap", minimum=1, precision=0, info="Starting from the 1st face, replace the first N faces") gr.Markdown("### ⚙️ Processing Options") delete_frames_all = gr.Checkbox(label="Delete temporary frame files after processing", value=True) add_audio_all = gr.Checkbox(label="Keep original video audio", value=True) copy_to_drive_all = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) submit_all_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_video_all = gr.Video(label="Swapped Video", height=400) log_all = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_all_btn.click( fn=swap_video_all_faces, inputs=[src_img_all, video_all, num_faces, delete_frames_all, add_audio_all, copy_to_drive_all], outputs=[output_video_all, log_all] ) # ==================== 标签页8: 视频自定义人脸映射 ==================== with gr.Tab("🎯 Video Custom Face Mapping", id="video_custom_mapping"): gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Specify which source face replaces each face in the video. E.g. mapping "2,1,3": 1st video face → source 2, 2nd → source 1, 3rd → source 3. - **Source Images** / **Target Video**: Upload - **Mapping**: Comma-separated, one number per face in video (numbers = source image index from 1) """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_imgs_video_custom = gr.Gallery(label="Source Images", type="numpy", columns=2, height=200) video_custom = gr.Video(label="Target Video", height=200) mapping_video = gr.Textbox( label="Mapping", placeholder="e.g., 2,1,3", info="Comma-separated values, corresponding to source image indices for each face in the video" ) gr.Markdown("### ⚙️ Processing Options") delete_frames_custom = gr.Checkbox(label="Delete temporary frame files after processing", value=True) add_audio_custom = gr.Checkbox(label="Keep original video audio", value=True) copy_to_drive_custom = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) submit_video_custom_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_video_custom = gr.Video(label="Swapped Video", height=400) log_video_custom = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_video_custom_btn.click( fn=swap_video_custom_mapping, inputs=[src_imgs_video_custom, video_custom, mapping_video, delete_frames_custom, add_audio_custom, copy_to_drive_custom], outputs=[output_video_custom, log_video_custom] ) # ==================== 标签页9: 一个源对应多个视频 ==================== with gr.Tab("📹 Single Source Multi-Video", id="single_src_multi_video"): gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) with gr.Accordion("💡 Usage Instructions", open=False): gr.Markdown(""" Batch: replace the same source face into multiple videos. - **Source Image** / **Target Videos**: Upload (multiple videos supported) - **Destination Face Indices**: Comma-separated, one face index per video """) with gr.Row(): with gr.Column(scale=1, elem_classes=["input-panel"]): gr.Markdown("### 📥 Input") src_img_multi_video = gr.Image(label="Source Image", type="numpy", height=200) videos_multi = gr.File( label="Target Videos", file_count="multiple", type="filepath", file_types=["video"] ) gr.Markdown("*💡 You can upload multiple video files*", elem_classes="small-text") dst_indices_multi_video = gr.Textbox( label="Destination Face Indices", placeholder="e.g., 1,2,1", info="Comma-separated values, corresponding to each video's face index" ) gr.Markdown("### ⚙️ Processing Options") delete_frames_multi_video = gr.Checkbox(label="Delete temporary frame files after processing", value=True) add_audio_multi_video = gr.Checkbox(label="Keep original video audio", value=True) copy_to_drive_multi_video = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) submit_multi_video_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") with gr.Column(scale=1, elem_classes=["output-panel"]): gr.Markdown("### 📤 Output") output_videos_gallery = gr.Gallery(label="Swapped Videos", type="filepath", columns=2, height=400) log_multi_video = gr.Textbox(label="Log Output", lines=10, interactive=False) submit_multi_video_btn.click( fn=swap_single_src_multi_video, inputs=[src_img_multi_video, videos_multi, dst_indices_multi_video, delete_frames_multi_video, add_audio_multi_video, copy_to_drive_multi_video], outputs=[output_videos_gallery, log_multi_video] ) gr.Markdown("""

💡 Processing time depends on size and complexity; video may take a while.

Face Swapping Free · InsightFace & Gradio

""") # 启用排队:长任务(视频/批量)显示排队状态,避免多用户同时提交时误以为卡死 demo.queue(max_size=10) # ==================== 程序入口 ==================== if __name__ == "__main__": # 创建命令行参数解析器 parser = argparse.ArgumentParser(description="Face Swapping Web Application") # 添加--share参数:如果指定,则创建公共链接(可通过互联网访问) parser.add_argument("--share", action="store_true", help="Launch Gradio with share=True (creates public link)") args = parser.parse_args() # 解析命令行参数 # 启动 Gradio 应用(优化后的配置) # share=args.share: 如果命令行指定了 --share,则创建公共链接 # server_name: # - "127.0.0.1" (默认): 只能本机访问(更安全) # - "0.0.0.0": 允许局域网访问(允许同一网络上的其他设备访问) # server_port=7860: 指定端口 # show_error=True: 显示详细错误信息 demo.launch( share=args.share, # server_name="127.0.0.1", # 取消注释以仅允许本机访问(更安全) # server_name="0.0.0.0", # 取消注释以允许局域网访问(用于在移动设备上测试) server_port=7860, show_error=True )