参考 switch face with python
1 使用dlib抽取面部关键点
关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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()])
|
特征抽取器predictor
传入一个矩形的人脸部分,预测内部的人脸的68个关键点坐标,即$68\times 2$个值。
2 使用procrustes分析进行人脸对齐
检测两张人脸的关键点之后,每个点的特定属性我们是知道的,比如第30个点代表的是鼻尖的坐标。我们接下来要做的是,如何扭曲、转换、以及缩放第一个人脸的点,使得它与目标关键点尽可能接近。这个相同的转换步骤可以用,第二个人脸图像来覆盖第一个人脸图像来实现。
数学形式的解法为,我们寻找$T,s,R$最小化下面的等式:
$$
\sum _{i=1} ^{68}|sRp_i ^T+T-q_i ^T|^2
$$
其中$R$是一个$2\times 2$的正交矩阵,$s$是个标量,$T$是一个2向量,$p_ihe q_i$是上面计算得到的68个关键点。此问题等价于求解一个正交procrustes分析问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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.])])
|
以上代码执行了如下步骤
- 将所有输入转换为浮点型,便于后续的计算
- 减去每个点集合的中心坐标(即减去均值)。一旦结果点集合的最优变换和扭曲解找到,中心的
c1
和c2
可以用来求解全局解。
- 类似的,每个点除以标准差。消除尺度影响
- 使用SVD计算扭曲比率,需要去查看正交Procrustes问题的求解过程才能了解。
- 返回完整的转换为放射变换矩阵。
结果可以用Opencv的cv2.wrapAffine
函数来映射第二张图到第一张图。
1 2 3 4 5 6 7 8 9
| 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
|
其实就是为了让两张图的点位能对得上,将某张图进行旋转,缩放,使得两张图的人脸的关键点能处于相同的坐标位置。
3 目标图的轮廓纠正
Non colour-corrected overlay
接下来需要解决的问题是,两张图像的不同肤色和光照差异,会导致连接处边缘的突兀。下面的方法是尝试纠正这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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))
|
此方法尝试改变第二张图像的轮廓去匹配第一张图的。做法是:第二张图除以其高斯模糊,然后乘以第一张图的高斯模糊。此算法源于RGB的缩放轮廓纠正,但是对所有图像使用了一个常量的缩放因子,每个像素有其局部缩放因子。
由此方法,两张图像的光照差异可以在某种程度上累加。例如,如果第一张图某一边在发光二第二张图有均衡的光照,那么轮廓纠正之后图二会出现出现某些暗处。也就是说这是个比较粗暴的方案,合适大小的高斯核是问题的关键。太小的话会导致图一种某些面部特征会出现在图二中,太大的话会导致核外面某些像素重叠,并出现变色。此处使用的是$0.6\times 瞳孔距离$。
4 将图二特征渲染回图一
使用一个mask从图二中抽取部分,渲染到图一中。
- 值为1的区域(上图中的白色区域)对应的是图二中人脸特征选取的部分
- 值为0的区域(上图中黑色区域)对应的是图一该出现的部分。0到1之间的区域是两张图的混合。
下面代码是生成如上区域的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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()
方法是用来定义生成图像的一个mask和一个面部关键点的矩阵。它画了两个白色凸多边形:一个环绕着眼睛区域,一个环绕着鼻子和嘴巴区域。接着它会羽化mask的边缘11个像素。羽化有助于隐藏遗留的颜色不连续问题
此类面部的mask会给两张图都生成。图二的mask会被转换进图一的坐标空间,使用步骤2相同的转换。
此mask接下来会逐像素取最大值。结合两个mask可以保证图一中的特征会被覆盖,同时图二中的特征也会被展示。
最后,mask用于得到最终的合成结果。
1
| output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask
|