参考资料:matthewearl.github.io/2015/07/28/…python
翻译:小马哥git
编辑:船长github
还记得吗?去年冬天,在国外 AI 圈有个事情闹得很火:知名论坛 Reddit 上突然出现一个叫 deepfakes 的大神,借助神经网络实现了人脸替换,让一些好莱坞女星“出演”了 AV。算法
后来根据这个项目又衍生了一个叫 FakeAPP 的桌面应用,可让尼古拉斯·凯奇这样的明星为所欲为的“出演”任何电影,固然换成任何人的脸部均可以。咱们曾详细分享过这些项目:数组
怎么样,是否是被这种换脸的效果惊到了?其实即使是不借助神经网络,咱们用 Python 和一些 Python 库也能实现换脸,只不过替换的是静态图像中的人脸,但凭此也足以显示出 Python 的“神秘力量”。ide
咱们下面就传授一下这门 Python “换脸”大法。函数
在本文,咱们会介绍如何经过一段简短的 Python 脚本(200行左右)将一张图片中面部特征自动替换为另一张图片中的面部特征。也就是实现下面这样的效果:ui
具体过程分为四个步骤:
本脚本的完整代码地址见文末。
本脚本使用 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 下载。
如今咱们已经有两个面部标志矩阵,其中的每一行都含有某个面部特征的坐标(如第 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))
复制代码
如今效果怎么样?咱们瞅瞅:
此函数试图改变图 2 的颜色来匹配图 1,也就是用 im2 除以 im2 的高斯模糊,而后乘以 im1 的高斯模糊。在这里咱们使用了颜色平衡( RGB scaling colour-correction),但不是直接使用全图的常数比例因子,而是采用每一个像素的局部比例因子。
经过这种方法也只能在某种程度上修正两图间的光线差别。好比说,若是图 1 的光线来自某一边,但图 2 的光线很是均匀,校色后图 2 也会出现有一边暗一些的状况。
也就是说,这是一个至关粗糙的解决方案,并且关键在于大小适当的高斯内核。若是过小,图 2 中会出现图 1 的面部特征。若是太大,内核会跑到被像素覆盖的面部区域以外,并变色。这里的内核大小为瞳距的 0.6 倍。
用一个蒙版(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)
复制代码
咱们来分析一下:
最后,将蒙版应用于最终图像:
output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask
复制代码
附:本项目代码地址:Github