社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

萌新如何用Python实现人脸替换?

景略集智 • 7 年前 • 1091 次点击  

萌新如何用Python实现人脸替换?

参考资料:matthewearl.github.io/2015/07/28/…

翻译:小马哥

编辑:船长


还记得吗?去年冬天,在国外 AI 圈有个事情闹得很火:知名论坛 Reddit 上忽然出现一个叫 deepfakes 的大神,借助神经网络实现了人脸替换,让一些好莱坞女星“出演”了 AV。

谁不喜欢这个技术呢?

后来根据这个项目又衍生了一个叫 FakeAPP 的桌面应用,可以让尼古拉斯·凯奇这样的明星随心所欲的“出演”任何电影,当然换成任何人的脸部都可以。我们曾详细分享过这些项目:

大意了!居然有人用AI技术制作假AV!

AI已经决定了,ta就是未来每一届奥斯卡最佳男主。

怎么样,是不是被这种换脸的效果惊到了?其实即便是不借助神经网络,我们用 Python 和一些 Python 库也能实现换脸,只不过替换的是静态图像中的人脸,但凭此也足以显示出 Python 的“神秘力量”。


我们下面就传授一下这门 Python “换脸”大法。

在本文,我们会介绍如何通过一段简短的 Python 脚本(200行左右)将一张图片中面部特征自动替换为另外一张图片中的面部特征。也就是实现下面这样的效果:

具体过程分为四个步骤:

  • 检测面部标志;
  • 旋转、缩放和平移图 2 以适应图 1;
  • 调整图 2 的白平衡以匹配图 1;
  • 将图 2 的特征融合到图 1 中;

本脚本的完整代码地址见文末。

使用dlib提取面部标志

本脚本使用 dlib 的 Python bindings 来提取面部标志:

dlib 实现了 Vahid Kazemi 和 Josephine Sullivan 所著论文《One Millisecond Face Alignment with an Ensemble of Regression Tree》一文中描述的算法。算法本身非常复杂,但是通过 dlib 的接口实现它非常简单:

PREDICTOR_PATH = "/home/matt/dlib-18.16/shape_predictor_68_face_landmarks.dat"

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(PREDICTOR_PATH)

def get_landmarks(im):
   rects = detector(im, 1)

   if len(rects) > 1:
       raise TooManyFaces
   if len(rects) == 0:
       raise NoFaces

return numpy.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])

get_landmarks() 函数 以 numpy 数组的形式接收图像,并返回一个 68x2 的元素矩阵。矩阵的每一行与输入图像中特定特征点的 x,y 坐标相对应。

特征提取器(predictor)需要一个大概的边界框作为算法的输入。这将由传统的面部检测器(detector)提供。该面部检测器会返回一个矩形列表,其中每一个矩形与图像中的一张人脸相对应。

生成 predictor 需要预先训练好的模型。该模型可在 dlib sourceforge repository 下载。

下载传送门

用普氏分析法(Procrustes Analysis)实现人脸对齐

现在我们已经有两个面部标志矩阵,其中的每一行都含有某个面部特征的坐标(如第 30 行给出了鼻尖的坐标)。我们现在只要弄明白如何旋转、平移和缩放第一个向量的所有点,使其尽可能匹配第二个向量中的点。同理,同样的变换可用于将第二张图叠加在第一张图上。

为使其更加数学化,我们设 T,s 和 R,并求如下等式最小值:

其中,R 是一个 2x2 的正交矩阵,s 是一个标量,T 是一个二维向量,pi 和 qi 是之前计算出的面部标志矩阵行标和列标。

事实证明,这类问题用常规普氏分析法(Ordinary Procrustes Analysis)可以解决:

def transformation_from_points(points1, points2):
   points1 = points1.astype(numpy.float64)
   points2 = points2.astype(numpy.float64)

   c1 = numpy.mean(points1, axis=0)
   c2 = numpy.mean(points2, axis=0)
   points1 -= c1
   points2 -= c2

   s1 = numpy.std(points1)
   s2 = numpy.std(points2)
   points1 /= s1
   points2 /= s2

   U, S, Vt = numpy.linalg.svd(points1.T * points2)
   R = (U * Vt).T

   return numpy.vstack([numpy.hstack(((s2 / s1) * R,
                                      c2.T - (s2 / s1) * R * c1.T)),
                        numpy.matrix([0., 0., 1.])])

我们逐步分析一下代码:

1.将输入矩阵转换为浮点型。这也是后续步骤的必要条件。

2.将每一个点集减去它的矩心。一旦为这两个新的点集找到了一个最佳的缩放和旋转方法,这两个矩心c1和c2就可以用来找到完整的解决方案。

3.同样,将每一个点集除以它的标准偏差。这消除了缩放偏差。

4.使用奇异值分解(singular value decomposition)计算旋转部分。请参阅维基百科有关Orthogonal Procrustes Problem的文章,以了解它的具体工作原理。

5.将整个变换过程以仿射变换矩阵形式返回。

之后,返回结果可以插入 OpenCV 的 cv2.warpAffine 函数,将第二个图片映射到第一个图片上:




    
def warp_im(im, M, dshape):
   output_im = numpy.zeros(dshape, dtype=im.dtype)
   cv2.warpAffine(im,
                  M[:2],
                  (dshape[1], dshape[0]),
                  dst=output_im,
                  borderMode=cv2.BORDER_TRANSPARENT,
                  flags=cv2.WARP_INVERSE_MAP)
return output_im

校正第二张图片的颜色

如果此时我们试图直接叠加面部特征,很快会发现一个问题:

这样肯定是没法儿看的

这样肯定是没法儿看的

两幅图像之间不同的肤色光线造成了覆盖区域边缘的不连续。所以我们尝试修正它:

COLOUR_CORRECT_BLUR_FRAC = 0.6
LEFT_EYE_POINTS = list(range(42, 48))
RIGHT_EYE_POINTS = list(range(36, 42))

def correct_colours(im1, im2, landmarks1):
   blur_amount = COLOUR_CORRECT_BLUR_FRAC * numpy.linalg.norm(
                             numpy.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -
                             numpy.mean(landmarks1[RIGHT_EYE_POINTS], axis=0))
   blur_amount = int(blur_amount)
   if blur_amount % 2 == 0:
       blur_amount += 1
   im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)
   im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)

   # Avoid divide-by-zero errors.
   im2_blur += 128 * (im2_blur <= 1.0)

   return (im2.astype(numpy.float64) * im1_blur.astype(numpy.float64) /
                                               im2_blur.astype(numpy.float64))

现在效果怎么样?我们瞅瞅:

这不是更奇怪了吗...

enter_image_description_here

此函数试图改变图 2 的颜色来匹配图 1,也就是用 im2 除以 im2 的高斯模糊,然后乘以 im1 的高斯模糊。在这里我们使用了颜色平衡( RGB scaling colour-correction),但不是直接使用全图的常数比例因子,而是采用每个像素的局部比例因子。

通过这种方法也只能在某种程度上修正两图间的光线差异。比如说,如果图 1 的光线来自某一边,但图 2 的光线非常均匀,校色后图 2 也会出现有一边暗一些的情况。

也就是说,这是一个相当粗糙的解决方案,而且关键在于大小适当的高斯内核。如果太小,图 2 中会出现图 1 的面部特征。如果太大,内核会跑到被像素覆盖的面部区域之外,并变色。这里的内核大小为瞳距的 0.6 倍。

将图 2 的特征融合到图 1 中

用一个蒙版(mask)来选择图 2 和图 1 应被最终显示的部分:

值为 1 (白色)的地方为图 2 应显示的区域,值为 0 (黑色)的地方为图 1 应显示的区域。值在 0 和 1 之间的地方为图 1 图 2 的混合区域。

这是生成上述内容的代码:

LEFT_EYE_POINTS = list(range(42, 48))
RIGHT_EYE_POINTS = list(range(36, 42))
LEFT_BROW_POINTS = list(range(22, 27))
RIGHT_BROW_POINTS = list(range(17, 22))
NOSE_POINTS = list(range(27, 35))
MOUTH_POINTS = list(range(48, 61))
OVERLAY_POINTS = [
   LEFT_EYE_POINTS + RIGHT_EYE_POINTS + LEFT_BROW_POINTS + RIGHT_BROW_POINTS,
   NOSE_POINTS + MOUTH_POINTS,
]
FEATHER_AMOUNT = 11

def draw_convex_hull(im, points, color):
   points = cv2.convexHull(points)
   cv2.fillConvexPoly(im, points, color=color)

def get_face_mask(im, landmarks):
   im = numpy.zeros(im.shape[:2], dtype=numpy.float64)

   for group in OVERLAY_POINTS:
       draw_convex_hull(im,
                        landmarks[group],
                        color=1)

   im = numpy.array([im, im, im]).transpose((1, 2, 0))

   im = (cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0) > 0) * 1.0
   im = cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0)

   return im

mask = get_face_mask(im2, landmarks2)
warped_mask = warp_im(mask, M, im1.shape)
combined_mask = numpy.max([get_face_mask(im1, landmarks1), warped_mask],
                         axis=0)

我们来分析一下:

  • 常规的 get_face_mask() 函数定义是:为一张图像和一个标志矩阵生成一个蒙版。蒙版会画出两个白色的凸多边形:一个是眼睛周围的区域,一个是鼻子和嘴部周围的区域。之后,蒙版的边缘区域向外羽化 11 个像素,这可以帮助消除剩下的不连续部分。
  • 为图 1 图 2 生成面部蒙版。使用与步骤 2 中的转换,可以使图 2 的蒙版转换至图 1 的坐标空间。
  • 之后,对所有元素取最大值操作,将这两个蒙版合二为一。这样做是为了保证图 1 的特征也能被覆盖的同时图 2 特征能显示出来。

最后,将蒙版应用于最终图像:

output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask

哈!换脸成功!

哈,换脸成功!

附:本项目代码地址:Github


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/LwKYZbkUxH
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/10613
 
1091 次点击