Switching Eds: Face swapping with Python, dlib, and OpenCV

Switching Eds: Face swapping with Python, dlib, and OpenCV

resost by http://matthewearl.github.io/2015/07/28/switching-eds-with-python/

Introduction

In this post I’ll describe how I wrote a short (200 line) Python script to automatically replace facial features on an image of a face, with the facial features from a second image of a face.

The process breaks down into four steps:

Detecting facial landmarks.
Rotating, scaling, and translating the second image to fit over the first.
Adjusting the colour balance in the second image to match that of the first.
Blending features from the second image on top of the first.
The full source-code for the script can be found here.

  1. Using dlib to extract facial landmarks
    The script uses dlib’s Python bindings to extract facial landmarks:

Dlib implements the algorithm described in the paper One Millisecond Face Alignment with an Ensemble of Regression Trees, by Vahid Kazemi and Josephine Sullivan. The algorithm itself is very complex, but dlib’s interface for using it is incredibly simple:

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()])
        ```
        
        
        The function get_landmarks() takes an image in the form of a numpy array, and returns a 68x2 element matrix, each row of which corresponding with the x, y coordinates of a particular feature point in the input image.

The feature extractor (predictor) requires a rough bounding box as input to the algorithm. This is provided by a traditional face detector (detector) which returns a list of rectangles, each of which corresponding with a face in the image.

To make the predictor a pre-trained model is required. Such a model can be downloaded from the dlib sourceforge repository.

##2. Aligning faces with a procrustes analysis

So at this point we have our two landmark matrices, each row having coordinates to a particular facial feature (eg. the 30th row gives the coordinates of the tip of the nose). We’re now going to work out how to rotate, translate, and scale the points of the first vector such that they fit as closely as possible to the points in the second vector, the idea being that the same transformation can be used to overlay the second image over the first.

To put it more mathematically, we seek TT, ss, and RR such that:


is minimized, where RR is an orthogonal 2x2 matrix, ss is a scalar, TT is a 2-vector, and pipi and qiqi are the rows of the landmark matrices calculated above.

It turns out that this sort of problem can be solved with an Ordinary Procrustes Analysis:

```python
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.])])

Stepping through the code:

Convert the input matrices into floats. This is required for the operations that are to follow.
Subtract the centroid form each of the point sets. Once an optimal scaling and rotation has been found for the resulting point sets, the centroids c1 and c2 can be used to find the full solution.
Similarly, divide each point set by its standard deviation. This removes the scaling component of the problem.
Calculate the rotation portion using the Singular Value Decomposition. See the wikipedia article on the Orthogonal Procrustes Problem for details of how this works.
Return the complete transformaton as an affine transformation matrix.
The result can then be plugged into OpenCV’s cv2.warpAffine function to map the second image onto the first:

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

Which produces the following alignment:


3. Colour correcting the second image

If we tried to overlay facial features at this point, we’d soon see we have a problem:


The issue is that differences in skin-tone and lighting between the two images is causing a discontinuity around the edges of the overlaid region. Let’s try to correct that:

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

This function attempts to change the colouring of im2 to match that of im1. It does this by dividing im2 by a gaussian blur of im2, and then multiplying by a gaussian blur of im1. The idea here is that of a RGB scaling colour-correction, but instead of a constant scale factor across all of the image, each pixel has its own localised scale factor.

With this approach differences in lighting between the two images can be accounted for, to some degree. For example, if image 1 is lit from one side but image 2 has uniform lighting then the colour corrected image 2 will appear darker on the unlit side aswell.

That said, this is a fairly crude solution to the problem and an appropriate size gaussian kernel is key. Too small and facial features from the first image will show up in the second. Too large and kernel strays outside of the face area for pixels being overlaid, and discolouration occurs. Here a kernel of 0.6 * the pupillary distance is used.

4. Blending features from the second image onto the first

A mask is used to select which parts of image 2 and which parts of image 1 should be shown in the final image:

Regions with value 1 (shown white here) correspond with areas where image 2 should show, and regions with colour 0 (shown black here) correspond with areas where image 1 should show. Value in between 0 and 1 correspond with a mixture of image 1 and image2.

Here’s the code to generate the above:

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)
                                                    
        

Let’s break this down:
A routine get_face_mask()
is defined to generate a mask for an image and a landmark matrix. It draws two convex polygons in white: One surrounding the eye area, and one surrounding the nose and mouth area. It then feathers the edge of the mask outwards by 11 pixels. The feathering helps hide any remaning discontinuities.
Such a face mask is generated for both images. The mask for the second is transformed into image 1’s coordinate space, using the same transformation as in step 2.
The masks are then combined into one by taking an element-wise maximum. Combining both masks ensures that the features from image 1 are covered up, and that the features from image 2 show through.

Finally, the mask is applied to give the final image:

Paste_Image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的阅读 13,306评论 5 6
  • 《每一天》 作者:王保帅 生命在每一天跳动, 生活在每一天刷新, 日出日落在每一天周转, 岁月穿梭每一天分秒,...
    仁厚道阅读 258评论 1 3
  • 这几天又有点犯懒惰了!昨天睡得晚,今天特别困! 今天已经过去三分之二了,九月份了,金九银十好日子!我始终相信,美好...
    成长路上的幸运儿阅读 106评论 0 1
  • 偶然的机会看到了这个软件app,习惯性的下载,开始是不怎么抱希望的毕竟好多app应用起来并不是太好,然而这款目前为...
    浅浅遇阅读 158评论 0 0