项目地址:QuantumLiu/FaceSwapper
百度网盘
****
还有三天就是七夕情人节了,你是不是正在规划怎么和TA度过一个浪漫的七夕节呢?
情书?
玫瑰、红酒、烛光晚餐?
买买买?
相信你的七夕已经安排得很浪漫了~
这里还有一款因缺思厅的Python程序,可以为你的浪漫七夕增添一点幽默!
这个程序实现了一张照片内两个人脸的互换,照片可以来自你和TA的合影,也可以是和你的好朋友、老板、仇人,甚至任何你想恶搞的人~
(我说我一个单身狗,怎么想的做这个程序,强行喂自己狗粮,摔!)




(本文使用图片,除特朗普和希拉里以外都来自pixabay和别样网)
使用
程序提供了GUI版,也提供了命令行版和接口。
最简单的,下载程序以后(coupleswapper.exe以及ico文件,txt文件和data文件夹),
双击程序,打开主界面。
或者通过源码:
- python coupleswapper.py

点击加载图片,选择要打开的照片。

程序会弹出Origin窗口预览。

接下来点击转换,即可完成转换,会弹出result窗口预览。

如果你想看一看对比反差并保存对比图,请点击保存对比,会弹出预览图片并让你选择保存位置。


如果只想保存转换后的结果,请点击保存结果。
祝各位玩的开心~
汪!
预告:编写本程序让单身狗作者受到了来自自己的1T点暴击,本系列下一作品,是单身狗七夕YY神器【换脸系列3】和女神/男神在一起的应该是我!

编程实现
在本系列上一篇【换脸系列1】军装照刷爆朋友圈?教你用Python+深度学习自制换脸软件!(改进)中,我们实现了将一张照片上的脸换到另一张照片的头上,并且用面向对象编程的方法定义了一个Faceswapper类。

这一次,我们是要在一张照片上,将两个脸互换。
显而易见,读取和写入图片、人脸定位和特征提取,这些功能我们都在Faceswapper里实现了,新的任务需要的操作是一样的,我们希望直接复用已有的操作。
使用类的继承是最常用的实现复用的方法。
我们新定义一个类Coupleswapper,他继承自Faceswapper类。
- class Coupleswapper(Faceswapper):
- '''
- 双人照人脸交换器类,继承自Faceswapper类
- 实例化时载入多个照片资源
- '''
这样,新的Coupleswapper类继承了父类的所有属性和方法。
接下来,我们根据任务的不同,改写一些类。
在父类中的get_landmarks方法中,我们要求脸数只能为1,否则抛出异常,最后返回一个landmarks的martrix。
现在,我们做的是给一张图片的两个脸换脸,脸数应当>=2,而且最后一次返回一个有两组landmark的list。所以我们需要改写get_landmarks方法。
- def get_landmarks(self,im,fname,n=2):
- '''
- 人脸定位和特征提取,定位到两张及以上脸或者没有人脸将抛出异常
- im:
- 照片的numpy数组
- fname:
- 照片名字的字符串
- 返回值:
- 人脸特征(x,y)坐标的矩阵
- '''
- rects = self.detector(im, 1)
-
- if len(rects) >=5:
- raise TooManyFaces('Too many faces in '+fname)
- if len(rects) <2:
- raise NoFace('No enough face in' +fname)
- return [np.matrix([[p.x, p.y] for p
- in self.predictor(im, rect).parts()]) for rect in rects]
当然,执行换脸完整过程的swap函数也要改写。
换每个脸的过程和原程序差不多,但是我们需要一个copy作为输出画布,还要在两个图片间进行互为脸/头来源进行循环。
- def swap(self,im_name):
- '''
- 主函数 人脸交换
- im_name
- 合影图片的键名字符串
- '''
- im,landmarks=self.heads[im_name]
- out_im=im.copy()#画布
- for i in [1,-1]:
- landmarks_head,landmarkslandmarks_face=landmarks[:2][::i]#实现倒序
- M = self.transformation_from_points(landmarks_head[self.ALIGN_POINTS],
- landmarks_face[self.ALIGN_POINTS])
-
- face_mask = self.get_face_mask(im, landmarks_face)
- warped_mask = self.warp_im(face_mask, M, im.shape)
- combined_mask = np.max([self.get_face_mask(im, landmarks_head), warped_mask],
- axis=0)
- warped_face = self.warp_im(im, M, im.shape)
- warped_corrected_im = self.correct_colours(im, warped_face, landmarks_head)
- out_imout_im=out_im * (1.0 - combined_mask) + warped_corrected_im * combined_mask
- return out_im
GUI界面
面向大众的程序还要有直观、易操作的GUI界面。
我们使用pyqt+pyinstaller来实习GUI和exe发布。
- import os,traceback
- from PyQt5.QtWidgets import QFileDialog
- from PyQt5 import QtCore,QtGui, QtWidgets
- import cv2
- import numpy as np
- from coupleswapper import Coupleswapper,TooManyFaces,NoFace
-
- class Ui_Form(QtWidgets.QMainWindow):
- def __init__(self):
- super(Ui_Form,self).__init__()
- self.swapper=[]
- self.im_path=''
- self.cur_im_path=''
- self.img_swapped=None
- self.img_ori=None
- self.compare=None
-
- def setupUi(self, Form):
- Form.setObjectName("Form")
- Form.resize(270, 360)
- Form.setAccessibleName("")
- self.verticalLayout = QtWidgets.QVBoxLayout(Form)
- self.verticalLayout.setObjectName("verticalLayout")
- Form.setWindowIcon(QtGui.QIcon('./male_female.ico'))
- self.help_label=QtWidgets.QLabel(Form)
- self.verticalLayout.addWidget(self.help_label)
- with open('./readme.txt','r',encoding='utf8') as f:
- self.readme=f.read()
- #显示运行log
- self.statu_text=QtWidgets.QTextBrowser(Form)
- selfself.vb=self.statu_text.verticalScrollBar()
- self.verticalLayout.addWidget(self.statu_text)#支持滚轮
- #加载按钮
- self.bt_load = QtWidgets.QPushButton(Form)
- self.bt_load.setDefault(True)
- self.bt_load.setObjectName("bt_load")
- self.bt_load.clicked.connect(self.load_image)
- self.verticalLayout.addWidget(self.bt_load)
- #转换按钮
- self.bt_swap = QtWidgets.QPushButton(Form)
- self.bt_swap.setDefault(True)
- self.bt_swap.setObjectName("bt_swap")
- self.bt_swap.clicked.connect(self.swap)
- self.verticalLayout.addWidget(self.bt_swap)
- #保存结果按钮
- self.bt_save = QtWidgets.QPushButton(Form)
- self.bt_save.setDefault(True)
- self.bt_save.setObjectName("bt_save")
- self.bt_save.clicked.connect(self.save_result)
- self.verticalLayout.addWidget(self.bt_save)
- #保存对比图按钮
- self.bt_save_comp = QtWidgets.QPushButton(Form)
- self.bt_save_comp.setDefault(True)
- self.bt_save_comp.clicked.connect(self.save_compare)
- self.bt_save_comp.setObjectName("bt_save_comp")
- self.verticalLayout.addWidget(self.bt_save_comp)
-
-
- self.retranslateUi(Form)
- QtCore.QMetaObject.connectSlotsByName(Form)
-
- def retranslateUi(self, Form):
- _translate = QtCore.QCoreApplication.translate
- Form.setWindowTitle(_translate("Form", "交换♂身体"))
- self.bt_load.setText(_translate("Form", "加载图片"))
- self.bt_swap.setText(_translate("Form", "转换"))
- self.bt_save.setText(_translate("Form", "保存结果"))
- self.bt_save_comp.setText(_translate("Form", "保存对比"))
- self.help_label.setText(_translate("Form",self.readme))
- self.statu_text.setText(_translate('Form','欢迎使用,请选择文件'))
-
-
- def load_image(self):
- '''
- 加载原图
- '''
- try:
- im_path,_=QFileDialog.getOpenFileName(self,'打开图片文件'
- ,'./','Image Files(*.png *.jpg *.bmp)')
- if not os.path.exists(im_path):
- return
- self.im_path=im_path
- self.statu_text.append('打开图片文件:'+self.im_path)
- if not self.swapper:
- self.swapper=Coupleswapper([self.im_path])
- elif not self.im_path== self.cur_im_path:
- self.swapper.load_heads([self.im_path])
- selfself.img_ori=self.swapper.heads[os.path.split(self.im_path)[-1]][0]
- cv2.imshow('Origin',self.img_ori)
- except (TooManyFaces,NoFace):
- self.statu_text.append(traceback.format_exc()+' 人脸定位失败,请重新选择!保证照片中有两张可识别的人脸。')
- return
-
- def swap(self):
- '''
- 执行换脸
- '''
- if not (self.swapper and os.path.exists(self.im_path)):
- return
- self.statu_text.append('转换成功!')
- selfself.img_swapped=self.swapper.swap(os.path.split(self.im_path)[-1])
- self.img_swapped[self.img_swapped>254.9]=254.9
- selfself.img_swapped=self.img_swapped.astype('uint8')
- cv2.imshow('Result',self.img_swapped)
-
- def save_result(self):
- '''
- 保存结果
- '''
- output_path,_=QFileDialog.getSaveFileName(self,'选择保存位置'
- ,'./','Image Files(*.png *.jpg *.bmp)')
- if not output_path:
- self.statu_text.append('无效路径,请重新选择')
- return
- self.swapper.save(output_path,self.img_swapped)
- self.statu_text.append('成功保存到:'+output_path)
-
- def save_compare(self):
- '''
- 保存对比图
- '''
- self.compare=np.concatenate([self.img_ori,self.img_swapped],1)
- cv2.imshow('Compare',self.compare)
- output_path,_=QFileDialog.getSaveFileName(self,'选择保存位置'
- ,'./','Image Files(*.png *.jpg *.bmp)')
- if not output_path:
- self.statu_text.append('无效路径,请重新选择')
- return
- self.swapper.save(output_path,self.compare)
- self.statu_text.append('成功保存对比图到:'+output_path)
- if __name__ == "__main__":
- import sys
- app = QtWidgets.QApplication(sys.argv)
- Form = QtWidgets.QWidget()
- ui = Ui_Form()
- ui.setupUi(Form)
- try:
- Form.show()
- except:
- traceback.print_exc()
- finally:
- cv2.destroyAllWindows()
- sys.exit(app.exec_())
(天清) |