Skip to content

Commit 5bbe49a

Browse files
committed
Initial commit of faceswap.py
0 parents  commit 5bbe49a

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

faceswap.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/python
2+
3+
# Copyright (c) 2015 Matthew Earl
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included
13+
# in all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
18+
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19+
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
21+
# USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
23+
import cv2
24+
import dlib
25+
import numpy
26+
27+
import sys
28+
29+
PREDICTOR_PATH = "/home/matt/dlib-18.16/shape_predictor_68_face_landmarks.dat"
30+
SCALE_FACTOR = 1
31+
FACE_POINTS = list(range(17, 68))
32+
MOUTH_POINTS = list(range(48, 61))
33+
RIGHT_BROW_POINTS = list(range(17, 22))
34+
LEFT_BROW_POINTS = list(range(22, 27))
35+
RIGHT_EYE_POINTS = list(range(36, 42))
36+
LEFT_EYE_POINTS = list(range(42, 48))
37+
DILATE_AMOUNT = 1
38+
39+
detector = dlib.get_frontal_face_detector()
40+
predictor = dlib.shape_predictor(PREDICTOR_PATH)
41+
42+
class TooManyFaces(Exception):
43+
pass
44+
45+
class NoFaces(Exception):
46+
pass
47+
48+
def get_landmarks(im):
49+
dets = detector(im, 1)
50+
51+
if len(dets) > 1:
52+
raise TooManyFaces
53+
if len(dets) == 0:
54+
raise NoFaces
55+
56+
def shape_to_mat(s):
57+
return numpy.matrix([[p.x, p.y] for p in s.parts()])
58+
59+
return shape_to_mat(predictor(im, dets[0]))
60+
61+
def annotate_landmarks(im, shape):
62+
im = im.copy()
63+
for idx, point in enumerate(shape):
64+
pos = (point[0, 0], point[0, 1])
65+
cv2.putText(im, str(idx), pos,
66+
fontFace=cv2.FONT_HERSHEY_PLAIN,
67+
fontScale=1,
68+
color=255)
69+
cv2.circle(im, pos, 5, color=255)
70+
return im
71+
72+
def get_face_mask(im, shape):
73+
im = numpy.zeros(im.shape[:2], dtype=numpy.float64)
74+
75+
points = shape[FACE_POINTS]
76+
points = cv2.convexHull(points)
77+
78+
cv2.fillConvexPoly(im, points, color=1)
79+
80+
im = numpy.array([im, im, im]).transpose((1, 2, 0))
81+
82+
im = (cv2.GaussianBlur(im, (DILATE_AMOUNT, DILATE_AMOUNT), 0) > 0) * 1.0
83+
im = cv2.GaussianBlur(im, (DILATE_AMOUNT, DILATE_AMOUNT), 0)
84+
85+
return im
86+
87+
def transformation_from_points(points1, points2):
88+
"""
89+
Return an affine transformation [s * R | T] such that:
90+
91+
sum ||s*R*p1,i + T - p2,i||^2
92+
93+
is minimized.
94+
95+
"""
96+
# Solve the procrustes problem by subtracting centroids, scaling by the
97+
# standard deviation, and then using the SVD to calculate the rotation. See
98+
# the following for more details:
99+
# https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem
100+
101+
points1 = points1.astype(numpy.float64)
102+
points2 = points2.astype(numpy.float64)
103+
104+
c1 = numpy.mean(points1, axis=0)
105+
c2 = numpy.mean(points2, axis=0)
106+
points1 -= c1
107+
points2 -= c2
108+
109+
s1 = numpy.std(points1)
110+
s2 = numpy.std(points2)
111+
points1 /= s1
112+
points2 /= s2
113+
114+
U, S, Vt = numpy.linalg.svd(points1.T * points2)
115+
116+
# The R we seek is in fact the transpose of the one given by U * Vt. This
117+
# is because the above formulation assumes the matrix goes on the right
118+
# (with row vectors) where as our solution requires the matrix to be on the
119+
# left (with column vectors).
120+
R = (U * Vt).T
121+
122+
return numpy.vstack([numpy.hstack(((s2 / s1) * R,
123+
c2.T - (s2 / s1) * R * c1.T)),
124+
numpy.matrix([0., 0., 1.])])
125+
126+
def read_im_and_landmarks(fname):
127+
im = cv2.imread(fname, cv2.IMREAD_COLOR)
128+
im = cv2.resize(im, (im.shape[1] * SCALE_FACTOR,
129+
im.shape[0] * SCALE_FACTOR))
130+
s = get_landmarks(im)
131+
132+
return im, s
133+
134+
def warp_im(im, M, dshape):
135+
output_im = numpy.zeros(dshape, dtype=im.dtype)
136+
cv2.warpAffine(im,
137+
M[:2],
138+
(dshape[1], dshape[0]),
139+
dst=output_im,
140+
borderMode=cv2.BORDER_TRANSPARENT,
141+
flags=cv2.WARP_INVERSE_MAP)
142+
return output_im
143+
144+
def correct_colours(im1, im2, mask):
145+
im1_blur = cv2.GaussianBlur(im1, (61, 61), 0)
146+
im2_blur = cv2.GaussianBlur(im2, (61, 61), 0)
147+
148+
return (im2.astype(numpy.float64) * im1_blur.astype(numpy.float64) /
149+
im2_blur.astype(numpy.float64))
150+
151+
#factor = (im1 * mask).mean(axis=(0, 1)) / (im2 * mask).mean(axis=(0, 1))
152+
#return im2 * factor
153+
154+
im1, shape1 = read_im_and_landmarks(sys.argv[1])
155+
im2, shape2 = read_im_and_landmarks(sys.argv[2])
156+
157+
#im1 = annotate_landmarks(im1, shape1)
158+
#im2 = annotate_landmarks(im2, shape2)
159+
160+
M = transformation_from_points(shape1[FACE_POINTS],
161+
shape2[FACE_POINTS])
162+
163+
mask = get_face_mask(im2, shape2)
164+
warped_mask = warp_im(mask, M, im1.shape)
165+
combined_mask = numpy.max([get_face_mask(im1, shape1), warped_mask], axis=0)
166+
167+
warped_im2 = warp_im(im2, M, im1.shape)
168+
warped_corrected_im2 = correct_colours(im1, warped_im2, combined_mask)
169+
170+
output_im = (im1.astype(numpy.float64) * (1.0 - combined_mask) +
171+
warped_corrected_im2.astype(numpy.float64) * combined_mask)
172+
173+
cv2.imwrite('output.jpg', output_im)
174+

0 commit comments

Comments
 (0)