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