利用opencv实现抖音最强变脸术

平移
缩放
旋转
平移
需要平移,是因为我们需要把两张图片上的人脸叠放到一块。平移的变换操作矩阵是:

[1001txty] \left[\begin{matrix}1 & 0 & tx \0 & 1 & ty\end{matrix} \right]
[
1
0

0
1

tx
ty

]

例如我们要向右平移100个像素,向下平移50个像素,那么变换矩阵就应该是:

[100110050] \left[\begin{matrix}1 & 0 & 100 \0 & 1 & 50\end{matrix} \right]
[
1
0

0
1

100
50

]

对应的运算是:

[x′y′]=[100110050]∗⎡⎣xy1⎤⎦ \left[\begin{matrix}x’ \y’ \\end{matrix} \right]=\left[\begin{matrix}1 & 0 & 100 \0 & 1 & 50\end{matrix} \right]*\left[\begin{matrix}x \y \1\end{matrix} \right]
[
x

y


]=[
1
0

0
1

100
50

]∗


x
y
1




{x′=1∗x+0∗y+100∗1y′=0∗x+1∗y+50∗1 \begin{cases}x’=1x+0y+1001 \y’=0x+1y+501\end{cases}
{
x

=1∗x+0∗y+100∗1
y

=0∗x+1∗y+50∗1

所以平移操作的本质就是对每个像素加上一个偏移量。下面是使用opencv对图像进行平移操作的代码:

img = cv2.imread(“./imgs/2.jpg”)
M = np.float32(
[
[1, 0, 100],
[0, 1, 50]
]
)

dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
cv2.imshow(“”, dst)
cv2.waitKey()
1
2
3
4
5
6
7
8
9
10
11
运行上面的代码可以看到这样的结果:

缩放
需要缩放,是因为我们在人脸对齐的时候需要尽可能的保证两张人脸大小一致。缩放的变换操作矩阵是:

[fx00fy00] \left[\begin{matrix}fx & 0 & 0 \0 & fy & 0\end{matrix} \right]
[
fx
0

0
fy

0
0

]

fx代表x方向的缩放因子,fy代表y方向的缩放因子。所以如果我们想x轴放大1.5倍,y轴放大2倍的代码如下:

img = cv2.imread(“./imgs/2.jpg”)
M = np.float32(
[
[1.5, 0, 0],
[0, 2, 0]
]
)

dst = cv2.warpAffine
(img, M, (img.shape[1], img.shape[0]))
cv2.imshow(“”, dst)
cv2.waitKey()
1
2
3
4
5
6
7
8
9
10
11
运行上面的代码可以看到这样的结果:

旋转
需要旋转,是因为我们需要把两张图片上的人脸进行对齐操作。旋转的变换操作矩阵是:

⎡⎣cos(θ)sin(θ)0−sin(θ)cos(θ)0001⎤⎦ \left[\begin{matrix}cos(\theta) & -sin(\theta) & 0 \sin(\theta) & cos(\theta) & 0 \0 & 0 & 1\end{matrix} \right]


cos(θ)
sin(θ)
0

−sin(θ)
cos(θ)
0

0
0
1



如果我们想要旋转30度,可以使用一下代码:

img = cv2.imread(“./imgs/2.jpg”)

theta = math.radians(-30)
M = np.float32(
[
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0]
]
)

dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
cv2.imshow(“”, dst)
cv2.waitKey()
1
2
3
4
5
6
7
8
9
10
11
12
13
运行效果如下:

观察结果可能发现了,这次旋转的中心是在原点,如果我们想以任意点为旋转中心怎么办? opencv提供了一个函数:

getRotationMatrix2D(center, angle, scale)
1
center: 指定旋转的中心

angle: 旋转角度

scale: 缩放因子

这个函数还顺手解决了我们上面需要的缩放操作。可以比较下面代码和上面的效果:

img = cv2.imread(“./imgs/2.jpg”)

M = cv2.getRotationMatrix2D((img.shape[1], img.shape[0]), 30, 1)

dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
cv2.imshow(“”, dst)
cv2.waitKey()
1
2
3
4
5
6
7

最强变脸术第一次实现
仿射变换版本其实就是利用了以上三种变换方式的组合,首先先定义一个函数入口。

def compose_img(name, frames_per_transformer, wait_frames, *imgs):
pass
1
2
参数1:生成视频的文件名

参数2:每两张图像之前的变换(称之为1次迭代)需要多少帧

参数3:每个迭代后写入多少帧静态图,也就是每次迭代完成后图片保持多少帧不变

参数4:参与生成视频的图片集合

除了这个函数外,我们还需要几个辅助函数。

def to_video(name, width, height):
fps = 10
video_writer = cv2.VideoWriter(name, cv2.VideoWriter_fourcc(‘I’, ‘4’, ‘2’, ‘0’), fps, (width, height))

return video_writer

def get_equation(x0, y0, x1, y1, pow_arg=1):
k = (y1 - y0) / (pow(x1, pow_arg) - pow(x0, pow_arg))
b = y0 - k * pow(x0, pow_arg)

def f(x):
return k * pow(x, pow_arg) + b

return f

def get_rotate_theta(from_landmarks, to_landmarks):
from_left_eye = from_landmarks[36]
from_right_eye = from_landmarks[45]

to_left_eye = to_landmarks[36]
to_right_eye = to_landmarks[45]

from_angle = math.atan2(from_right_eye[1] - from_left_eye[1], from_right_eye[0] - from_left_eye[0])
to_angle = math.atan2(to_right_eye[1] - to_left_eye[1], to_right_eye[0] - to_left_eye[0])

from_theta = -from_angle * (180 / math.pi)
to_theta = -to_angle * (180 / math.pi)

return to_theta - from_theta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
to_video函数主要是用来创建一个视频生成器的。get_equation函数是用来生成一个根据时间变化的方程,主要用到了一次方程和二次方程。get_rotate_theta这个函数是通过计算左右眼的夹角来估计人脸倾斜角度差值,下标的值可以参考第一张图片。

最后我们就要进入主函数的实现了,主要思路是遍历所有图片,每个迭代拿出当前图和下一张图,然后识别出两张人脸中的关键点,通过这些关键点我们可以计算出两张图在某一时刻需要的旋转角度、旋转中心、缩放比例、位移像素数等关键参数。最终我们再次迭代frames_per_transformer次通过对两张图片分别做旋转、平移变换来达到效果。

def compose_img(name, frames_per_transformer, wait_frames, *imgs):
video_writer = to_video(“{}.avi”.format(name), imgs[0].shape[1], imgs[0].shape[0])

img_count = len(imgs)
for idx in range(img_count - 1):
from_img = imgs[idx]
to_img = imgs[idx + 1]

from_width = from_img.shape[1]
from_height = from_img.shape[0]

to_width = to_img.shape[1]
to_height = to_img.shape[0]

from_face_region, from_landmarks = face_detector(from_img)
to_face_region, to_landmarks = face_detector(to_img)

第一张图最终的旋转角度

from_theta = get_rotate_theta(from_landmarks, to_landmarks)

第二张图初始的旋转角度

to_theta = get_rotate_theta(to_landmarks, from_landmarks)

两张图的旋转中心

from_rotate_center = (from_face_region.left() + (from_face_region.right() - from_face_region.left()) / 2, from_face_region.top() + (from_face_region.bottom() - from_face_region.top()) / 2)
to_rotate_center = (to_face_region.left() + (to_face_region.right() - to_face_region.left()) / 2, to_face_region.top() + (to_face_region.bottom() - to_face_region.top())/2)

from_face_area = from_face_region.area()
to_face_area = to_face_region.area()

第一张图的最终缩放因子

to_scaled = from_face_area / to_face_area

第二张图的初始缩放因子

from_scaled = to_face_area / from_face_area

平移多少的基准

to_translation_base = to_rotate_center
from_translation_base = from_rotate_center

equation_pow = 1 if idx % 2 == 0 else 2

建立变换角度的方程

to_theta_f = get_equation(0, to_theta, frames_per_transformer - 1, 0, equation_pow)
from_theta_f = get_equation(0, 0, frames_per_transformer - 1, from_theta, equation_pow)

建立缩放系数的角度

to_scaled_f = get_equation(0, to_scaled, frames_per_transformer - 1, 1, equation_pow)
from_scaled_f = get_equation(0, 1, frames_per_transformer - 1, from_scaled, equation_pow)

for i in range(frames_per_transformer):

当前时间点的旋转角度

cur_to_theta = to_theta_f(i)
cur_from_theta = from_theta_f(i)

当前时间点的缩放因子

cur_to_scaled = to_scaled_f(i)
cur_from_scaled = from_scaled_f(i)

生成第二张图片变换矩阵

to_rotate_M = cv2.getRotationMatrix2D(to_rotate_center, cur_to_theta, cur_to_scaled)

对第二张图片执行仿射变换

to_dst = cv2.warpAffine(to_img, to_rotate_M, (to_width, to_height), borderMode=cv2.BORDER_REPLICATE)

生成第一张图片的变换矩阵

from_rotate_M = cv2.getRotationMatrix2D(from_rotate_center, cur_from_theta, cur_from_scaled)

对第一张图片执行仿射变换

from_dst = cv2.warpAffine(from_img, from_rotate_M, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)

重新计算变换后的平移基准

to_left_rotated = to_rotate_M[0][0] * to_translation_base[0] + to_rotate_M[0][1] * to_translation_base[1] + to_rotate_M[0][2]
to_top_rotated = to_rotate_M[1][0] * to_translation_base[0] + to_rotate_M[1][1] * to_translation_base[1] + to_rotate_M[1][2]

from_left_rotated = from_rotate_M[0][0] * from_translation_base[0] + from_rotate_M[0][1] * from_translation_base[1] + from_rotate_M[0][2]
from_top_rotated = from_rotate_M[1][0] * from_translation_base[0] + from_rotate_M[1][1] * from_translation_base[1] + from_rotate_M[1][2]

当前时间点的平移数

to_left_f = get_equation(0, from_left_rotated - to_left_rotated, frames_per_transformer - 1, 0, equation_pow)
to_top_f = get_equation(0, from_top_rotated - to_top_rotated, frames_per_transformer - 1, 0, equation_pow)

from_left_f = get_equation(0, 0, frames_per_transformer - 1, to_left_rotated - from_left_rotated, equation_pow)
from_top_f = get_equation(0, 0, frames_per_transformer - 1, to_top_rotated - from_top_rotated, equation_pow)

生成第二张图片平移的变换矩阵

to_translation_M = np.float32(
[
[1, 0, to_left_f(i)],
[0, 1, to_top_f(i)]
]
)

对第二张图片执行平移变换

to_dst = cv2.warpAffine(to_dst, to_translation_M, (to_width, to_height), borderMode=cv2.BORDER_REPLICATE)

生成第一张图片平移的变换矩阵

from_translation_M = np.float32(
[
[1, 0, from_left_f(i)],
[0, 1, from_top_f(i)]
]
)

对第一张图片执行平移变换

from_dst = cv2.warpAffine(from_dst, from_translation_M, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)

将两张图片合成到一张,并写入视频帧

new_img = cv2.addWeighted(from_dst, 1 - ((i + 1) / frames_per_transformer), to_dst, (i + 1) / frames_per_transformer, 0)
video_writer.write(new_img)

一个迭代完成,迭代n次写入第二张图片

for _ in range(wait_frames):
video_writer.write(to_img)

video_writer.release()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
以上就是利用仿射变换实现的代码。效果可以看下面的gif(忽略gif的颜色问题,视频正常!完整视频可以到http://www.iqiyi.com/w_19saz225ol.html查看)

通过观察效果和代码,我们来总结一下这个版本的不足之处:

两张人脸并未真正实现大小一致。
人脸对齐也做的不够好。
仅在2D空间做了变换,对于脸朝向的变换不敏感。
代码复杂。
仅利用了68个人脸关键点中的一小部分,并未充分利用人脸的特征。
以上几个问题其实就决定了仿射变换版本的使用局限性很大,跟抖音实现的效果差距很大。这也迫使我寻找另一种解决方案,结果就是透视变换版本,这个版本代码简单而且效果更接近抖音。

透视变换
仿射变换仅在二维空间做线性变换和平移,所以两条平行线变换后还是平行的,因而我们感受不到立体变换的效果。而透视变换则不同,它是在3D空间做变换,最后在映射到2D平面。以下是透视变换的数学原理。

⎡⎣x′y′z⎤⎦=⎡⎣a11a21a31a12a22a32a13a23a33⎤⎦∗⎡⎣xy1⎤⎦ \left[\begin{matrix}x’ \y’ \z\end{matrix} \right]=\left[\begin{matrix}a_{11} & a_{12} & a_{13} \a_{21} & a_{22} & a_{23} \a_{31} & a_{32} & a_{33}\end{matrix} \right]*\left[\begin{matrix}x \y \1\end{matrix} \right]


x

y

z





a
11

a
21

a
31

a
12

a
22

a
32

a
13

a
23

a
33







x
y
1



从公式中可以看到变换后做了第3个维度z。展开为方程组形式:
⎧⎩⎨⎪⎪x′=a11∗x+a12∗y+a13y′=a21∗x+a22∗y+a23z=a31∗x+a32∗y+a33 \begin{cases}x’=a_{11}*x+a_{12}*y+a_{13} \y’=a_{21}*x+a_{22}*y+a_{23} \z=a_{31}*x+a_{32}*y+a_{33}\end{cases}





x

=a
11

∗x+a
12

∗y+a
13

y

=a
21

∗x+a
22

∗y+a
23

z=a
31

∗x+a
32

∗y+a
33

最后映射回2维空间:
⎧⎩⎨x′=x′z=a11∗x+a12∗y+a13a31∗x+a32∗y+a33y′=y′z=a21∗x+a22∗y+a23a31∗x+a32∗y+a33 \begin{cases}x’=\frac{x’}{z}=\frac{a_{11}*x+a_{12}*y+a_{13}}{a_{31}*x+a_{32}*y+a_{33}} \y’=\frac{y’}{z}=\frac{a_{21}*x+a_{22}*y+a_{23}}{a_{31}*x+a_{32}*y+a_{33}}\end{cases}
{
x

z
x

a
31

∗x+a
32

∗y+a
33

a
11

∗x+a
12

∗y+a
13

y

z
y

a
31

∗x+a
32

∗y+a
33

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如果你需要这些资料, ⬅ 专栏获取
}{a_{31}*x+a_{32}*y+a_{33}}\end{cases}
{
x

z
x

a
31

∗x+a
32

∗y+a
33

a
11

∗x+a
12

∗y+a
13

y

z
y

a
31

∗x+a
32

∗y+a
33

最后

小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

[外链图片转存中…(img-f1WJeCAO-1719340631585)]

[外链图片转存中…(img-hoPvHmAX-1719340631586)]

[外链图片转存中…(img-pdun3mos-1719340631586)]

[外链图片转存中…(img-b6dUtcRX-1719340631587)]

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如果你需要这些资料, ⬅ 专栏获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值