最近更新于 2023-06-07 02:19

本文主要是为了介绍使用 face_recognition(后简称 fr),该项目提供的 API 可以快速实现人脸识别的功能。

fr 本身是一个开源项目,地址位于:https://github.com/ageitgey/face_recognition,中译文档:https://github.com/ageitgey/face_recognition/blob/master/README_Simplified_Chinese.md

安装 fr 后,可以通过命令行使用,也可以通过编写 Python 程序使用接口。

测试环境

Ubuntu 20.04 x86_64

Python 3.9.10

face_recognition 1.3.0

pip3 install face_recognition==1.3.0

OpenCV 4.5.5

pip3 install opencv-python==4.5.5.64

命令行使用

检测人脸

# 单张图片
face_detection [图片名]

# 多张图片
face_detection [图片所在路径]

我在网上下载的几张拜登的图片进行测试

检测到人脸后会分别显示 上、右、下、左的坐标

识别人脸

face_recognition [已知的人脸图片的文件夹] [待识别的人脸图片的文件夹]

我先创建两个文件夹 known 和 unknow,known 中存放已知的人脸,unknow 中存放待识别的人脸。

测试时发现出了问题,图片2明明只有川普,却有识别出拜登

那么此时可以调节容错率,默认容错率是 0.6,那么这里设置为 0.54 试试

--tolerance=0.54

这次识别就没问题了

如果要查看识别的人脸匹配程度,可以加上参数

--show-distance true

数值越小匹配度越高

当图片量比较大时,也可以支持并行,通过参数指定并行数量

--cpus

如果传 -1 则对应所有 CPU 核心

我这里测试出现了意外,居然并行数越多越慢,我猜测可能是图片数量比较少,就两张,本来很简单的工作,强行拆分到几个核心去,时间就耗在了分配上。

Python 开发

检测人脸

素材图片

from PIL import Image
import face_recognition


# 导入图片,需要自己指定图片
image = face_recognition.load_image_file("faces.png")

# 寻找图片中所有的人脸
# 基于 HOG-based 模型
# 该模型的准确度很高,不过相对 CNN 低,也没有 GPU 加速
face_locations = face_recognition.face_locations(image)

print("我在图片中发现了 {} 张脸。".format(len(face_locations)))

for face_location in face_locations:
    # 显示每张人脸的位置信息
    top, right, bottom, left = face_location
    print("人脸位置: 上: {}, 左: {}, 下: {}, 右: {}".format(top, left, bottom, right))

    # 单独截取人脸出来截取
    face_image = image[top:bottom, left:right]
    pil_image = Image.fromarray(face_image)
    pil_image.show()

上面的人脸检测是基于 HOG-based 模型,如果需要更高的准确度,可以使用 CNN。

但是相应的速度会很慢,有 GPU 加速支持的话会好一些。face_recognition 的底层依赖的是 Dlib 库,如果需要 GPU 加速支持,那么需要先配置 CUDA,然后再编译安装 Dlib,参阅:https://blog.iyatt.com/?p=1237

这里我是用的自己的笔记本电脑测试,有 NVIDIA 的独显。如果是用树梅派,那就没法用 GPU 加速了,NVIDIA 的 Jetson 开发板有 GPU,目前还没测试,等 fr 整体介绍写完后,我再看看。

from PIL import Image
import face_recognition

image = face_recognition.load_image_file("faces.png")

# CNN 模型
# number_of_times_to_upsample 多次采样次数,值越大,可以发现更小的人脸
face_locations = face_recognition.face_locations(image, number_of_times_to_upsample=0, model="cnn")

print("I found {} face(s) in this photograph.".format(len(face_locations)))

print("我在图片中发现了 {} 张脸。".format(len(face_locations)))

for face_location in face_locations:
    # 显示每张人脸的位置信息
    top, right, bottom, left = face_location
    print("人脸位置: 上: {}, 左: {}, 下: {}, 右: {}".format(top, left, bottom, right))

    # 单独截取人脸出来截取
    face_image = image[top:bottom, left:right]
    pil_image = Image.fromarray(face_image)
    pil_image.show()

当运行这个程序进行检测人脸,可以在左边(nvidia-smi -l 命令持续查看 GPU 资源使用信息)看到 GPU 使用率瞬间上升到 67%,并多出了一个 Python3 进程,代表 GPU 参与了加速计算。

有了 GPU 加速,检测速率会提高很多。前面是单张图像的,现在引入多张图像,使用一段小视频演示。

import face_recognition
import cv2


# 读视频
video_capture = cv2.VideoCapture("short_hamilton_clip.mp4")

frames = []
frame_count = 0

while video_capture.isOpened():
    # 从视频中获取一帧图像
    ret, frame = video_capture.read()
    # 视频结尾退出循环
    if not ret:
        break

    # 将图像从 OpenCV 的 BGR 转为 RGB
    frame = frame[:, :, ::-1]

    # 将视频中每一帧图像都存入列表中
    frame_count += 1
    frames.append(frame)

    # batch 默认大小为 128,如果显存足够大,可以改为 128
    # 我笔记本电脑独显就 2GB,同时处理的图像数再大一点就会超过显存容量,运行出错
    if len(frames) == 28:
        batch_of_face_locations = face_recognition.batch_face_locations(frames, number_of_times_to_upsample=0)

        # 列出找到的所有人脸
        for frame_number_in_batch, face_locations in enumerate(batch_of_face_locations):
            number_of_faces_in_frame = len(face_locations)

            frame_number = frame_count - 128 + frame_number_in_batch
            print("在 {} 帧图像中找到了 {} 张人脸".format(frame_number, number_of_faces_in_frame))

            for face_location in face_locations:
                # 显示出人脸在对应所在图片中的位置
                top, right, bottom, left = face_location
                print("人脸位于 上:{} 左: {} 下: {} 右: {}".format(top, left, bottom, right))

        # 清空 batch
        frames = []

人脸关键点提取

素材图片

from PIL import Image, ImageDraw
import face_recognition


# 导入图片
image = face_recognition.load_image_file("two_people.jpg")

# 寻找图像中所有人脸的面部特征
face_landmarks_list = face_recognition.face_landmarks(image)

print("在图像中找到 {} 张人脸".format(len(face_landmarks_list)))

# 创建一个 PIL 绘图对象,以便在图像中绘画
pil_image = Image.fromarray(image)
d = ImageDraw.Draw(pil_image)

for face_landmarks in face_landmarks_list:
    # 显示人脸面部特征在图像中的位置
    for facial_feature in face_landmarks.keys():
        print("特征 {} 位于 {}".format(facial_feature, face_landmarks[facial_feature]))

    # 用线条勾勒出面部特征
    for facial_feature in face_landmarks.keys():
        d.line(face_landmarks[facial_feature], width=5)

# 显示图片
pil_image.show()

人脸识别

图像素材

import face_recognition

# 导入图像
biden_image = face_recognition.load_image_file("biden.jpg")
obama_image = face_recognition.load_image_file("obama.jpg")
unknown_image = face_recognition.load_image_file("obama2.jpg")

# Get the face encodings for each face in each image file
# Since there could be more than one face in each image, it returns a list of encodings.
# But since I know each image only has one face, I only care about the first encoding in each image, so I grab index 0.
# 检测图像中人脸
# 一张图像可能含有多张人脸,返回结果是一个列表
# 但是根据这里准备的素材,我自己是知道一张图片只有一张人脸,所以检测到的人脸肯定位于下标 0 的位置
try:
    biden_face_encoding = face_recognition.face_encodings(biden_image)[0]
    obama_face_encoding = face_recognition.face_encodings(obama_image)[0]
    unknown_face_encoding = face_recognition.face_encodings(unknown_image)[0]
except IndexError:
    print("我在其中一张图像中未发现任何人脸,请准备含有人脸的图片文件!")
    quit()

# 已知的人脸
known_faces = [biden_face_encoding, obama_face_encoding]

# 结果返回 True 或 False,告诉我们待识别的人脸是否有匹配的已知人脸
results = face_recognition.compare_faces(known_faces, unknown_face_encoding)

print("识别到的人脸是 Biden? {}".format(results[0]))
print("识别到的人脸是 Obama? {}".format(results[1]))
print("识别到的人脸是新面孔?没有匹配的已知人脸? {}".format(not True in results))

人脸识别完整样例程序,支持实时预览

'''
人脸识别样例程序
视频源可以来自于摄像头、视频文件、视频源链接
实时预览识别情况,对识别出的人脸进行标注

使用时需要在代码所在目录下创建一个文件夹名为 known_faces ,将已知人脸图片放在里面,图片格式要求为 .jpg,不能在该文件夹下放置其它文件
如果识别时在已知人脸中有匹配的图像,那么会用已知人脸图片名对识别到的人脸进行标注

该样例程序单纯完整地示例一遍人脸识别的实现,并未写异常处理,如果未按要求放置已知人脸图片文件,可能会出现意想不到的错误。


Copyright (C) 2022 IYATT-yx iyatt@iyatt.com
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''

import os
import cv2
from cv2 import batchDistance
import numpy as np
from face_recognition import *


class Face_recognition(object):
    def __init__(self, cap):
        # 已知人脸的路径
        known_face_folder = os.sep.join([os.path.abspath(os.path.dirname(__file__)), 'known_faces'])
        # 已知人脸图片的文件名
        self.known_face_names = [known_face_name.strip('.jpg') for known_face_name in os.listdir(known_face_folder)]
        # 已知人脸图片的绝对路径
        known_face_paths = [os.sep.join([known_face_folder, known_face_name + '.jpg']) for known_face_name in self.known_face_names]
        # 导入已知人脸的图片
        known_face_imgs = [load_image_file(known_face_name) for known_face_name in known_face_paths]
        # 提取人脸并编码 
        self.known_face_encodings = [face_encodings(known_face_img, face_locations(known_face_img))[0] for known_face_img in known_face_imgs]
        # 打开视频流
        self.cap = cv2.VideoCapture(cap)
        # 保存上次的计数,用于 FPS 计算
        self.last = 0

    def add_fps(self, img, x, y, r, g, b):
        now = cv2.getTickCount()
        fps = int(cv2.getTickFrequency() / (now - self.last))
        cv2.putText(img, 'FPS: {}'.format(fps), (x,y), cv2.FONT_HERSHEY_COMPLEX, 1, (b,g,r))
        self.last = now

    def run(self):
        while True:
            ret, bgr_img = self.cap.read()
            # 视频播放完就退出
            if not ret:
                break
            # BGR 转 RGB
            rgb_img = bgr_img[:, :, ::-1]
            # 在图像上添加 FPS
            self.add_fps(bgr_img, 50, 50, 255, 0, 0)
            # 检测人脸
            locations = face_locations(rgb_img)
            # 没有检测到人脸继续下一帧
            if not locations:
                cv2.imshow('face_recognition', bgr_img)
                if cv2.waitKey(1) == 27:
                    break
                continue
            # 对检测到的人脸编码
            unknow_face_encodings = face_encodings(rgb_img, locations)

            # 将待检测的人脸数据与已知人脸数据进行对比
            face_names = []
            for unknow_face_encoding in unknow_face_encodings:
                matches = compare_faces(self.known_face_encodings, unknow_face_encoding)
                name = 'stranger'
                distance = face_distance(self.known_face_encodings, unknow_face_encoding)
                best_match_idx = np.argmin(distance)
                if matches[best_match_idx]:
                    name = self.known_face_names[best_match_idx]
                print('识别为 {},与 {} 的匹配度分别为:{}'.format(name, self.known_face_names, 1 - distance))
                face_names.append(name)

            # 根据检测到的人来内位置在图像上框画出来,并标注识别结果
            for (top, right, bottom, left), name in zip(locations, face_names):
                cv2.rectangle(bgr_img, (left, top), (right, bottom), (0, 0, 255), 2)
                cv2.putText(bgr_img, name, (left + 6, bottom - 6), cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), 1)

            # 预览图像
            cv2.imshow('face_recognition', bgr_img)
            if cv2.waitKey(1) == 27:  # 按 Esc 退出
                break

    def __del__(self):
        self.cap.release()  # 析构释放视频采集对象


if __name__ == '__main__':
    fr = Face_recognition(0)  # 指定数字就对应系统摄像头编号,另外也可以指定视频路径或者视频源的链接
    fr.run()  # 运行识别程序

运行效果