在PhotoShop中,我们经常利用液化工具的向前工具来对人脸进行形变处理,例如瘦脸、放大眼睛等常规P图操作。
瘦脸与眼睛放大可以算作图像局部扭曲算法的一个应用,其参考文献可以追溯至1993年的一篇博士论文:Interactive Image Warping。这篇论文详细描述了算法原理,并提供了伪码实现。
图像局部扭曲算法有三个:局部缩放(Local Scaling)算法、局部平移(Local Transition)算法和局部旋转(Local Rotation)算法。其中应用局部缩放算法可实现眼睛放大,局部平移算法则可用于实现瘦脸效果。下面来应用图像局部平移算法让一张图片“笑”起来,其背后原理和瘦脸原理如出一辙。
实现效果
具体实现步骤
利用dlib库的人脸关键点检测器检测出人脸的68个关键点,其68个特征点在人脸中的位置和在关键点序列中的次序如下
划定一个圆形选区,一般是要让哪里的局部图像变形就在哪里划定圆形选取,因为这里要让人脸“笑”起来,所以分别在关键点49、60(左嘴角)和55、56(右嘴角)之间划定圆形选区
对于圆形选区里的每一像素,取出其R,G,B各分量,存入3个变量(r, g, b)中(也即,三个变量分别存储选区内的原图像的R,G,B三个通道的数值)
对于圆形选区里的每一个像素U
根据图像局部平移算法公式的映射关系,算出它变形后的位置坐标X(实际运算过程采取逆运算,即由变形后坐标X推出变形前坐标U)
代码实现(代码已附上详细注释)
import dlib
import cv2
import numpy as np
import math
import matplotlib.pyplot as plt
class FaceAdjust():
def __init__(self):
self.predictor_path = "dat/shape_predictor_68_face_landmarks.dat"
#获取人脸框检测器
self.detector = dlib.get_frontal_face_detector()
#获取人脸关键点检测器
self.predictor = dlib.shape_predictor(self.predictor_path)
def landmark_dec_dlib_fun(self,img_src):
img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
#用于绘制人脸关键点的img
img_tag = img_src.copy()
land_marks = []
rects = self.detector(img_gray, 0)
for i in range(len(rects)):
#把人脸关键点列表转为矩阵保存至列表中
land_marks_node = np.matrix([[p.x, p.y] for p in self.predictor(img_gray, rects[i]).parts()])
land_marks.append(land_marks_node)
#在图中标记出人脸关键点
for j in land_marks_node:
pt_pos = (j[0,0],j[0,1])
cv2.circle(img_tag, pt_pos, 2, (0, 255, 0), -1)
cv2.imwrite(r'tag.jpg', img_tag)
return land_marks
'''
localTranslationWarp:Interactive Image Warping 局部平移算法(移动圆内的像素)
具体实现步骤
2.1 对于圆形选区里的每一像素,取出其R,G,B各分量,存入3个变量(r, g, b)中(也即,三个变量分别存储选区内的原图像的R,G,B三个通道的数值)
2.2 对于圆形选区里的每一个像素U
2.3 根据图像局部平移变形,算出它变形后的位置坐标精确值X
2.4 用插值方法,根据U的位置,和r, g, b中的数值,计算U所在位置处的R,G,B等分量,将R,G,B等分量合成新的像素,作为X处的像素值(bilinearInsert方法)
'''
def localTranslationWarp(self,srcImg, startX, startY, endX, endY, radius):
ddradius = float(radius * radius)
copyImg = np.zeros(srcImg.shape, np.uint8)
copyImg = srcImg.copy()
# 计算公式中的|m-c|^2
ddmc = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)
H, W, C = srcImg.shape
for i in range(W):
for j in range(H):
# 计算该点是否在形变圆的范围之内
# 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
continue
distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
if (distance < ddradius):
# 计算出(i,j)坐标的原坐标
# 计算公式中右边平方号里的部分
ratio = (ddradius - distance) / (ddradius - distance + ddmc)
ratio = ratio * ratio
# 映射原位置(向后变形,j后+变-即为向前变形)
UX = i + ratio * (endX - startX)
UY = j + ratio * (endY - startY)
# 根据双线性插值法得到UX,UY的值
value = self.bilinearInsert(srcImg, UX, UY)
# 改变当前 i ,j的值
copyImg[j, i] = value
return copyImg
"""
bilinearInsert:双线性插值法(变换像素)
"""
def bilinearInsert(self,src, ux, uy):
w, h, c = src.shape
if c == 3:
x1 = int(ux)
x2 = x1 + 1
y1 = int(uy)
y2 = y1 + 1
part1 = src[y1, x1].astype(np.float) * (float(x2) - ux) * (float(y2) - uy)
part2 = src[y1, x2].astype(np.float) * (ux - float(x1)) * (float(y2) - uy)
part3 = src[y2, x1].astype(np.float) * (float(x2) - ux) * (uy - float(y1))
part4 = src[y2, x2].astype(np.float) * (ux - float(x1)) * (uy - float(y1))
insertValue = part1 + part2 + part3 + part4
return insertValue.astype(np.int8)
def face_adjust_auto(self,src):
#1.获取人脸关键点
landmarks = self.landmark_dec_dlib_fun(src)
# 如果未检测到人脸关键点,就不进行脸部微调
if len(landmarks) == 0:
return
#2.对嘴巴关键点(左嘴角和右嘴角)进行局部平移算法(微笑)
thin_image = src
landmarks_node = landmarks[0]
start_landmark1 = landmarks_node[48]
end_landmark1 = landmarks_node[59]
start_landmark2 = landmarks_node[54]
end_landmark2 = landmarks_node[55]
r = math.sqrt((start_landmark1[0, 0] - end_landmark1[0, 0]) * (start_landmark1[0, 0] - end_landmark1[0, 0]) +
(start_landmark1[0, 1] - end_landmark1[0, 1]) * (start_landmark1[0, 1] - end_landmark1[0, 1]))
thin_image = self.localTranslationWarp(thin_image, start_landmark1[0, 0], start_landmark1[0, 1], end_landmark1[0,0], end_landmark1[0, 1], r)
thin_image = self.localTranslationWarp(thin_image, start_landmark2[0, 0], start_landmark2[0, 1], end_landmark2[0,0], end_landmark2[0, 1], r)
# 显示
# cv2.imshow('thin', thin_image)
cv2.imwrite(r'thin.jpg', thin_image)
if __name__ == '__main__':
src = cv2.imread(r'face_adjust.jpg')
face_adjust_control = FaceAdjust()
face_adjust_control.face_adjust_auto(src)