利用OpenCV实现抖音最强变脸术 | CSDN原力计划

作者 | 亓斌

来源 | CSDN原力计划获奖作品

(*点击阅读原文,查看作者更多文章)

最近一个“最强变脸术”又火爆抖音啦,还不知道的朋友建议先打开抖音,搜索“最强变脸术”看个十来个视频再回来看这篇文章。视频看起来炫酷,其实本质就是图像的各种变换组合到一块的结果。那我们能不能也搞出一个来玩玩?我利用周末刷了两天抖音,不停的暂停、继续… 最终在尝试了仿射变换和透视变换两种方案后,搞出了一个“低配版最强变脸术”。首先先来看看最终实现的效果(忽略gif颜色问题),也可以到http://www.iqiyi.com/w_19saz1z92h.html查看完整视频,然后从数学原理、opencv代码实现入手一步步的搞一个“最强变脸术”。

人脸关键点识别

看过“最强变脸术”的都知道,这个效果最基础的技术就是人脸识别。都2020年了,人脸识别当然不是多难的事了,可以选择的技术也很多,比如可以利用深度学习自己训练一个,也可以和我一样使用dlib这个三方库。

dlib用起来很简单,下面直接上代码了。

 1img = cv2.imread("./imgs/2.jpg")
 2dets = detector(img, 1)
 3
 4shape = predictor(img, dets[0])
 5landmarks = []
 6for p in shape.parts():
 7    landmarks.append(np.array([p.x, p.y]))
 8
 9for idx, point in enumerate(landmarks):
10    cv2.putText(img, str(idx), (point[0], point[1]), fontFace=cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,
11                fontScale=0.3, color=(0, 255, 0))
12cv2.imshow("--", img)
13cv2.waitKey()

运行上面的代码可以看到这样的结果:

请注意上面图中36、45、29三个数字的位置,因为在下面仿射变换的版本中我们要用到。

版本一:仿射变换实现

人脸关键点搞定后的第一次尝试,我是用的图像仿射变换来实现的。通过不断观察,我拆解出了一下三种变换方式:

  • 平移

  • 缩放

  • 旋转

平移

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

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

对应的运算是:

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

 1img = cv2.imread("./imgs/2.jpg")
 2M = np.float32(
 3    [
 4        [1, 0, 100],
 5        [0, 1, 50]
 6    ]
 7)
 8
 9dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
10cv2.imshow("", dst)
11cv2.waitKey()

运行上面的代码可以看到这样的结果:

缩放

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

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

 1img = cv2.imread("./imgs/2.jpg")
 2M = np.float32(
 3    [
 4        [1.5, 0, 0],
 5        [0, 2, 0]
 6    ]
 7)
 8
 9dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
10cv2.imshow("", dst)
11cv2.waitKey()

运行上面的代码可以看到这样的结果:

旋转

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

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

 1img = cv2.imread("./imgs/2.jpg")
 2
 3theta = math.radians(-30)
 4M = np.float32(
 5    [
 6        [np.cos(theta), -np.sin(theta), 0],
 7        [np.sin(theta), np.cos(theta), 0]
 8    ]
 9)
10
11dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
12cv2.imshow("", dst)
13cv2.waitKey()

运行效果如下:

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

1getRotationMatrix2D(center, angle, scale)

center: 指定旋转的中心
angle: 旋转角度
scale: 缩放因子

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

1img = cv2.imread("./imgs/2.jpg")
2
3M = cv2.getRotationMatrix2D((img.shape[1], img.shape[0]), 30, 1)
4
5dst = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
6cv2.imshow("", dst)
7cv2.waitKey()

最强变脸术第一次实现

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

1def compose_img(name, frames_per_transformer, wait_frames, *imgs):
2    pass

参数1:生成视频的文件名
参数2:每两张图像之前的变换(称之为1次迭代)需要多少帧
参数3:每个迭代后写入多少帧静态图,也就是每次迭代完成后图片保持多少帧不变
参数4:参与生成视频的图片集合

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

 1def to_video(name, width, height):
 2    fps = 10
 3    video_writer = cv2.VideoWriter(name, cv2.VideoWriter_fourcc('I', '4', '2', '0'), fps, (width, height))
 4
 5    return video_writer
 6
 7def get_equation(x0, y0, x1, y1, pow_arg=1):
 8    k = (y1 - y0) / (pow(x1, pow_arg) - pow(x0, pow_arg))
 9    b = y0 - k * pow(x0, pow_arg)
10
11    def f(x):
12        return k * pow(x, pow_arg) + b
13
14    return f
15
16def get_rotate_theta(from_landmarks, to_landmarks):
17    from_left_eye = from_landmarks[36]
18    from_right_eye = from_landmarks[45]
19
20    to_left_eye = to_landmarks[36]
21    to_right_eye = to_landmarks[45]
22
23    from_angle = math.atan2(from_right_eye[1] - from_left_eye[1], from_right_eye[0] - from_left_eye[0])
24    to_angle = math.atan2(to_right_eye[1] - to_left_eye[1], to_right_eye[0] - to_left_eye[0])
25
26    from_theta = -from_angle * (180 / math.pi)
27    to_theta = -to_angle * (180 / math.pi)
28
29    return to_theta - from_theta

to_video函数主要是用来创建一个视频生成器的。get_equation函数是用来生成一个根据时间变化的方程,主要用到了一次方程和二次方程。get_rotate_theta这个函数是通过计算左右眼的夹角来估计人脸倾斜角度差值,下标的值可以参考第一张图片。

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

  1def compose_img(name, frames_per_transformer, wait_frames, *imgs):
  2    video_writer = to_video("{}.avi".format(name), imgs[0].shape[1], imgs[0].shape[0])
  3
  4    img_count = len(imgs)
  5    for idx in range(img_count - 1):
  6        from_img = imgs[idx]
  7        to_img = imgs[idx + 1]
  8
  9        from_width = from_img.shape[1]
 10        from_height = from_img.shape[0]
 11
 12        to_width = to_img.shape[1]
 13        to_height = to_img.shape[0]
 14
 15        from_face_region, from_landmarks = face_detector(from_img)
 16        to_face_region, to_landmarks = face_detector(to_img)
 17
 18        # 第一张图最终的旋转角度
 19        from_theta = get_rotate_theta(from_landmarks, to_landmarks)
 20        # 第二张图初始的旋转角度
 21        to_theta = get_rotate_theta(to_landmarks, from_landmarks)
 22
 23        # 两张图的旋转中心
 24        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)
 25        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)
 26
 27        from_face_area = from_face_region.area()
 28        to_face_area = to_face_region.area()
 29
 30        # 第一张图的最终缩放因子
 31        to_scaled = from_face_area / to_face_area
 32        # 第二张图的初始缩放因子
 33        from_scaled = to_face_area / from_face_area
 34
 35        # 平移多少的基准
 36        to_translation_base = to_rotate_center
 37        from_translation_base = from_rotate_center
 38
 39        equation_pow = 1 if idx % 2 == 0 else 2
 40
 41        # 建立变换角度的方程
 42        to_theta_f = get_equation(0, to_theta, frames_per_transformer - 1, 0, equation_pow)
 43        from_theta_f = get_equation(0, 0, frames_per_transformer - 1, from_theta, equation_pow)
 44
 45        # 建立缩放系数的角度
 46        to_scaled_f = get_equation(0, to_scaled, frames_per_transformer - 1, 1, equation_pow)
 47        from_scaled_f = get_equation(0, 1, frames_per_transformer - 1, from_scaled, equation_pow)
 48
 49        for i in range(frames_per_transformer):
 50            # 当前时间点的旋转角度
 51            cur_to_theta = to_theta_f(i)
 52            cur_from_theta = from_theta_f(i)
 53
 54            # 当前时间点的缩放因子
 55            cur_to_scaled = to_scaled_f(i)
 56            cur_from_scaled = from_scaled_f(i)
 57
 58            # 生成第二张图片变换矩阵
 59            to_rotate_M = cv2.getRotationMatrix2D(to_rotate_center, cur_to_theta, cur_to_scaled)
 60            # 对第二张图片执行仿射变换
 61            to_dst = cv2.warpAffine(to_img, to_rotate_M, (to_width, to_height), borderMode=cv2.BORDER_REPLICATE)
 62
 63            # 生成第一张图片的变换矩阵
 64            from_rotate_M = cv2.getRotationMatrix2D(from_rotate_center, cur_from_theta, cur_from_scaled)
 65            # 对第一张图片执行仿射变换
 66            from_dst = cv2.warpAffine(from_img, from_rotate_M, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)
 67
 68            # 重新计算变换后的平移基准
 69            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]
 70            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]
 71
 72            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]
 73            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]
 74
 75            # 当前时间点的平移数
 76            to_left_f = get_equation(0, from_left_rotated - to_left_rotated, frames_per_transformer - 1, 0, equation_pow)
 77            to_top_f = get_equation(0, from_top_rotated - to_top_rotated, frames_per_transformer - 1, 0, equation_pow)
 78
 79            from_left_f = get_equation(0, 0, frames_per_transformer - 1, to_left_rotated - from_left_rotated, equation_pow)
 80            from_top_f = get_equation(0, 0, frames_per_transformer - 1, to_top_rotated - from_top_rotated, equation_pow)
 81
 82            # 生成第二张图片平移的变换矩阵
 83            to_translation_M = np.float32(
 84                [
 85                    [1, 0, to_left_f(i)],
 86                    [0, 1, to_top_f(i)]
 87                ]
 88            )
 89
 90            # 对第二张图片执行平移变换
 91            to_dst = cv2.warpAffine(to_dst, to_translation_M, (to_width, to_height), borderMode=cv2.BORDER_REPLICATE)
 92
 93            # 生成第一张图片平移的变换矩阵
 94            from_translation_M = np.float32(
 95                [
 96                    [1, 0, from_left_f(i)],
 97                    [0, 1, from_top_f(i)]
 98                ]
 99            )
100
101            # 对第一张图片执行平移变换
102            from_dst = cv2.warpAffine(from_dst, from_translation_M, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)
103
104            # 将两张图片合成到一张,并写入视频帧
105            new_img = cv2.addWeighted(from_dst, 1 - ((i + 1) / frames_per_transformer), to_dst, (i + 1) / frames_per_transformer, 0)
106            video_writer.write(new_img)
107
108        # 一个迭代完成,迭代n次写入第二张图片
109        for _ in range(wait_frames):
110            video_writer.write(to_img)
111
112    video_writer.release()

以上就是利用仿射变换实现的代码。效果可以看下面的gif(忽略gif的颜色问题,视频正常!完整视频可以到http://www.iqiyi.com/w_19saz225ol.html查看)

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

1. 两张人脸并未真正实现大小一致。
2. 人脸对齐也做的不够好。
3. 仅在2D空间做了变换,对于脸朝向的变换不敏感。
4. 代码复杂。
5. 仅利用了68个人脸关键点中的一小部分,并未充分利用人脸的特征。

以上几个问题其实就决定了仿射变换版本的使用局限性很大,跟抖音实现的效果差距很大。这也迫使我寻找另一种解决方案,结果就是透视变换版本,这个版本代码简单而且效果更接近抖音。

透视变换

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

从公式中可以看到变换后做了第3个维度z。展开为方程组形式:

最后映射回2维空间:

从公式中可以看到,假设将a33设为1,那么会有8个未知数,也就是我们至少需要4个点才能求得方程的接。在python中可以轻松的实现:

 1img = cv2.imread("./1.jpg")
 2src_pts = np.float32(
 3[
 4    [
 5        [0, 0],
 6        [0, 626],
 7        [500, 626],
 8        [500, 0]
 9    ]
10])
11
12dst_pts = np.float32(
13    [
14        [100, 50],
15        [150, 200],
16        [500, 626],
17        [500, 0]
18    ]
19)
20
21M = cv2.getPerspectiveTransform(src_pts, dst_pts)
22dst = cv2.warpPerspective(img, M, (img.shape[0], img.shape[1]))
23cv2.imshow("", dst)
24cv2.waitKey()

上面代码效果如下:

上面的代码是通过getPerspectiveTransform函数找到src的4个点和dst的4个点的变换矩阵,还有一个函数findHomography可以在一堆点中找到最佳的变换矩阵,很明显,第二个函数更符合这个需求的实现,可以直接将人脸识别后的关键点扔给这个函数,然后找到最佳变换矩阵。所以透视变换版本的代码如下:

 1def compose_img(name, frames_per_transformer, wait_frames, *imgs):
 2    video_writer = to_video("{}.avi".format(name), imgs[0].shape[1], imgs[0].shape[0])
 3
 4    img_count = len(imgs)
 5    for idx in range(img_count - 1):
 6        from_img = imgs[idx]
 7        to_img = imgs[idx + 1]
 8
 9        from_width = from_img.shape[1]
10        from_height = from_img.shape[0]
11
12        to_width = to_img.shape[1]
13        to_height = to_img.shape[0]
14
15        equation_pow = 1 if idx % 2 == 0 else 2
16
17        from_face_region, from_landmarks = face_detector(from_img)
18        to_face_region, to_landmarks = face_detector(to_img)
19
20        homography_equation = get_equation(0, from_landmarks, frames_per_transformer - 1, to_landmarks, equation_pow)
21
22        for i in range(frames_per_transformer):
23            from_H, _ = cv2.findHomography(from_landmarks, homography_equation(i))
24            to_H, _ = cv2.findHomography(to_landmarks, homography_equation(i))
25
26            from_dst = cv2.warpPerspective(from_img, from_H, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)
27            to_dst = cv2.warpPerspective(to_img, to_H, (to_width, to_height), borderMode=cv2.BORDER_REPLICATE)
28
29            new_img = cv2.addWeighted(from_dst, 1 - ((i + 1) / frames_per_transformer), to_dst, (i + 1) / frames_per_transformer, 0)
30            video_writer.write(new_img)
31
32        for _ in range(wait_frames):
33            video_writer.write(to_img)
34
35    video_writer.release()

可以看到代码简化了不少,也仅用了一次变换就完成了。如上面所说,我们使用findHomography函数,在68个关键点中寻找最佳变换矩阵,然后利用warpPerspective函数进行变换,效果可以看下面的gif(忽略gif的颜色问题,视频正常!完整视频可以到http://www.iqiyi.com/w_19saz1z92h.html查看)

可以看到这次的效果完全有了立体感,而且人脸的对齐也比第一个版本好的多,跟抖音的差距也缩小了不少。

最终所有代码都可以再我的github下载:

https://github.com/qibin0506/change_face

技术的道路一个人走着极为艰难?

一身的本领得不施展?

优质的文章得不到曝光?

别担心,

即刻起,CSDN 将为你带来创新创造创变展现的大舞台,

扫描下方二维码,欢迎加入 CSDN 「原力计划」!

(*本文为AI科技大本营转载文章,转载请联系作者)

精彩推荐

推荐阅读

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值