5

视频是不能 P 的系列:使用 Dlib 实现人脸识别

 1 year ago
source link: https://blog.yuanpei.me/posts/dlib-face-recognition-with-machine-learning/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

视频是不能 P 的系列:使用 Dlib 实现人脸识别

Featured image of post 视频是不能 P 的系列:使用 Dlib 实现人脸识别

本文是 #视频是不能 P 的系列# 的第三篇。此前,我们已经可以通过 OpenCV 或者 Dlib 实现对人脸的检测,并在此基础上实现了某种相对有趣的应用。譬如,利用人脸特征点提取面部轮廓并生成表情包、将图片中的人脸批量替换为精神污染神烦狗 等等。当然,在真实的应用场景中,如果只是检测到人脸,那显然远远不够的,我们更希望识别出这张人脸是谁。此时,我们的思绪将会被再次拉回到人脸识别这个话题。在探索未知世界的过程中,博主发现 OpenCV 自带的 LBPH 方法,即局部二值模式直方图方法,识别精度完全达不到预期效果。所以,博主最终选择了 Dlib 里的特征值方法,即:对每一张人脸计算一个 128 维的向量,再通过计算两个向量间的欧式距离来判断是不是同一张人脸。在此基础上,博主尝试结合 支持向量机 来实现模型训练。因此,这篇文章其实是对整个探索过程的梳理和记录,希望能给大家带来一点启发。

如下图所示,假设对于每一个人物 X ,我们有 N 个人脸样本,通过 Dlib 提供的 compute_face_descriptor() 方法,我们可以计算出该人脸样本的特征值,这是一个 128 维度的向量。如果我们对这些人脸样本做同样的处理,我们就可以得到人物 X 的特征值列表 feature_list_of_person_x。在此基础上,利用 MumPy 中的 mean() 方法,我们就可以计算出人物 X 的平均特征 features_mean_person_x。最终,我们把人物 X 的平均特征和名称一起写入到一个 CSV 文件里面。至此,我们已经拥有了一个简单的人脸数据库。

Dlib 人脸识别原理说明图

Dlib 人脸识别原理说明图

可以预见的是,一旦我们把人脸特征数值化,那么,人脸识别就从一个图形学问题变成了数学问题。对于图中的待检测人脸,我们只需要按同样地方式计算出特征值,然后从 CSV 文件中找一个距离它最近的特征即可。这里,博主使用的是欧式距离,并且人为规定了一个阈值 0.4, 即:当这个距离小于 0.4 时,我们认为人脸匹配成功;当这个距离大于 0.4 时,我们认为人脸匹配失败。下面的例子展示了使用 Dlib 计算人脸特征值的基本过程:

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
face_reco_model = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")

# 计算特征值
image = Image.open('/faces/person/sample-0.jpg')
image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
faces = detector(image, 1)
if len(faces) != 0:
    face = faces[0]
    shape = predictor(image, face)
    face_descriptor = face_reco_model.compute_face_descriptor(image, shape)

此时, face_descriptor 就是当前人脸的特征值,这是一个 128 维的向量。我们只需要对每一个人脸样本重复以上步骤,就可以获得人物 X 的特征值列表,进而计算出人物 X 的特征均值。

如下图所示,我们为每一个人物建立了一个文件夹,文件夹的名称即为对应人物的名称:

人脸样本的目录结构

人脸样本的目录结构

显然,对于每一个人物而言,我们只需要遍历该文件夹下的所有图片,即可计算出它的特征均值:

def get_mean_features_of_face(path):
    path = os.path.abspath(path)
    subDirs = [os.path.join(path, f) for f in os.listdir(path)]
    subDirs = list(filter(lambda x:os.path.isdir(x), subDirs))
    for index in range(0, len(subDirs)):
        subDir = subDirs[index]
        person_label = os.path.split(subDir)[-1]
        image_paths = [os.path.join(subDir, f) for f in os.listdir(subDir)]
        image_paths = list(filter(lambda x:os.path.isfile(x), image_paths))
        feature_list_of_person_x = []
        for image_path in image_paths:
            # 计算每一个图片的特征
            feature = get_128d_features_of_face(image_path)
            if feature == 0:
                continue
            
            feature_list_of_person_x.append(feature)
        
        # 计算当前人脸的平均特征
        features_mean_person_x = np.zeros(128, dtype=object, order='C')
        if feature_list_of_person_x:
            features_mean_person_x = 
                np.array(feature_list_of_person_x, dtype=object).mean(axis=0)
        
        yield (features_mean_person_x, person_label)

其中,get_128d_features_of_face() 方法用来计算某个图片的特征值:

def get_128d_features_of_face(image_path):
    image = Image.open(image_path)
    image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
    faces = detector(image, 1)

    if len(faces) != 0:
        shape = predictor(image, faces[0])
        face_descriptor = face_reco_model.compute_face_descriptor(image, shape)
    else:
        face_descriptor = 0
    return face_descriptor

好了,当我们计算出每一个人物的特征均值以后,我们需要把当前人物的名称、特征均值一起写入到 CSV 文件里面,这样做的目的是方便我们后面做人脸识别:

def extract_features_to_csv(faces_dir):
    mean_features_list = list(get_mean_features_of_face(faces_dir))
    with open(FACES_FEATURES_CSV_FILE, "w", newline="") as csvfile:
        writer = csv.writer(csvfile)
        for mean_features in mean_features_list:
            person_features = mean_features[0]
            person_label = mean_features[1]
            person_features = np.insert(person_features, 0, person_label, axis=0)
            writer.writerow(person_features)

此刻,我们就拥有了一个简单的人脸数据库,对于任意一个待检测的人脸,我们只需要计算其特征值,然后再从人脸数据库中找到一个距离最小的特征即可:

def compare_face_fatures_with_database(database, image_path):
    image = Image.open(image_path)
    image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
    faces = detector(image, 1)
    
    campare_results = []
    if len(faces) != 0:
        for i in range(len(faces)):
            face = faces[i]
            shape = predictor(image, faces[0])
            face_descriptor = face_reco_model.compute_face_descriptor(image, shape)
            face_feature_distance_list = []
            for face_data in database:
                # 比对人脸特征,当距离小于 0.4 时认为匹配成功
                dist = get_euclidean_distance(face_descriptor, face_data[1])
                dist = round(dist, 4)

                if dist >= FACES_FATURES_DISTANCE_THRESHOLD:
                    continue

                face_feature_distance_list.append((face_data[0], dist))
            
            # 按距离排序,取最小值进行绘制
            sorted(face_feature_distance_list, key=lambda x:x[1])
            if face_feature_distance_list:
                person_dist = face_feature_distance_list[0][1]
                person_label = face_feature_distance_list[0][0]
                campare_results.append((person_label, person_dist))

    return campare_results

可以注意到,我们会遍历人脸数据库中的每一个特征值,然后计算它和当前人脸特征的欧式距离。接下来,我们会对这些距离进行排序,选取距离最小的一组作为匹配结果,下面的代码片段展示了如何去计算一个欧氏距离:

def get_euclidean_distance(feature_1, feature_2):
    feature_1 = np.array(feature_1)
    feature_2 = np.array(feature_2)
    return np.sqrt(np.sum(np.square(feature_1 - feature_2)))

如下图所示,我们可以输出每一张人脸的名称,以及当前人脸相对于特征均值的距离,经博主测试,这个方案的正确率可以达到 94.58% 左右,是一种相对比较靠谱的做法:

人脸识别效果展示

人脸识别效果展示

不过,这个方案的缺点同样非常明显,即:它必须遍历人脸数据库中的每一条数据,这意味着它的时间复杂度是 O(n)。考虑到现实生活中需要录入的人脸数据可能是成百上千的,这个方案的执行效率注定会越来越低,这迫使我们不得不换一种思路来解决这个问题。重新审视人脸识别这个问题,我们会发现,这其实是一个分类问题,就像我们人为地去给一堆照片贴上标签一样。当然,它并不是一个非此即彼的二分类问题,而是一个多分类的问题。如果你接触过 Scikit Learn,那么,你大概会想到通过某种分类器进行模型训练的思路,这里以支持向量机为例:

# 提取人脸特征和标签
mean_features_list = list(get_mean_features_of_face(faces_dir))
features = list(map(lambda x:x[0], face_encodings_mean_list))
labels = list(map(lambda x:x[1], face_encodings_mean_list))

# 通过支持向量机训练模型
clf = svm.SVC(C=1.0, kernel="linear", gamma='scale', probability=True)
clf.fit(features, labels)

在这种情况下,我们可以利用 joblib.dump()joblib.load() 两个方法对模型进行保存和加载。此时,我们该如何识别一个人脸呢?简单来说,你可以把支持向量机想象成一个线性函数,因此,我们只需要传入待检测图片的特征值,它就可以帮我们计算/预测出人脸的分类。显然,它不需要像前面的方案一样遍历每一条人脸数据,因此,支持向量机这个方案执行效率上要更好一点:

features = get_128d_features_of_face(image_path)
predict_label = clf.predict(features)

当然,世界上没有百分之百完美的方案,支持向量机如果遇到了一张陌生的人脸,它的预测结果就会变得离谱起来,譬如,孙燕姿被识别为堺雅人、堺雅人被识别为胡歌,用鲁迅先生的话讲,屏幕内外充满了快活的空气。虽然,Scikit Learn 里可以用 predict_proba() 方法引入概率来评估结果的准确性,可博主实际使用下来,发现效果并没有好多少。无独有偶,OpenCV 自带的 LBPH 方法,其模型训练和这个思路非常地相似,无非是它接收是一个 Mat 类型的参数:

recognizer = cv2.face.LBPHFaceRecognizer_create()
detector = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")

# 获取人脸样本及标签
face_samples = get_images_and_labels(DATASETS_DIR)
faces = list(map(lambda x:x[0], face_samples))
faceIds = list(map(lambda x:x[1], face_samples))

# 模型训练
recognizer.train(faces, np.array(faceIds))
recognizer.save('/train/train_data.yml')

# 模型预测
recognizer.read('/train/train_data.yml')
faceId, confidence = recognizer.predict(image)

它会返回人脸的 Id 以及置信度,我们可以结合这个置信度去做进一步的判断,比如置信度超过 50% 就认为它匹配到了人脸。我一开始提到这个方案的精度达不到要求,就是因为它经常给出错误的预测结果,甚至比支持向量机遇到陌生人脸时还要离谱。所以,对于 OpenCV 官方的这个方案呢,我们知道它是怎么回事就可以了,我个人并不推荐在真实的场景中使用这个方法。当然,OpenCV 后来开始支持 CNN,我们值得更好的人脸识别方案,限于篇幅和精力,这个等以后有机会了再展开说说。

人脸识别运用到视频中的效果展示:献给我永远的歌晨 CP

人脸识别运用到视频中的效果展示:献给我永远的歌晨 CP

本文分享了 OpenCVDlib 下的人脸识别的实现,由于 OpenCV 自带的 LBPH 方法识别效果不符合预期,博主不得不使用 Dlib 来实现人脸识别。这里采用的思路是,计算出每一个人物的人脸特征均值,它是一个 128 维的向量,通过计算待检测人脸特征值与已知人脸特征值的欧式距离,来判断两张脸是否为同一张脸,经过测试,该方案的识别率可以达到 94.58% ,是一种相对来说比较靠谱的方案。考虑到这个方案的时间复杂度为 O(n) ,我们不得不考虑效率更高的做法。在这个基础上,我们尝试结合支持向量机做进一步的优化,支持向量机最大的问题时,当它面对一张陌生人脸的时候,其预测结果会变得非常离谱,可是在最好的情况下,它的准确率依然接近 97.04%。作为对照组,博主捎带着介绍了一下 OpenCV 自带的 LBPH 方法如何训练模型,权当帮大家扩展思路,只有此中的成败利钝,只有靠大家自己去取舍啦!最后,听说射雕英雄传又要翻拍啦,我宣布我永远都喜欢胡歌这一版的射雕英雄传,哈哈!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK