介绍
DLib库:一个机器学习的开源库,包含了机器学习的很多算法,使用起来很方便,直接包含头文件即可,并且不依赖于其他库(自带图像编解码库源码)Dlib可以帮助您创建很多复杂的机器学习方面的软件来帮助解决实际问题。目前Dlib已经被广泛的用在行业和学术领域,包括机器人,嵌入式设备,移动电话和大型高性能计算环境。
ps:这里以对静态图像进行人脸识别为例,如需进行视频实时人脸识别,只需利用opencv调用摄像头对每帧进行识别即可,思路和原理是一样的。
人脸识别的一般步骤以及原理:
- 人脸检测:
- 输入一张带有人脸的图像,将图像中的人脸检测出来,例如常见的检测人脸位置并在对应位置绘 制矩形框
- 提取人脸特征点:
- 在检测得到图像中人脸位置的基础之上,进一步对人脸进行关键点检测,每张人脸上都对应许多关键点,每个关键点都对应人脸的特定位置,这些关键点包含着该张人脸的各种信息
- 将人脸信息映射为特征信息并保存:
- 利用相关已经训练好的模型进行信息映射,输入检测的人脸信息,将其转化为该人脸对应的128D特征信息并保存,认为每张人脸与128D特征信息相对应
- 人脸识别:
- 人脸识别顾名思义就是输入一张人脸图像,识别出该人脸对应的信息,当我们输入一张新的人脸图像后,将其进行以上三个步骤得到其128D特征信息,然后将其与存储的映射信息(每个人脸128D特征信息对应名字)进行匹配
人脸检测
思路:
-
准备工作:
- 给定待检测图像路径,利用opencv将图像加载进来
- 调用dlib的get_frontal_face_detector() 得到dlib提供的正向人脸检测器, 该检测器用于识别图像中的人脸位置 (输入图像, 得到图像中的人脸位置信息)
-
人脸检测:
- 利用正向人脸检测器直接对图像进行检测,该类实现了__call__方法,因此可以直接通过对象进行调用。此外,传入两个参数,第一个为待检测图像,由于opencv加载的图像默认为为BGR(Blue, Green, Red),而该参数需要RGB的信息格式,这里将其转化为RGB(Red, Green, Blue)并进行传入,第二个参数默认为1,表示对图像进行上采样一次以获取更多相关信息,这里上采样其实就是和池化相反的操作 (池化是进行信息融合/下采样以得到更小的维度但丢失了部分信息)
- ps:这里不转化为RGB其实也可以,因为人脸检测对于颜色信息不敏感,用处不大
-
处理结果
- 得到一个序列对象,该对象包含检测到的人脸的信息。由于一张图像中可能有着多张人脸,因此对其进行遍历,每个元素对应一张人脸,包含对应人脸的位置信息。
- 通过opencv在检测到的特定位置进行矩形框绘制并显示图像
代码:
import dlib
import cv2
import os
def detector_face(img_path):
print(f'正在处理的图片路径: {img_path}')
img = cv2.imread(img_path) # 加载图片
detector = dlib.get_frontal_face_detector() # 正向人脸检测器
# 上采样一次并进行人脸检测
results_dets = detector(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 1) # 上采样一次并进行检测
print(type(results_dets))
# 输出检测结果
print(f'共检测到 {len(results_dets)} 张人脸')
for index, d in enumerate(results_dets):
print(f'Detector: {index}, rectangle: position1-{(d.left(), d.top())}, position2-{d.right(), d.bottom()}')
# 绘制矩形框
cv2.rectangle(img, (d.left(),d.top()),(d.right(), d.bottom()), color=(255,255,0), thickness=3)
cv2.imshow('result', img)
cv2.waitKey(0) # 等待无限长时间, 等待用户按下一个键
if __name__ == '__main__':
test_img_list = ['test1.jpg', 'test2.jpeg', 'test4.jpg']
test_data_path = os.path.join('data', 'image_data')
for test_img in test_img_list:
img_path = os.path.join(test_data_path, test_img)
detector_face(img_path)
在main方法中进行测试,测试结果如下(测试图像来源于Bing图片):
如图所示,人脸检测部分完成,总体逻辑很简单
人脸特征点检测
思路和人脸检测基本相同:
在通过正向人脸检测器检测得到图像中的人脸位置信息之后,通过 68个关键点检测模型 输入每张人脸位置数据,得到68个特征点位置信息,并通过opencv将这些关键点绘制并连线 (ps: 绘制只是可视化操作,特征点连线非必须)
- 68个关键点检测模型:
model/shape_predictor_68_face_landmarks.dat
从网上下载,指定路径通过DLib提供的方法进行加载即可。
人脸68个关键点:
关键点对应信息:每个关键点对应一个序号
# 例如lips: 关键点 [48, 60) 表示嘴唇相关信息, (Red, Green, Blue, 透明度)
pred_types = {
'face': ((0, 17), (0.682, 0.780, 0.909, 0.5)),
'eyebrow1': ((17, 22), (1.0, 0.498, 0.055, 0.4)),
'eyebrow2': ((22, 27), (1.0, 0.498, 0.055, 0.4)),
'nose': ((27, 31), (0.345, 0.239, 0.443, 0.4)),
'nostril': ((31, 36), (0.345, 0.239, 0.443, 0.4)),
'eye1': ((36, 42), (0.596, 0.875, 0.541, 0.3)),
'eye2': ((42, 48), (0.596, 0.875, 0.541, 0.3)),
'lips': ((48, 60), (0.596, 0.875, 0.541, 0.3)),
'teeth': ((60, 68), (0.596, 0.875, 0.541, 0.4))
}
对应图片:注意图片中是从1开始的,上面的说明是从索引0开始的
代码:
import dlib
import cv2
import os
# 将当前特征点与相邻的下一个特征点之间绘制连线
def draw_line(img, shape, i):
cv2.line(img, pt1=(shape.part(i).x, shape.part(i).y), pt2=(shape.part(i+1).x, shape.part(i+1).y),
color=(255, 0, 0), thickness=1)
# 连接关键点
def connect_points(img, shape, i):
if i + 1 < 17:
# face
draw_line(img, shape, i)
elif 17 < i + 1 < 22:
# eyebrow1
draw_line(img, shape, i)
elif 22 < i + 1 < 27:
# eyebrow2
draw_line(img, shape, i)
elif 27 < i + 1 < 31:
# nose
draw_line(img, shape, i)
elif 31 < i + 1 < 36:
# nostril
draw_line(img, shape, i)
elif 36 < i + 1 < 42:
# eye1
draw_line(img, shape, i)
elif 42 < i + 1 < 48:
# eye2
draw_line(img, shape, i)
elif 48 < i + 1 < 60:
# lips
draw_line(img, shape, i)
elif 60 < i + 1 < 68:
# teeth
draw_line(img, shape, i)
# 使用DLib库提供的训练好的模型shape_predictor_68_face_landmarks.dat检测出人脸上的68个关键点
def face_landmarks_detector(img_path, is_point_digit=False, is_connect=False, is_save_result=False):
print(f'正在处理图像: {img_path}')
# 正面人脸检测器
detector = dlib.get_frontal_face_detector()
predictor_path = 'model/shape_predictor_68_face_landmarks.dat'
# 将人脸关键点检测模型加载到内存中并创建一个shape_predictor实例
shape_predictor = dlib.shape_predictor(predictor_path)
img = cv2.imread(img_path) # 加载图像
dets = detector(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 1) # 上采样一次
print(f'检测到的人脸数量: {len(dets)}')
for i, d in enumerate(dets):
print(f'Detection: {i}, pos-1: {(d.right(), d.top())}, pos-2: {(d.left(), d.bottom())}')
cv2.rectangle(img, (d.right(), d.top()), (d.left(), d.bottom()), color=(255, 0, 0), thickness=2) # 绘制矩形框
shape = shape_predictor(img, d) # 调用模型通过人脸位置信息进行关键点检测
# 打印相关信息
print(f'关键点个数: {shape.num_parts}')
print(f'每个面部的矩形框位置列表: {shape.rect}')
print(f'关键点-1: {shape.part(0)}, 关键点-2: {shape.part(67)}')
print(f'关键点坐标: {shape.parts()}') # 68个关键点, 因此有68个坐标
# 绘制关键点
for i, point in enumerate(shape.parts()):
cv2.circle(img, (point.x, point.y), 1, color=(255, 0, 255), thickness=1) # 每一个关键点通过绘制一个小圆形来表示
if is_point_digit:
# 在每个关键点的位置绘制关键点编号文本
cv2.putText(img, str(i+1), (point.x, point.y), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.3, color=(0, 255, 0))
if is_connect:
connect_points(img, shape, i)
cv2.imshow('detect landmarks', img)
if is_save_result:
dir, f= os.path.split(img_path)
suffix = f.split('.')[1]
save_f = os.path.join(dir, 'detection_results', f.split('.')[0] + '_points' + '.' + suffix)
cv2.imwrite(save_f, img)
cv2.waitKey(0)
if __name__ == '__main__':
test_img_list = ['test1.jpg', 'test2.jpeg','test4.jpg']
test_data_path = os.path.join('data', 'image_data')
for test_img in test_img_list:
img_path = os.path.join(test_data_path, test_img)
face_landmarks_detector(img_path, is_point_digit=True, is_connect=True, is_save_result=True)
draw_line和connect_points方法用于对不同部位的关键点绘制连接,思路与人脸检测代码类似,在检测到人脸位置后进一步提取人脸关键点信息并绘制。
测试结果(测试图像来源于Bing图片):
人脸识别
人脸识别一般包括两个步骤:
- 注册人脸信息 (gallery)
- 人脸识别
例如有张三、李四、王五三个人,我们要对它们进行人脸识别,要先将这几个人进行注册,得到它们的128D人脸信息并存储在特定的位置,例如{张三的人脸信息: {张三}}, 当下一次对张三进行人脸识别时,我们对新检测到的张三人脸与存储的人脸信息列表进行挨个比对,若成功匹配上则识别到对应的人名。
这里有几个问题:
-
如何注册:由于项目比较简单,我们仅仅对每个人的一张人脸图像进行128D特征信息提取保存即可,注册的人脸图像一般质量要求较高(光照、遮挡、阴影等)。在实际应用中,现实情况往往较为复杂,我们可以对每个人的人脸的不同角度(正脸、侧脸等)进行提取并保存,存储中这几个人脸信息都对应同一人名,可以一定程度上提高人脸识别的准确度
-
如何转换为128D特征信息:这里利用的是
dlib_face_recognition_resnet_model_v1.dat
可以在网上自行下载,利用DLib提供的相关API直接加载即可。- 这里利用的模型是ResNet-34模型
-
如何匹配人脸特征:计算两个人脸信息的欧式距离(L2范数),然后设定为一个阈值,当欧式举例小于该距离时,则认为是同一张人脸
此外,face_descriptor = facerec.compute_face_descriptor(img, shape, 100, 0.25)
用于提取128D特征信息,参数分别为: 处理图像, 人脸特征点信息, 重新采样次数, 填充大小;第三个参数若重新采样次数为100,计算速度则为值为10时候的十倍;第四个参数是用来调整人脸检测框的大小的,它表示相对于检测到的人脸框的大小,需要扩大或缩小的比例,通过调整第四个参数的值,可以控制计算人脸特征描述子的区域的大小,从而影响计算特征的准确性和速度。
以下代码中的face_chip是对原始图像进行对齐处理,即将人脸旋转和缩放到标准位置和大小,以便于后续的特征提取和匹配。get_face_chip()
函数可以将人脸图像进行对齐,并提取出人脸区域。它将原始人脸图像中的人脸部分旋转、缩放、裁剪等变换操作,得到一个150*150大小的图像,使得得到的人脸区域能够更好地适应后续的特征提取和识别过程。
代码:
import cv2
import glob
import os
import dlib
import numpy as np
# 使用CUDA, 如果不支持CUDA将自动切换为CPU
dlib.DLIB_USE_CUDA=True
# 计算得到欧氏距离
def get_euclidean(img1, img2):
return np.linalg.norm(img1 - img2, ord=2)
def extracting_feature_vector(img, model_path):
"""
# 提取图像中人脸嵌入向量信息
:param img: 原始图像数组
:param model_path: 模型所在路径
:return: [(图像中每个人脸的嵌入向量信息, 矩形框位置信息)]
"""
print('extracting feature vector...')
# 加载模型
detector = dlib.get_frontal_face_detector() # 前向人脸检测器
predictor = dlib.shape_predictor(model_path['shape_predictor']) # 关键点提取器
recognizer = dlib.face_recognition_model_v1(model_path['face_resnet-34']) # 人脸识别器
# 检测人脸
dets = detector(img, 1)
print(f'图像中的人脸数量: {len(dets)}')
# 存储每张图片中人脸的矩形框坐标以及特征
face_features, face_rects = [], []
# 处理当前图像中检测到的人脸
for i, det in enumerate(dets):
# 打印矩形框信息
print(f'Detection: {i}, pos-1: {(det.left(), det.top())}, pos-2: {(det.right(), det.bottom())}')
# 存储矩形框位置坐标
face_rects.append({'pos1': (det.left(), det.top()), 'pos2': (det.right(), det.bottom())})
# 关键点检测
shape = predictor(img, det)
# 这个和下面那个选一个用就行了, 这里用的是下面那个
# face_desc = recognizer.compute_face_descriptor(img, shape, 10, 0.25)
# 对齐原始图像,提取人脸区域并进行归一化处理, 得到图像对应的face chip(ndarray对象)
face_chip = dlib.get_face_chip(img, shape)
face_chip_desc = recognizer.compute_face_descriptor(face_chip) # 转换为128D的特征向量信息
face_chip_desc_array = np.array(face_chip_desc)
face_features.append(face_chip_desc_array) # 存储起来
# 每个人脸的嵌入向量信息和矩形框位置信息
face_features_and_rects = list(zip(face_features, face_rects))
return face_features_and_rects
# 提取数据集中所有图片信息, 并转化为字典, label为图片名
def extract_feature(regist_img_paths, model_path):
register_features = {}
for p in regist_img_paths:
img = cv2.imread(p)
img_name = os.path.split(p)[-1].split('.')[0]
face_features_and_rects = extracting_feature_vector(img, model_path=model_path)
register_features[img_name] = face_features_and_rects[0][0] # {name: 特征向量}
return register_features
def face_recognition(img_paths, regist_img_paths, model_path, is_display_info=False, is_save=False):
# 注册图像信息
register_features_dict = extract_feature(regist_img_paths, model_path=model_path)
# 读取图片
for p in img_paths:
tar_img = cv2.imread(p)
tar_img_name = os.path.split(p)[-1]
print(f'正在识别: {tar_img_name}...')
tar_face_features_and_rects = extracting_feature_vector(tar_img, model_path=model_path) # 将该图像转换为128D信息
# 绘制显示目标图片信息
if is_display_info:
cv2.putText(tar_img, tar_img_name.split('.')[0], (20, tar_img.shape[0] - 20), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.65, color=(255, 0, 0), thickness=2)
cv2.putText(tar_img, f'Face Num: {len(tar_face_features_and_rects)}', (20, tar_img.shape[0] - 50),
fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.8, color=(255, 0, 0), thickness=2)
for feature_and_rect in tar_face_features_and_rects: # 对目标图像中识别到的每个人脸进行对比
tar_face_feature = feature_and_rect[0] # 128D信息
tar_face_rect = feature_and_rect[1] # 矩形框位置信息
# 对当前图像中的当前人物进行矩形框绘制
cv2.rectangle(tar_img, tar_face_rect['pos1'], tar_face_rect['pos2'], color=(0,255,255),
thickness=2)
# 对比目标人脸和数据库中的每张人脸
euclideans_dict = dict() # {欧氏距离: 图片名}
for src_name, src_face_feature in register_features_dict.items():
# 计算源图像和目标图像之间的欧氏距离
euclideans_distance = get_euclidean(src_face_feature, tar_face_feature)
euclideans_dict[euclideans_distance] = src_name
# 得到记录中最小的欧氏距离及图片名
min_eucli = min(euclideans_dict.keys())
# 这里将阈值设为0.6
if min_eucli < 0.6:
predict_name = euclideans_dict[min_eucli]
# 将识别结果绘制在图片上
cv2.putText(tar_img, predict_name, (tar_face_rect['pos1'][0], tar_face_rect['pos1'][1]-2),
fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.65, color=(0,255,255), thickness=2)
else:
cv2.putText(tar_img, 'unknown', (tar_face_rect['pos1'][0], tar_face_rect['pos1'][1] - 2),
fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.65, color=(255, 0, 0), thickness=2)
print(f'匹配成功: {predict_name}')
if is_save:
save_name = tar_img_name.split('.')[0] + '_reco.' + tar_img_name.split('.')[1]
save_filename = os.path.join('data', 'reco_results', save_name)
cv2.imwrite(save_filename, tar_img)
print('输出结果已保存...')
if __name__ == '__main__':
# 模型路径
model_path = {'shape_predictor': os.path.join('model', 'shape_predictor_68_face_landmarks.dat'),
'face_resnet-34': os.path.join('model', 'dlib_face_recognition_resnet_model_v1.dat')}
# 图片数据路径
img_paths = glob.glob(os.path.join('data', 'test_data', '*.jpg'))
register_img_paths = glob.glob(os.path.join('data', 'register_image_data', '*.jpg'))
# 人脸识别
face_recognition(img_paths=img_paths,regist_img_paths=register_img_paths , model_path=model_path, is_display_info=True, is_save=True)
这里仅仅是简单地将图像的名称作为对应label,将检测结果绘制到图像上,若没有匹配到,则为unknown。
总结
人脸识别的一般步骤:
- 人脸检测
- 人脸关键点提取
- 人脸识别
以上人脸识别代码还有许多地方可以优化,例如可以将上面提到的用多张人脸进行注册并放到对应数据库中,此外,当图像中出现人脸数量过多时会导致识别速度变慢,这里可以利用Python提供的线程池对人脸识别/检测进行多线程检测,另外对于人脸特征匹配的算法也可以进一步优化等等… 这些就不再做一一介绍了,在一般思路上面进行优化即可。
参考: