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