混合边缘 AI 面部对齐






4.82/5 (3投票s)
在本文中,我将解释如何根据检测器找到的人脸标志点来执行人脸对齐。
引言
人脸识别是人工智能(AI)的一个领域,在过去十年中,深度学习(DL)取得了巨大的成功。最好的人脸识别系统能够以与人类相同甚至更高的精度识别图像和视频中的人。人脸识别的两个主要基本阶段是身份验证和身份识别。
在本系列文章的第一个(当前)部分,我们将
- 讨论现有的人脸检测AI方法,并开发一个程序来运行预训练的DNN模型
- 考虑人脸对齐,并使用人脸标志点实现一些对齐算法
- 在树莓派设备上运行人脸检测DNN,探索其性能,并考虑可能的加速运行方法,以及实时检测人脸
- 创建一个简单的人脸数据库,并用从图像或视频中提取的人脸填充它
我们假设您熟悉DNN、Python、Keras和TensorFlow。欢迎下载此项目代码...
在上一篇文章中,我们实现了基于现代AI方法(人脸检测)的通用人脸识别系统的第一阶段。在本文中,我们将讨论流水线的第二阶段——人脸对齐——基于OpenCV库的透视变换函数。这不是一个强制性的阶段。然而,它可以提高识别准确率,并且在现实的人脸识别软件中总是会用到。
对齐算法
简而言之,人脸对齐是一种将从图像或视频帧中提取的人脸图像转换为指定方向、位置和尺度的算法。在需要进行人脸识别的真实图像和帧中,人脸可能以各种角度和各种尺度出现。下图显示了同一张脸在相对于摄像机视角的不同位置。
1 - 人脸向右旋转
2 - 人脸侧倾
3 - 人脸向左旋转
4 - 人脸向下倾斜
人脸对齐的目标是将人脸调整到相同大小的图像中,并标准化人脸视图:使人脸居中;相对于图像进行缩放,等等。
人脸对齐可以使用各种算法,这取决于人脸及其位置的可用信息。我们将使用MTCNN模型,因为我们知道人脸的边界框在2D图像中,并且有五个面部标志点(关键点)。我们需要开发一种转换,利用人脸的关键点将其恢复到标准视图。
幸运的是,我们不需要重复造轮子。在计算机视觉中,这种转换被称为透视变换,并且使用OpenCV库实现起来相对容易。我们只需要在源图像中选择四个点,并在目标图像中选择四个匹配的点。
看下图。左侧是我们用MTCNN算法找到的关键点。右侧显示了三个点,这些点可以根据三个关键点(1、2、3)的已知坐标来计算。
点6在点1和点2之间。点7和点8与关键点1和2一起形成一个平行四边形。点1和点7之间的距离与点2和点8之间的距离相等,这是点6和点3之间距离的两倍。所以点3是平行四边形的中心。
算法实现
让我们使用平行四边形的四个角来实现透视变换算法
class Face_Align:
def __init__(self, size):
self.size = size
def align_point(self, point, M):
(x, y) = point
p = np.float32([[[x, y]]])
p = cv2.perspectiveTransform(p, M)
return (int(p[0][0][0]), int(p[0][0][1]))
def align(self, frame, face):
(x1, y1, w, h) = face['box']
(l_eye, r_eye, nose, mouth_l, mouth_r) = Utils.get_keypoints(face)
(pts1, pts2) = self.get_perspective_points(l_eye, r_eye, nose, mouth_l, mouth_r)
s = self.size
M = cv2.getPerspectiveTransform(pts1, pts2)
dst = cv2.warpPerspective(frame, M, (s, s))
f_aligned = copy.deepcopy(face)
f_aligned['box'] = (0, 0, s, s)
f_img = dst
l_eye = self.align_point(l_eye, M)
r_eye = self.align_point(r_eye, M)
nose = self.align_point(nose, M)
mouth_l = self.align_point(mouth_l, M)
mouth_r = self.align_point(mouth_r, M)
f_aligned = Utils.set_keypoints(f_aligned, (l_eye, r_eye, nose, mouth_l, mouth_r))
return (f_aligned, f_img)
class Face_Align_Nose(Face_Align):
def get_perspective_points(self, l_eye, r_eye, nose, mouth_l, mouth_r):
(xl, yl) = l_eye
(xr, yr) = r_eye
(xn, yn) = nose
(xm, ym) = ( 0.5*(xl+xr), 0.5*(yl+yr) )
(dx, dy) = (xn-xm, yn-ym)
(xl2, yl2) = (xl+2.0*dx, yl+2.0*dy)
(xr2, yr2) = (xr+2.0*dx, yr+2.0*dy)
s = self.size
pts1 = np.float32([[xl, yl], [xr, yr], [xr2, yr2], [xl2, yl2]])
pts2 = np.float32([[s*0.25, s*0.25], [s*0.75, s*0.25], [s*0.75, s*0.75], [s*0.25,s*0.75]])
return (pts1, pts2)
上面的代码包含两个类。基类Face_Align
使用OpenCV库的两个函数来实现变换算法:getPerspectiveTransform
和warpPerspective
。第一个函数评估变换矩阵,第二个函数使用矩阵变换图像。用于评估透视矩阵的点必须通过get_perspective_points
方法提供。该方法的具体实现实现在继承类Face_Align_Nose
中。
将人脸对齐添加到检测器中
现在我们可以将人脸对齐算法添加到上一篇文章中描述的视频人脸检测器中
class VideoFD:
def __init__(self, detector):
self.detector = detector
def detect(self, video, save_path = None, align = False, draw_points = False):
detection_num = 0;
capture = cv2.VideoCapture(video)
img = None
dname = 'AI face detection'
cv2.namedWindow(dname, cv2.WINDOW_NORMAL)
cv2.resizeWindow(dname, 960, 720)
frame_count = 0
dt = 0
face_num = 0
if align:
fa = Face_Align_Nose(160)
# Capture all frames
while(True):
(ret, frame) = capture.read()
if frame is None:
break
frame_count = frame_count+1
t1 = time.time()
faces = self.detector.detect(frame)
t2 = time.time()
p_count = len(faces)
detection_num += p_count
dt = dt + (t2-t1)
if (not (save_path is None)) and (len(faces)>0) :
f_base = os.path.basename(video)
for (i, face) in enumerate(faces):
if align:
(f_cropped, f_img) = fa.align(frame, face)
else:
(f_cropped, f_img) = self.detector.extract(frame, face)
if (not (f_img is None)) and (not f_img.size==0):
if draw_points:
Utils.draw_faces([f_cropped], (255, 0, 0), f_img, draw_points, False)
face_num = face_num+1
dfname = os.path.join(save_path, f_base + ("_%06d" % face_num) + ".png")
cv2.imwrite(dfname, f_img)
if len(faces)>0:
Utils.draw_faces(faces, (0, 0, 255), frame)
if not (save_path is None):
dfname = os.path.join(save_path, f_base + ("_%06d" % face_num) + "_frame.png")
cv2.imwrite(dfname, frame)
# Display the resulting frame
cv2.imshow(dname,frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
capture.release()
cv2.destroyAllWindows()
fps = frame_count/dt
return (detection_num, fps)
让我们在视频文件上启动人脸检测
d = MTCNN_Detector(50, 0.95)
vd = VideoFD(d)
v_file = r"C:\PI_FR\video\5_2.mp4"
save_path = r"C:\PI_FR\detect"
(f_count, fps) = vd.detect(v_file, save_path, True, False)
print("Face detections: "+str(f_count))
print("FPS: "+str(fps))
下图中,我们显示了相同位置的人脸,但现在它们已使用我们的算法进行了对齐。
您可以看到,现在所有图像的大小都相同。但是,对齐算法做得并不好。当人脸向左或向右旋转时,算法会失败并严重扭曲人脸。这是因为鼻尖与其他点不在同一平面上。当人脸侧倾时,点3不再是平行四边形的中心。
修改对齐算法
让我们尝试使用其他面部标志点来修改对齐算法。下图中,我们引入了一个新点 - 9。
点9计算为点4和点5之间的中间点。我们仍然使用四个点 - 1、2、8、7 - 作为透视变换的基础。但现在点7和点8的计算是基于点9的坐标。我们假设点6和点9位于平行四边形的中线上。下面的代码实现了这个算法
class Face_Align_Mouth(Face_Align):
def get_perspective_points(self, l_eye, r_eye, nose, mouth_l, mouth_r):
(xl, yl) = l_eye
(xr, yr) = r_eye
(xml, yml) = mouth_l
(xmr, ymr) = mouth_r
(xn, yn) = ( 0.5*(xl+xr), 0.5*(yl+yr) )
(xm, ym) = ( 0.5*(xml+xmr), 0.5*(yml+ymr) )
(dx, dy) = (xm-xn, ym-yn)
(xl2, yl2) = (xl+1.1*dx, yl+1.1*dy)
(xr2, yr2) = (xr+1.1*dx, yr+1.1*dy)
s = self.size
pts1 = np.float32([[xl, yl], [xr, yr], [xr2, yr2], [xl2, yl2]])
pts2 = np.float32([[s*0.3, s*0.3], [s*0.7, s*0.3], [s*0.7, s*0.75], [s*0.3, s*0.75]])
return (pts1, pts2)
在视频检测器中使用新的Face_Align_Mouth
类,并使用相同的视频文件启动它,我们得到以下对齐的人脸。
您可以看到,现在没有面部失真,并且所有面部都已正确对齐。请注意,并非所有对齐的面部看起来都像正对着摄像头。例如,旋转后的面部仍然看起来略有旋转。不幸的是,我们无法在2D图像中重建旋转人脸的前视图。
尽管如此,所有面部关键点现在相对于最终图像都具有相同的坐标。这正是我们期望从对齐算法中获得的——使人脸标准化到相同的图像坐标。这种方法确保了从数据库中的人脸和通过摄像头输入的人脸提取的特征属于同一空间。
后续步骤
在本系列的下一篇文章中,我们将在树莓派设备上运行我们的面部检测器。敬请期待!