opencv-python学习笔记(十):实现人脸特征转换

article/2025/10/3 20:51:00

引言

本次实验来自实验楼,而实验楼代码的出处为如下GitHub链接,加上一些自己的理解与说明,总结成本文笔记。

https://github.com/matthewearl/faceswap

所需环境

Dlib是一个高级的机器学习库,它是为解决复杂的现实世界问题而创建的。这个库是用C++编程语言创建的,它与C/C++、Python和java一起工作。

本次因为有dlib库需要安装,个人更习惯于Linux系统,所以本次使用环境为个人腾讯云ubuntu 18.04,关于dlib库,它的安装需要cmake与boost,如果没有编译这两个,那么pip install dlib的时候,Building wheel for dlib (setup.py)是会有问题的,即首先需要解决环境问题。

这里安装方式有简便的直接apt-get,也有拉源码重新编译,这里直接参考本篇博客:

linux下openssl、cmake与boost的更新总结

验证安装是否成功,cmake直接通过verison进行查看,boost可以运行如下脚本:

#include <iostream>
#include <boost/version.hpp>
#include <boost/config.hpp>
using namespace std;
int main(void)
{cout << BOOST_VERSION << endl;cout << BOOST_LIB_VERSION << endl;cout << BOOST_PLATFORM << endl;cout << BOOST_COMPILER << endl;cout << BOOST_STDLIB << endl;return 0;
}

将以上代码保存为test.cpp,然后再进行编译:

g++ -Wall -o test test.cpp// 106501
// 1_65_1
// linux
// GNU C++ version 7.5.0
// GNU libstdc++ version 20191114

如果出现注释中提示内容,那么基本就没有问题,因为dlib对于boost与cmake没有版本要求,所以在依赖安装没有问题的情况下,可以不使用源码,直接apt-get安装。这里不再介绍,那么下面就能直接通过pip安装dlib与opencv:

pip install opencv-python
pip install dlib

然后原git代码还用了docopt库,我看了下好像跟argparse性质差不多,记得很早之前有总结过argparse的一个demo,总结了它的一些api,画了张思维导图,这里引用一下导图,触类旁通:

在这里插入图片描述
Python利用argparse模块图片转字符文件

而关于docopt,可以直接看最上引言中引用的git链接,这也是总的代码,这里指贴出一个测试demo:

"""
faceswap can put facial features from one face onto another.Usage: faceswap [options] <image1> <image2>Options:-v --version     show the version.-h --help        show usage message.
"""import cv2
import dlib
import numpy as np
from docopt import docopt__version__ = '1.0'def main():arguments = docopt(__doc__, version=__version__)if __name__ == '__main__':main()

实验思路

  • 借助 dlib 库检测出图像中的脸部特征
  • 计算将第二张图像脸部特征对齐到一张图像脸部特征的变换矩阵
  • 综合考虑两张照片的面部特征获得合适的面部特征掩码
  • 根据第一张图像人脸的肤色修正第二张图像
  • 通过面部特征掩码拼接这两张图像
  • 保存图像

实验过程

首先需要下载dlib官方提供的模型数据shape_predictor_68_face_landmarks.dat,这里,官方的GitHub链接为:

https://github.com/davisking/dlib-models

如果网速很慢,我上传了百度云盘,能直接下载:

链接:https://pan.baidu.com/s/1RU4li6qiJwEyykwKRfN-Qg?pwd=q4iw
提取码:q4iw

这个dat从名字上就能知道,它提取的是面部耳鼻喉68个特征点的训练模型,而更复杂的128特征点这里不再过多介绍,有了这个模型之后我们就不需要自己再耗费时间去训练模型,直接拿来使用即可。

关于这68个特征点,可以根据 用Python检测人脸特征 中一图表示:
在这里插入图片描述

  • 颚点= 0–16
  • 右眉点= 17–21
  • 左眉点= 22–26
  • 鼻点= 27–35
  • 右眼点= 36–41
  • 左眼点= 42–47
  • 口角= 48–60
  • 嘴唇分数= 61–67

根据上述,用代码如下表示:

FACE_POINTS = list(range(17, 68))  # 脸
MOUTH_POINTS = list(range(48, 61))  # 嘴巴
RIGHT_BROW_POINTS = list(range(17, 22))  # 右眉毛
LEFT_BROW_POINTS = list(range(22, 27))  # 左眉毛
RIGHT_EYE_POINTS = list(range(36, 42))  # 右眼睛
LEFT_EYE_POINTS = list(range(42, 48))  # 左眼睛
NOSE_POINTS = list(range(27, 35))  # 鼻子
JAW_POINTS = list(range(0, 17))  # 下巴

我们也可以直接使用dlib画出这68个特征点:

代码为:

import dlib
import os
import cv2predictor_path  = "shape_predictor_68_face_landmarks.dat"
png_path = "ceshi.png"detector = dlib.get_frontal_face_detector()
# dlib预测器
predicator = dlib.shape_predictor(predictor_path)
img1 = cv2.imread(png_path)  dets = detector(img1,1)	# 人脸数dets
print("Number of faces detected : {}".format(len(dets)))
for k,d in enumerate(dets):print("Detection {}  left:{}  Top: {} Right {}  Bottom {}".format(k,d.left(),d.top(),d.right(),d.bottom()))lanmarks = [[p.x,p.y] for p in predicator(img1,d).parts()]  # 获取特征点for idx,point in enumerate(lanmarks):# 68点的坐标point = (point[0],point[1])cv2.circle(img1,point,5,color=(0,0,255))  # 利用cv2.circle给每个特征点画一个圈,共68个font = cv2.FONT_HERSHEY_COMPLEX_SMALLcv2.putText(img1,str(idx),point,font,0.5,(0,255,0),1,cv2.LINE_AA)	# 对标记点进行递归;cv2.namedWindow("img",cv2.WINDOW_NORMAL)
cv2.imshow("img",img1)
cv2.waitKey(0)"""
Number of faces detected : 1
Detection 0  left:161  Top: 162 Right 546  Bottom 547
"""

如果图片中有多张人脸,并且足够清晰,那么检测到多少张人脸,它就会打印出多少张人脸数与位置,并进行相应标记,只不过标记得会比一张人脸更加密集,有点影响观感,所以可以自行测试。

回归正题,首先我们需要初始化提取器以及定义之前写得特征点以及索引:

# 加载训练模型
PREDICTOR_PATH = "shape_predictor_68_face_landmarks.dat"
# 图像放缩因子
SCALE_FACTOR = 1 
FEATHER_AMOUNT = 11FACE_POINTS = list(range(17, 68))  # 脸
MOUTH_POINTS = list(range(48, 61))  # 嘴巴
RIGHT_BROW_POINTS = list(range(17, 22))  # 右眉毛
LEFT_BROW_POINTS = list(range(22, 27))  # 左眉毛
RIGHT_EYE_POINTS = list(range(36, 42))  # 右眼睛
LEFT_EYE_POINTS = list(range(42, 48))  # 左眼睛
NOSE_POINTS = list(range(27, 35))  # 鼻子
JAW_POINTS = list(range(0, 17))  # 下巴# 选取了左右眉毛、眼睛、鼻子和嘴巴位置的特征点索引
ALIGN_POINTS = (LEFT_BROW_POINTS + RIGHT_EYE_POINTS + LEFT_EYE_POINTS +RIGHT_BROW_POINTS + NOSE_POINTS + MOUTH_POINTS)# 选取用于叠加在第一张脸上的第二张脸的面部特征
# 特征点包括左右眼、眉毛、鼻子和嘴巴
OVERLAY_POINTS = [LEFT_EYE_POINTS + RIGHT_EYE_POINTS + LEFT_BROW_POINTS + RIGHT_BROW_POINTS,NOSE_POINTS + MOUTH_POINTS,
]# 定义用于颜色校正的模糊量,作为瞳孔距离的系数
COLOUR_CORRECT_BLUR_FRAC = 0.6# 实例化脸部检测器
detector = dlib.get_frontal_face_detector()
# 加载训练模型
# 并实例化特征提取器
predictor = dlib.shape_predictor(PREDICTOR_PATH)# 定义了两个类处理意外
class TooManyFaces(Exception):passclass NoFaces(Exception):passim1, landmarks1 = read_im_and_landmarks("dt.jpg")
im2, landmarks2 = read_im_and_landmarks("hc.jpg")# 选取两组图像特征矩阵中所需要的面部部位
# 计算转换信息,返回变换矩阵
M = transformation_from_points(landmarks1[ALIGN_POINTS],landmarks2[ALIGN_POINTS])

然后定义检测人脸特征点的代码,这里省略了画特征点以及一些信息的打印,做了一些限制,为了下一步的矩阵转换:

...
# 获取特征点
def get_landmarks(im):# detector 是特征检测器# predictor 是特征提取器rects = detector(im, 1)# 如果检测到多张脸if len(rects) > 1:raise TooManyFaces# 如果没有检测到脸if len(rects) == 0:raise NoFaces# 返回一个 n*2 维的矩阵,该矩阵由检测到的脸部特征点坐标组成return np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])# 读取图片文件并获取特征点
def read_im_and_landmarks(fname):# 以 RGB 模式读取图像im = cv2.imread(fname, cv2.IMREAD_COLOR)# 对图像进行适当的缩放im = cv2.resize(im, (im.shape[1] * SCALE_FACTOR,im.shape[0] * SCALE_FACTOR))s = get_landmarks(im)return im, s
...

然后是transformation_from_points 人脸对齐程序,这里的原理是因为我们拿到的特征点的脸型是第一张上面的,如果需要transform到第二张,我们就必须对特征点进行调整,如旋转、平移、缩放,这里参照的公式是:
∑ i = 1 68 ∥ s R p i T + T − q i T ∥ 2 \sum_{i=1}^{68}\left\|s R p_{i}^{T}+T-q_{i}^{T}\right\|^{2} i=168sRpiT+TqiT2

其中,旋转R,平移T,缩放S,第一个人脸关键点是 q i ( i = 1 , 2 , . . . , 68 ) q_{i}(i=1,2,...,68) qi(i=1,2,...,68),第二个人脸关键点是 p i ( i = 1 , 2 , . . . , 68 ) p_{i}(i=1,2,...,68) pi(i=1,2,...,68),关于旋转、平移与缩放,可以看上一篇关于仿射变换的实例,以及作者引用的wiki:

python-opencv学习笔记(九):图像的仿射变换与应用实例

Ordinary Procrustes Analysis

后面这篇需要梯子,它很好的阐述了仿射变换的性质:

The shape of an object can be considered as a member of an equivalence class formed by removing the translational, rotational and uniform scaling components.

并建议当对象是三维时,使用奇异值分解求解找到最佳的旋转R值(see the solution for the constrained orthogonal Procrustes problem, subject to det® = 1),而为什么用奇异值分解,这里可以参考:

奇异值分解总结 (Summary on SVD)

当然也可以不用svd,我看有资料提到了直接使用opencv做透视变换,但效果并不是很好,所以这里不再引述。那么transformer的代码为:

# 计算转换信息,返回矩阵
def transformation_from_points(points1, points2):points1 = points1.astype(np.float64)points2 = points2.astype(np.float64)c1 = np.mean(points1, axis=0)c2 = np.mean(points2, axis=0)points1 -= c1points2 -= c2s1 = np.std(points1)s2 = np.std(points2)points1 /= s1points2 /= s2U, S, Vt = np.linalg.svd(points1.T * points2)R = (U * Vt).Treturn np.vstack([np.hstack(((s2 / s1) * R,c2.T - (s2 / s1) * R * c1.T)),np.matrix([0., 0., 1.])])

在仿射变换得到矩阵后,我们还需要提取第一张图片需要替换部分的掩膜,并同样进行仿射变换,这里找了半天没找到啥比较好的图,在B站秉着学习的思想逛了一圈舞蹈区,虽然是找到很多,但是当我把两张熟悉的图转换后,我发现还是挺羞耻的,emmm。。。所以与原实验一样,选择Trump:

(4/24修改:川普和佩议长算违规图?那我赶紧拿出了我的偶像练习生与明星图,这应该不会违规了。)

获取右图的脸部掩码,并进行变化,使其和左图相符:

# 绘制凸多边形
def draw_convex_hull(im, points, color):points = cv2.convexHull(points)cv2.fillConvexPoly(im, points, color=color)# 获取面部的掩码
def get_face_mask(im, landmarks):im = np.zeros(im.shape[:2], dtype=np.float64)for group in OVERLAY_POINTS:draw_convex_hull(im,landmarks[group],color=1)im = np.array([im, im, im]).transpose((1, 2, 0))# 应用高斯模糊im = (cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0) > 0) * 1.0im = cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0)return im
...# 获取 im2 的面部掩码
mask = get_face_mask(im2, landmarks2)
plt.imshow(mask)

在这里插入图片描述

图像掩码作用于原图之后,原图中对应掩码部分为白色的部分才能显示出来,黑色的部分则不予显示,因此通过图像掩码我们就能实现对图像“裁剪”。

由 get_face_mask 获得的图像掩码还不能直接使用,因为一般来讲用户提供的两张图像的分辨率大小很可能不一样,而且即便分辨率一样,图像中的人脸由于拍摄角度和距离等原因也会呈现出不同的大小以及角度,所以如果不能只是简单地把第二个人的面部特征抠下来直接放在第一个人脸上,我们还需要根据两者计算所得的面部特征区域进行匹配变换,使得二者的面部特征尽可能重合。

...
# 变换图像
def warp_im(im, M, dshape):output_im = np.zeros(dshape, dtype=im.dtype)# 仿射函数,能对图像进行几何变换# 三个主要参数,第一个输入图像,第二个变换矩阵 np.float32 类型,第三个变换之后图像的宽高cv2.warpAffine(im,M[:2],(dshape[1], dshape[0]),dst=output_im,borderMode=cv2.BORDER_TRANSPARENT,flags=cv2.WARP_INVERSE_MAP)return output_imwarped_mask = warp_im(mask, M, im1.shape)
combined_mask = np.max([get_face_mask(im1, landmarks1), warped_mask],axis=0)
plt.imshow(combined_mask)

在这里插入图片描述

这里将第二幅图根据第一幅图同样做仿射变换,使两张人脸对齐,而我们还可以利用numpy里的multiply方法,将上述得到的掩膜作用于仿射后结果,得到掩膜下的耳鼻喉图:

warped_im2 = warp_im(im2, M, im1.shape)plt.figure(figsize=[16,16])
plt.subplot(121)
plt.imshow(cv2.cvtColor(warped_im2,cv2.COLOR_BGR2RGB))
plt.subplot(122)
plt.imshow(np.multiply(warped_im2,combined_mask).astype('uint8')[...,::-1])

在这里插入图片描述
最后我们还需要修改皮肤颜色,使两张图片在拼接时候显得更加自然:

# 修正颜色
def correct_colours(im1, im2, landmarks1):blur_amount = COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(np.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -np.mean(landmarks1[RIGHT_EYE_POINTS], axis=0))blur_amount = int(blur_amount)if blur_amount % 2 == 0:blur_amount += 1im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)# 避免出现 0 除im2_blur += (128 * (im2_blur <= 1.0)).astype(im2_blur.dtype)return (im2.astype(np.float64) * im1_blur.astype(np.float64) /im2_blur.astype(np.float64))warped_corrected_im2 = correct_colours(im1, warped_im2, landmarks1)
output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask

emmm,虽然还是很鬼畜,但至少违和感不强了,因为B站之前看过太多建国的视频,本文只是基于特征变换做的一个小demo,需要了解的东西依然还有很多,完整的代码是在开头的GitHub中,运行方式为:

$ python3 face_swap.py <image1> <image2>

之后会考虑深入了解一下人脸识别,以及之前很火的一些人脸GitHub项目,最近发现确实挺有意思的。


http://chatgpt.dhexx.cn/article/7rrRflvb.shtml

相关文章

ROS会议 ROSCon 2017

----ROSCon2012-2017----来源链接&#xff1a;https://roscon.ros.org 具体讲座的日程安排&#xff1a; 2017&#xff1a;https://roscon.ros.org/2017/ 2016&#xff1a;https://roscon.ros.org/2016/ 2015&#xff1a;https://roscon.ros.org/2015/ 2014&#xff1a;http…

Frenet坐标系下横纵向轨迹决策规划(SL投影及ST投影)及Apollo决策算法解析

参考&#xff1a; &#xff08;1&#xff09;《攻城狮说 | 应对复杂路况&#xff0c;自动驾驶如何规划它的下一步&#xff1f; “老司机”炼成记&#xff01;》微信公众号文章 Pony.ai小马智行 &#xff08;2&#xff09;《【Apollo】apollo3.5决策分享 --by 百度美研 Yifei J…

基于Ubuntu 18.04机器人操作系统环境和深度学习环境配置

基于Ubuntu 18.04机器人操作系统环境和深度学习环境配置详解 CUDACudnnROSanacondaubuntu装机必备 笔记本双系统安装U盘启动项安装ubuntu18.04.1关闭无线驱动冲突&#xff08;联想&#xff09;(option)更新软件源为国内软件源apt-get 锁问题rc.localaria2c BaiduExport ROS me…

几何向量:向量乘法(叉乘)

转载自: https://blog.csdn.net/yinhun2012/article/details/79444277 之前我们学习了物理意义上的做功&#xff0c;也就是数学中向量点积的实际意义&#xff0c;这一篇我们学习物理上另外一种力的作用&#xff0c;也就是力矩。 物理上定义力矩是力对物体产生转动作用的物理量…

向量和矩阵的点乘和叉乘

向量 定义&#xff1a;向量是由N个实数组成的一行N列或N行一列的的数组。 点乘&#xff1a;又叫做点积、内积、数量积、标量积&#xff0c;向量a[a1,a2,...,an]和向量b[b1,b2b...,bn]点乘的结果是一个标量&#xff0c;记作a.b&#xff1b; 几何解释&#xff1a;a.b |a| |b| &…

向量叉乘与叉乘矩阵

本文以三维向量来说明向量的叉乘计算原理以及叉乘矩阵如何求取 1、向量叉乘的计算原理 a、b分别为三维向量&#xff1a; a叉乘b一般定义为&#xff1a; 或 可是这只是一个符号的定义啊&#xff0c;具体怎么得到代数值呢 关键方法就是引入单位坐标向量&#xff0c; 这里用i j k…

3维向量的点乘叉乘运算

3维向量的点乘叉乘运算 文章目录 3维向量的点乘叉乘运算三维向量的点乘三维向量的叉乘点到直线的距离点到平面的距离 三维向量的点乘 点乘得到的是对应元素乘积的和&#xff0c;是一个标量&#xff0c;没有方向 V1( x1, y1, z1)V2(x2, y2, z2) x1x2 y1y2 z1*z2 点乘可以用…

通俗理解三维向量的点乘与叉乘

通俗理解三维向量的点乘和叉乘 一般接触得比较多的是二维向量的点乘和叉乘&#xff0c;但是做到与三维几何相关的工作的时候&#xff0c;三维向量的知识是必不可少的。 注意&#xff1a;三维向量和三维矢量是同一个东西&#xff0c;都是来自英文单词的Vector的中文翻译&#…

栅栏密码加密/解密【传统型】在线工具

栅栏密码(Rail-fence Cipher)就是把要加密的明文分成N个一组&#xff0c;然后把每组的第1个字符组合&#xff0c;每组第2个字符组合…每组的第N(最后一个分组可能不足N个)个字符组合&#xff0c;最后把他们全部连接起来就是密文。 遇到这种的栅栏加密的密文&#xff0c;解密的…

栅栏密码加密/解密【W型】在线工具

栅栏密码加密/解密【W型】在线工具 栅栏密码(Rail-fence Cipher)就是把要加密的明文分成N个一组&#xff0c;然后把每组的第1个字符组合&#xff0c;每组第2个字符组合…每组的第N(最后一个分组可能不足N个)个字符组合&#xff0c;最后把他们全部连接起来就是密文。 本工具所…

栅栏密码解密

题目&#xff1a; 一只小羊翻过了2个栅栏 KYsd3js2E{a2jda} 通过栅栏密码在线解密即可得出flag 解密结果&#xff1a; flagKEY{sad23jjdsa2}

猪圈密码 摩斯密码 QWE加密 栅栏加密 当铺密码

1.猪圈密码 猪圈密码&#xff1a;对应下图就是HORSE 2.摩斯密码 在线摩斯密码翻译器&#xff1a;http://www.mathsking.net/morse.htm 3.QWE加密 键盘按ABC的顺序排列得到对应的字母&#xff0c;QA,WB.... 4.栅栏加密 加密过程&#xff1a; 如果我们想要加密一句话&#xff0c;…

python3解密栅栏密码的正确方法

python3解密栅栏密码的正确方法 今天在做ctf时想找一个栅栏密码的解密脚本&#xff0c;在网上搜过了下&#xff0c;发现竟然没找到一个正确的脚本&#xff08;也可能是自己的搜索水平太差&#xff09;&#xff0c;倒不是说大家的脚本都有问题&#xff0c;只是在解决特殊情况时…

维吉尼亚密码加密/解密在线工具

维吉尼亚密码加密/解密在线工具&#xff0c;工具链接&#xff1a;http://www.atoolbox.net/Tool.php?Id856 维吉尼亚密码是在凯撒密码基础上产生的一种加密方法&#xff0c;它将凯撒密码的全部25种位移排序为一张表&#xff0c;与原字母序列共同组成26行及26列的字母表。另外…

Crypto之栅栏密码

栅栏密码是一种简单的移动字符位置的加密方法&#xff0c;规则简单&#xff0c;容易破解。栅栏密码的加密方式&#xff1a;把文本按照一定的字数分成多个组&#xff0c;取每组第一个字连起来得到密文1&#xff0c;再取每组第二个字连起来得到密文2……最后把密文1、密文2……连…

CTF-Show密码学【Base64、栅栏密码、16进制】

题目内容 密文&#xff1a;53316C6B5A6A42684D3256695A44566A4E47526A4D5459774C5556375A6D49324D32566C4D4449354F4749345A6A526B4F48303D 提交格式&#xff1a;KEY{XXXXXXXXXXXXXX}工具下载&#xff1a;https://www.lanzoui.com/i9fn2aj萌新_密码13 分析和解决过程 初步分析…

CTF密码学·置换密码,栅栏密码,曲路密码

CTF密码学置换密码&#xff0c;栅栏密码&#xff0c;曲路密码 1.置换密码列置换周期置换 2.栅栏密码3.曲路密码 1.置换密码 置换密码&#xff08;Permutation Cipher)又叫换位密码&#xff08;Transposi-tionCipher)&#xff0c;它根据一定的规则重新排列明文&#xff0c;以便…

从加(解)密角度讲栅栏密码

目录 普通型栅栏加密原理解密原理 W型栅栏加密原理实例 解密原理 这今天在做新生赛的一道“只有倒着翻过十八层的篱笆才能抵达北欧神话的终点”&#xff0c;研究了很长时间的栅栏原理&#xff0c;flag没出来&#xff0c;而且自己对于普通和W型的加密解密整的有的蒙… 原先一篇文…

栅栏密码及其变形W型栅栏密码

栅栏密码 题目描述 名称&#xff1a;铁栅栏 提示&#xff1a;我拿到一幅画&#xff0c;上面画着一个农妇在栅栏里面喂5只小鸡 密文&#xff1a;Sv9eaj3O}B{zy1vailw5YFhQ 加密原理 把将要传递的信息中的字母交替排成上下两行。再将下面一行字母排在上面一行的后边&#x…