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

人脸关键点识别

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

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

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

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

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

版本一:仿射变换实现

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

  1. 平移
  2. 缩放
  3. 旋转

平移

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

[ 1 0 t x 0 1 t y ] \left[ \begin{matrix} 1 & 0 & tx \\ 0 & 1 & ty \end{matrix} \right] [10​01​txty​]

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

[ 1 0 100 0 1 50 ] \left[ \begin{matrix} 1 & 0 & 100 \\ 0 & 1 & 50 \end{matrix} \right] [10​01​10050​]

对应的运算是:

[ x ′ y ′ ] = [ 1 0 100 0 1 50 ] ∗ [ x y 1 ] \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′​]=[10​01​10050​]∗⎣⎡​xy1​⎦⎤​


{ x ′ = 1 ∗ x + 0 ∗ y + 100 ∗ 1 y ′ = 0 ∗ x + 1 ∗ y + 50 ∗ 1 \begin{cases} x'=1*x+0*y+100*1 \\ y'=0*x+1*y+50*1 \end{cases} {x′=1∗x+0∗y+100∗1y′=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()

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

缩放

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

[ f x 0 0 0 f y 0 ] \left[ \begin{matrix} fx & 0 & 0 \\ 0 & fy & 0 \end{matrix} \right] [fx0​0fy​00​]

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()

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

旋转

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

[ c o s ( θ ) − s i n ( θ ) 0 s i n ( θ ) c o s ( θ ) 0 0 0 1 ] \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​001​⎦⎤​

如果我们想要旋转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()

运行效果如下:

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

getRotationMatrix2D(center, angle, scale)

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()

最强变脸术第一次实现

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

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

参数1:生成视频的文件名

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

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

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

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

def to_video(name, width, height):fps = 10video_writer = cv2.VideoWriter(name, cv2.VideoWriter_fourcc('I', '4', '2', '0'), fps, (width, height))return video_writerdef 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) + breturn fdef 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

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_centerfrom_translation_base = from_rotate_centerequation_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()

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

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

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

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

透视变换

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

[ x ′ y ′ z ] = [ a 11 a 12 a 13 a 21 a 22 a 23 a 31 a 32 a 33 ] ∗ [ x y 1 ] \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​⎦⎤​=⎣⎡​a11​a21​a31​​a12​a22​a32​​a13​a23​a33​​⎦⎤​∗⎣⎡​xy1​⎦⎤​
从公式中可以看到变换后做了第3个维度z。展开为方程组形式:
{ 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 \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′=a11​∗x+a12​∗y+a13​y′=a21​∗x+a22​∗y+a23​z=a31​∗x+a32​∗y+a33​​
最后映射回2维空间:
{ x ′ = x ′ z = a 11 ∗ x + a 12 ∗ y + a 13 a 31 ∗ x + a 32 ∗ y + a 33 y ′ = y ′ z = a 21 ∗ x + a 22 ∗ y + a 23 a 31 ∗ x + a 32 ∗ y + a 33 \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′=zx′​=a31​∗x+a32​∗y+a33​a11​∗x+a12​∗y+a13​​y′=zy′​=a31​∗x+a32​∗y+a33​a21​∗x+a22​∗y+a23​​​

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

img = cv2.imread("./1.jpg")
src_pts = np.float32(
[[[0, 0],[0, 626],[500, 626],[500, 0]]
])dst_pts = np.float32([[100, 50],[150, 200],[500, 626],[500, 0]]
)M = cv2.getPerspectiveTransform(src_pts, dst_pts)
dst = cv2.warpPerspective(img, M, (img.shape[0], img.shape[1]))
cv2.imshow("", dst)
cv2.waitKey()

上面代码效果如下:

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

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]equation_pow = 1 if idx % 2 == 0 else 2from_face_region, from_landmarks = face_detector(from_img)to_face_region, to_landmarks = face_detector(to_img)homography_equation = get_equation(0, from_landmarks, frames_per_transformer - 1, to_landmarks, equation_pow)for i in range(frames_per_transformer):from_H, _ = cv2.findHomography(from_landmarks, homography_equation(i))to_H, _ = cv2.findHomography(to_landmarks, homography_equation(i))from_dst = cv2.warpPerspective(from_img, from_H, (from_width, from_height), borderMode=cv2.BORDER_REPLICATE)to_dst = cv2.warpPerspective(to_img, to_H, (to_width, to_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)for _ in range(wait_frames):video_writer.write(to_img)video_writer.release()

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


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

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

利用opencv实现抖音最强变脸术相关推荐

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

    作者 | 亓斌 来源 | CSDN原力计划获奖作品 (*点击阅读原文,查看作者更多文章) 最近一个"最强变脸术"又火爆抖音啦,还不知道的朋友建议先打开抖音,搜索"最强变脸 ...

  2. 动态给a标签赋值_怎样利用Excel制作抖音上的心形动态函数图像?

    最近在抖音上看到有用Excel制作心形动态函数图像,感觉很新奇,闲来无事,准备自己动手做做,遂网上搜了教程,按照教程一步步做,前面都很顺利,但到最后一部确卡壳,问了公司Excel大牛也未找到原因,知道 ...

  3. 使用python+OpenCV实现抖音特效“蓝线挑战”

    使用OpenCV实现抖音"蓝线挑战"特效.原理比较简单,当蓝线在视频画面中滑动,然后从滑过的每一帧中截取部分画面生成一幅静态图片,由于上一帧画面已经定格,那么通过调整下一帧画面可以 ...

  4. 会声会影x4素材_如何利用会声会影制作抖音短视频

    短视频的越发火爆,当我们看到抖音里面那些触动心弦的画面,那些震撼人心的画面,那些精彩纷繁的剪辑视频时候,是不是我们的内心也在呼唤,我该怎么制作抖音视频呢?下面小编就教大家如何利用会声会影制作抖音短视频 ...

  5. 利用JS制作抖音同款3D照片墙(three.js)

    利用JS制作抖音同款3D照片墙(three.js) 520快到了,跟我一起学习threeJS 用threeJS制作抖音同款3D照片墙 源码下载:3D照片墙源码下载地址

  6. html5弹球打砖块代码,利用JS实现抖音弹球打砖块游戏代码

    特效描述:利用JS实现 抖音弹球 打砖块 游戏代码.利用JS实现抖音弹球打砖块游戏代码 代码结构 1. HTML代码 0分 总分:74 确定 /* javascript中严格区分大小写 a!==A; ...

  7. 抖音引流话术怎么写?这些引流话术你必须掌握

    抖音引流话术怎么写?这些引流话术你必须掌握 在这个以流量为红利的互联网时代,越来越多的人抓住这个营销风口利用抖音平台来吸引流量赚钱.那么我们应该如何在抖音上正确的话术引流而不被限流封号呢?被降权限流后 ...

  8. obs多推流地址_(无人直播)教程利用OBS推流抖音直播电脑屏幕或PC游戏

    目前的火爆程度相信大家都有目共睹,也為部分活躍用戶提供直播功能,開通直播的方法有:①粉絲達到10000粉絲,官方會自動邀請妳開通直播權限.②加入和官方合作的工會,無需粉絲,也可以開通直播權限.開通直播 ...

  9. 抖音最强python_装逼篇 | 抖音超火的九宫格视频是如何生成的,Python 告诉你答案...

    1. 场景 如果你经常刷抖音和微信朋友圈,一定发现了最近九宫格短视频很火! 从朋友圈九宫格图片,到九宫格视频,相比传统的图片视频,前者似乎更有个性和逼格 除了传统的剪辑软件可以实现,是否有其他更加快捷 ...

最新文章

  1. 【Linux原理】Linux中硬链接和软链接的区别和联系
  2. php性能提升5倍的秘诀,停机维护时长缩短5倍,全靠这3个秘诀
  3. ICCV 2021 | DeeperAction挑战赛三大赛道开启报名
  4. java 简单获取Excel表格内容(初学者)
  5. Mac重复文件清理软件—Cisdem Duplicate Finder for mac
  6. tensorflow机器学习实战指南 源代码_小小白TensorFlow机器学习实战基础
  7. java 简单数组_Java 数组分析及简单实例
  8. [USB-Blaster] Error (209040): Can't access JTAG chain
  9. solidword入门使用
  10. 前端 Coder 手中的 Docker
  11. 极化SAR图像四成分分解
  12. python实现12306自助刷票下单
  13. 细谈永恒之蓝,实现复现
  14. 理论计算机科学方向,计算机科学与技术专业考研方向:计算机软件与理论
  15. 谷医堂与日俱进!谷医堂优化产品和提升服务两不误
  16. jQuery —— 元素绑定单击事件(click),但是双击该元素也能触发单击事件,同时会触发两次单击事件的问题
  17. java catch空指针异常处理_Java基础学习:java文件空指针异常处理
  18. insmod depmod modprobe的区别及用法
  19. 序列化Serializable、Parcelable
  20. C++ vector 容器的全排列算法 next_permutation

热门文章

  1. C#设计模式》PPT及源码分享
  2. 国际高级护理实践的发展
  3. 中国剩余定理又叫孙子定理
  4. BSP学习Day11 C语言基础 宏定义和宏函数 函数调用 类型转换 数组
  5. 谷歌关闭Google.cn 通过香港网站提供服务
  6. 使用shareSDK实现微信多图分享到朋友圈Url分享到朋友圈URL分享到好友 问题记录
  7. “21天好习惯“第一期——2
  8. 小程序(四)小程序生命周期
  9. Android系统截屏监听工具
  10. reaver 使用方法