FaceSwap / app.py
aidao's picture
Integrate into Gradio UI
da2fe4c
Raw
History Blame Contribute Delete
78.8 kB
"""
文件路径: 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 = """
<div style="text-align: center; padding: 20px 24px; background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); border-radius: 14px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(79,70,229,0.12); border: none;">
<h1 style="color: #fff; margin: 0; font-size: 2.25rem; font-weight: 600; letter-spacing: -0.02em;">Face Swapping Free</h1>
<p style="color: rgba(255,255,255,0.9); font-size: 1rem; margin: 8px 0 0 0;">Photos, videos, batch — one tool</p>
</div>
"""
# 创建全局的人脸交换器实例,所有函数共享使用
# 这样可以避免重复初始化,提高性能
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 = '<script type="text/javascript">(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,"clarity","script","umy0o631ds");</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("""
<div style="text-align: center; padding: 16px; margin-top: 16px; border-top: 1px solid #e7e5e4; color: #57534e; font-size: 0.875rem;">
<p style="margin: 0;">💡 Processing time depends on size and complexity; video may take a while.</p>
<p style="margin: 6px 0 0 0;">Face Swapping Free · InsightFace & Gradio</p>
</div>
""")
# 启用排队:长任务(视频/批量)显示排队状态,避免多用户同时提交时误以为卡死
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
)