diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..439f099
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+video_split/
+video_split_swap/
+test.ipynb
+test.py
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/deployment.xml b/.idea/deployment.xml
new file mode 100644
index 0000000..ce3137d
--- /dev/null
+++ b/.idea/deployment.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/face_fusion_damo.iml b/.idea/face_fusion_damo.iml
new file mode 100644
index 0000000..cd33354
--- /dev/null
+++ b/.idea/face_fusion_damo.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..e9ff253
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..823c3f9
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..2538af2
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..65d5532
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# 🎭 Painted Skin 🎭
+
+> Welcome to the 🎭 Painted Skin 🎭! Get ready to have some fun with faces. 😄
+
+ ![License](https://img.shields.io/badge/license-Apache 2.0-blue)
+
+## 🐾 Preview
+
+![image-20231108160501352](https://black-thompson.oss-cn-beijing.aliyuncs.com/img/image-20231108160501352.png)
+
+
+
+## 🪄 How to use
+
+1. **Clone the Repository:**
+
+ ```bash
+ git clone https://github.com/BlackThompson/Painted-Skin.git
+ ```
+
+2. **Create a New Environment and Install Dependencies:**
+
+ - Note: Python version should be >=3.8
+ - Note: The versions of `torch`, `torchvision`, and `torchaudio` should align with your CUDA version.
+
+ ```bash
+ conda create --name painted_skin python=3.8
+ conda activate painted_skin
+ pip install -r requirements.txt
+ ```
+
+3. **Run `gradio_UI.py`:**
+ ```bash
+ gradio gradio_UI.py
+ ```
+
+4. **Click the Local URL**
+
+## 🦾 How it Works
+
+This tool allows you to perform a face swap. Simply follow these steps:
+
+1. **Upload Source:**
+ - Choose between `src_picture` and `src_video` for the face you want to swap.
+ - Note: You can only upload one source at a time, and the tool processes one task at a time.
+2. **Upload Target:**
+ - Upload `target_picture`, and our model will swap the face onto the source image or video.
+3. **Submit and Wait:**
+ - Click the submit button and patiently wait for the process to complete.
+ - Keep in mind that face swapping in videos might take some time, so be patient! 😅
+
+## ❗ Important Note
+- Ensure that `target_picture` is uploaded for the face swap to work effectively.
+
+Enjoy swapping faces and have a good laugh! 😆
+
+## 💌 Acknowledgements
+
+This repository borrows heavily from [facefusion-damo](https://www.modelscope.cn/models/damo/cv_unet-image-face-fusion_damo/summary) and [face-change](https://github.com/Quietbe/mv_face_change/blob/main/video_cut_cv_h.py). Thanks to the authors for sharing their code and models.
diff --git a/__pycache__/face_swap_utils.cpython-38.pyc b/__pycache__/face_swap_utils.cpython-38.pyc
new file mode 100644
index 0000000..43ee4e3
Binary files /dev/null and b/__pycache__/face_swap_utils.cpython-38.pyc differ
diff --git a/__pycache__/gradio_UI.cpython-38.pyc b/__pycache__/gradio_UI.cpython-38.pyc
new file mode 100644
index 0000000..15841a9
Binary files /dev/null and b/__pycache__/gradio_UI.cpython-38.pyc differ
diff --git a/example/CXK.jpg b/example/CXK.jpg
new file mode 100644
index 0000000..5219008
Binary files /dev/null and b/example/CXK.jpg differ
diff --git a/example/James.jpg b/example/James.jpg
new file mode 100644
index 0000000..670050c
Binary files /dev/null and b/example/James.jpg differ
diff --git a/example/Obama.jpg b/example/Obama.jpg
new file mode 100644
index 0000000..54e21af
Binary files /dev/null and b/example/Obama.jpg differ
diff --git a/example/Trump.jpg b/example/Trump.jpg
new file mode 100644
index 0000000..8bd483c
Binary files /dev/null and b/example/Trump.jpg differ
diff --git a/face_swap_utils.py b/face_swap_utils.py
new file mode 100644
index 0000000..e62c345
--- /dev/null
+++ b/face_swap_utils.py
@@ -0,0 +1,161 @@
+# _*_ coding : utf-8 _*_
+# @Time : 2023/11/6 17:04
+# @Author : Black
+# @File : face_swap_utils
+# @Project : face_fusion_damo
+
+import cv2
+import os
+from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_audio
+import moviepy.editor as mp
+import torch
+from modelscope.outputs import OutputKeys
+from modelscope.pipelines import pipeline
+from modelscope.utils.constant import Tasks
+from moviepy.editor import VideoFileClip, AudioFileClip, ImageSequenceClip
+import natsort
+
+
+# 将视频video_path分割成图片和音频文件,保存到save_path文件夹中
+def video2mp3_img(video_path, save_path):
+ def _video2img(video_path, save_path):
+ cap = cv2.VideoCapture(video_path)
+ frame_rate = int(cap.get(cv2.CAP_PROP_FPS)) # 获取视频帧率
+ i = 0
+ while True:
+ ret, frame = cap.read()
+ if ret:
+ cv2.imwrite(save_path + '/' + str(i) + '.jpg', frame)
+ i += 1
+ else:
+ break
+ cap.release()
+ print(f"Video frame rate: {frame_rate}")
+ return frame_rate
+
+ def _video2mp3(video_path, save_path):
+ # 提取音频并保存为临时文件(默认是WAV格式)
+ audio_temp_file = os.path.join(save_path, 'audio.wav')
+ ffmpeg_extract_audio(video_path, audio_temp_file)
+
+ # 将音频转换为MP3格式
+ audio_clip = mp.AudioFileClip(audio_temp_file)
+ audio_temp_file = os.path.join(save_path, 'audio.mp3')
+ audio_clip.write_audiofile(audio_temp_file)
+ # 删除临时音频文件
+ audio_clip.close()
+
+ if not os.path.exists(save_path):
+ os.makedirs(save_path)
+
+ # 视频分割
+ frame_rate = _video2img(video_path, save_path)
+ print("split picture finished!")
+ # 视频提取音频
+ _video2mp3(video_path, save_path)
+ print("extract audio finished!")
+
+ return frame_rate
+
+
+# 将图片文件夹中的图片进行人脸替换,保存到save_dir文件夹中
+def replace_all_img(template_dir, user_path, save_dir):
+ # image_face_fusion = pipeline(Tasks.image_face_fusion, model='damo/cv_unet-image-face-fusion_damo')
+ for root, dirs, files in os.walk(template_dir):
+ for file in files:
+ if file.endswith(".jpg") or file.endswith(".png"):
+ template_path = os.path.join(root, file)
+ replace_single_img(template_path=template_path, user_path=user_path,
+ save_dir=save_dir)
+
+
+# 将单张图片进行人脸替换,保存到save_dir文件夹中
+def replace_single_img(template_path, user_path, save_dir):
+ image_face_fusion = pipeline(Tasks.image_face_fusion, model='damo/cv_unet-image-face-fusion_damo')
+ filename = os.path.basename(template_path)
+ print(f"{filename} start face swapping!")
+ # filename = os.path.splitext(os.path.basename(template_path))[0]
+
+ # 替换面部依赖: template为原图,即视频拆分图; user为用户要替换脸的图
+ result = image_face_fusion(dict(template=template_path, user=user_path))
+ filepath = os.path.join(save_dir, filename)
+ cv2.imwrite(filepath, result[OutputKeys.OUTPUT_IMG])
+
+ # 释放CUDA内存
+ # 只是删除了该内部函数作用域中的变量引用,而不会影响外部函数的变量
+ del image_face_fusion
+ torch.cuda.empty_cache()
+ print(f"{filename} finished!")
+ return filepath
+
+
+# 将图片文件夹中的图片合成视频,保存到output_dir文件夹中
+def img2mp4(save_name, output_dir, img_folder, mp3_folder, fps=25):
+ images = []
+ for root, dirs, files in os.walk(img_folder):
+ for file in files:
+ if file.endswith(".jpg") or file.endswith(".png"):
+ file_path = os.path.join(root, file)
+ images.append(file_path)
+ # 将图片按照文件名进行自然排序
+ images = natsort.natsorted(images)
+
+ clips = [ImageSequenceClip(images, fps=fps)]
+ video_clip = clips[0]
+ audio_path = os.path.join(mp3_folder, 'audio.wav')
+ audio_clip = AudioFileClip(audio_path)
+
+ # 如果音频和视频时长不匹配,可以选择截取或重复音频以使其匹配视频长度
+ if audio_clip.duration > video_clip.duration:
+ audio_clip = audio_clip.subclip(0, video_clip.duration)
+ else:
+ video_clip = video_clip.subclip(0, audio_clip.duration)
+
+ video_clip = video_clip.set_audio(audio_clip)
+
+ save_name = save_name + '.mp4'
+ video_clip.write_videofile(os.path.join(output_dir, save_name), codec="libx264")
+ return os.path.join(output_dir, save_name)
+
+
+# 整个视频的替换人脸
+def replace_video(video_path, user_path):
+ # .py文件所在路径
+ BASE = os.path.dirname(__file__)
+ template_dir = os.path.join(BASE, 'video_split')
+ save_dir = os.path.join(BASE, 'video_split_swap')
+ output_dir = os.path.join(BASE, 'output')
+
+ if not os.path.exists(template_dir):
+ os.makedirs(template_dir)
+ if not os.path.exists(save_dir):
+ os.makedirs(save_dir)
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # 将视频分割成图片和音频
+ fps = video2mp3_img(video_path=video_path, save_path=template_dir)
+ replace_all_img(template_dir=template_dir, user_path=user_path, save_dir=save_dir)
+ save_path = img2mp4(save_name='swapped_video', output_dir=output_dir, img_folder=save_dir, mp3_folder=template_dir,
+ fps=fps)
+ return save_path
+
+
+if __name__ == '__main__':
+ # .py文件所在路径
+ BASE = os.path.dirname(__file__)
+ # .ipynb文件所在路径
+ # BASE = os.getcwd()
+ # 设置工作路径为当前脚本所在的路径
+ os.chdir(BASE)
+
+ user_path = r'./input/Trump.jpg'
+ template_dir = r'./middle'
+ save_dir = r'./middle_after_swap'
+ output_dir = r'./output'
+ video_path = r'./input/zhan.mp4'
+
+ # 将视频分割成图片和音频
+ fps = video2mp3_img(video_path=video_path, save_path=template_dir)
+ replace_all_img(template_dir=template_dir, user_path=user_path, save_dir=save_dir)
+ img2mp4(save_name='Zhan_Trump', output_dir=output_dir, img_folder=save_dir, mp3_folder=template_dir, fps=fps)
diff --git a/gradio_UI.py b/gradio_UI.py
new file mode 100644
index 0000000..230091d
--- /dev/null
+++ b/gradio_UI.py
@@ -0,0 +1,64 @@
+# _*_ coding : utf-8 _*_
+# @Time : 2023/11/7 21:42
+# @Author : Black
+# @File : gradio_UI
+# @Project : face_fusion_damo
+
+import gradio as gr
+import os
+from face_swap_utils import *
+
+
+def face_swap(src_picture=None, src_video=None,
+ target_picture=os.path.join(os.path.dirname(__file__), 'example/Trump.jpg')):
+ save_dir = os.path.join(os.path.dirname(__file__), 'output')
+ # 检查是否存在临时文件夹,如果不存在则创建
+ if not os.path.exists(save_dir):
+ os.makedirs(save_dir)
+
+ if src_picture is not None:
+ print(src_picture)
+ result = replace_single_img(src_picture, target_picture, save_dir)
+ return result, None
+ elif src_video is not None:
+ print(src_video)
+ result = replace_video(src_video, target_picture)
+ return None, result
+
+
+demo = gr.Interface(fn=face_swap,
+ inputs=[gr.Image(type='filepath'), gr.Video(), gr.Image(type='filepath')],
+ outputs=[gr.Image(type='filepath', label='swapped_image'),
+ gr.Video(label='swapped_video')],
+ title="🎭 Painted Skin 🎭",
+ description="# Face Swap Tool\n"
+ "Welcome to the 🎭 Painted Skin 🎭! Get ready to have some fun with faces. 😄\n"
+ "## How it Works\n"
+ "This tool allows you to perform a face swap. Simply follow these steps:\n"
+ "1. **Upload Source:**\n"
+ " - Choose between `src_picture` and `src_video` for the face you want to swap.\n"
+ " - Note: You can only upload one source at a time, and the tool processes one task at a time.\n"
+ "2. **Upload Target:**\n"
+ " - Upload `target_picture`, and our model will swap the face onto the source image or video.\n"
+ "3. **Submit and Wait:**\n"
+ " - Click the submit button and patiently wait for the process to complete.\n"
+ " - Keep in mind that face swapping in videos might take some time, so be patient! 😅\n"
+ "## Important Note\n"
+ "- Ensure that `target_picture` is uploaded for the face swap to work effectively.\n"
+ "Enjoy swapping faces and have a good laugh! 😆\n",
+
+ # css="body {background-color: red;}",
+ # article="Attention is all you need!",
+ examples=[[os.path.join(os.path.dirname(__file__), 'example/Trump.jpg'),
+ None,
+ os.path.join(os.path.dirname(__file__), 'example/Obama.jpg')],
+ [os.path.join(os.path.dirname(__file__), 'example/CXK.jpg'),
+ None,
+ os.path.join(os.path.dirname(__file__), 'example/James.jpg')]
+ ],
+ # allow_flagging="never",
+ # cache_examples=True
+ )
+
+if __name__ == "__main__":
+ demo.launch(share=True)
diff --git a/output/Zhan_Trump.mp4 b/output/Zhan_Trump.mp4
new file mode 100644
index 0000000..4292438
Binary files /dev/null and b/output/Zhan_Trump.mp4 differ
diff --git a/output/image.png b/output/image.png
new file mode 100644
index 0000000..b825e4f
Binary files /dev/null and b/output/image.png differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2ea9e87
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+gradio==4.1.2
+modelscope==1.9.4
+moviepy==1.0.3
+natsort==8.4.0
+opencv_python==4.8.1.78
+torch==1.13.1+cu116