作为一名技术博主,我们不仅要展示酷炫的成果,更要清晰地剖析其背后的实现原理。今天,我们将为你呈现一个功能完善的人脸识别门禁系统,整个项目分为两大核心模块:用户注册实时识别

一、项目准备与环境搭建

在开始编码之前,确保你的开发环境已就绪是第一步。人脸识别项目依赖于一些强大的Python库,特别是 face_recognition (基于 dlib) 和 OpenCV,以及用于中文显示的 Pillow

我们需要安装几个关键的Python库。在你的终端或命令提示符中,执行以下命令:

pip install face-recognition opencv-python numpy Pillow

1. 安装核心库

face_recognition: 这个库极大地简化了人脸识别任务,它封装了 dlib 的复杂模型,让你能轻松进行人脸检测、特征提取和比对。

2.dlib 库的安装挑战与解决

face_recognition 库的核心依赖是 dlibdlib 是一个 C++ 库,它提供了高级的机器学习算法,包括人脸检测器和特征提取器。

自动安装:通常,pip install face-recognition 会尝试自动安装 dlib

常见错误与解决

编译错误dlib 的安装有时会因为缺少 C++ 编译工具链(如 Visual Studio Build Tools for Windows, CMake, 或 GCC on Linux/macOS)而失败。

  • Windows 用户:请确保安装了 Visual Studio Build Tools(选择 "C++ build tools" 工作负载)。同时,安装 CMake还可以选择安装已经编译好的whl文件 --> 项目地址

  • macOS 用户:安装 Xcode Command Line Tools (xcode-select --install) 和 CMake。

  • Linux 用户:安装 build-essential, cmake, python3-dev 等包(具体命令取决于你的Linux发行版,例如 sudo apt-get install build-essential cmake python3-dev)。

版本兼容性:不同版本的 face_recognitiondlib 之间可能存在兼容性问题。如果遇到 TypeError: compute_face_descriptor(): incompatible function arguments 这样的错误,通常意味着 dlib 的某个版本与 face_recognition 不匹配。此时,最佳解决方案是

  1. 卸载pip uninstall dlib face-recognition

  2. 重新安装pip install face-recognition (它会尝试安装兼容的 dlib 版本)

  • 如果问题依旧,可以尝试安装特定版本的 dlib,例如 pip install dlib==19.21.0 (具体版本可能需要根据 face_recognition 的版本进行调整)。

3. 项目文件结构

your_project_folder/
├── faces/                  # 存放注册人脸图片的文件夹
│   ├── person1/            # 文件夹名即为注册人名
│   │   ├── img1.jpg
│   │   ├── img2.png
│   │   └── ...
│   ├── person2/
│   │   ├── imgA.jpg
│   │   └── ...
│   └── ...
├── enroll_faces.py         # 用户注册脚本
├── main.py                 # 实时识别脚本
├── face_encodings.csv      # 注册后生成的人脸特征数据库
└── requirements.txt        # 依赖库列表 (可选,用于记录)

二、用户注册脚本

这个脚本是整个系统的“大脑”,它负责将每个用户的脸部信息转换为独一无二的“人脸指纹”,并将其永久保存起来。这就像为每个用户制作一张数字化的身份证。

1. 核心功能与解释

  • 扫描文件夹:脚本会自动遍历 faces 文件夹下的所有子文件夹,将每个文件夹名作为用户的姓名。

  • 特征提取:它会读取每个文件夹里的照片,使用 face_recognition 库提取出人脸的 128维特征向量。为了提高准确性,它会计算多张照片的平均特征向量。

  • 数据持久化:最终,脚本将 人名 和对应的 特征向量 一起保存到 face_encodings.csv 文件中,以便后续的识别程序使用。

CSV_FILE = 'face_encodings.csv': 定义了存储所有用户特征的 CSV 文件名。

FACES_DIR = 'faces': 指定了存放所有用户人脸图片的根目录。

generate_encodings() 函数:

  • 扫描目录: 遍历 FACES_DIR。每一个直接子文件夹名被当作一个用户的姓名(例如 faces/张三,则姓名为“张三”)。

  • 加载图片: 读取该用户文件夹下的所有 .png, .jpg, .jpeg 格式的图片。

  • 人脸检测与特征提取:

    • face_recognition.load_image_file(image_path): 加载单张图片。

    • face_recognition.face_locations(image, model="hog"): 检测图片中的所有人脸位置。model="hog" 是一个基于 HOG 特征的检测器,速度较快但可能不够精确;model="cnn"(需要 GPU 支持)通常更准确。

    • face_recognition.face_encodings(image, model="large"): 为检测到的人脸提取 128 维的特征向量。model="large" 使用更精确的模型。

    • 平均特征: 为了提高识别的鲁棒性,脚本会计算同一个用户多张照片特征向量的平均值,得到一个更稳定的“代表性”特征。

  • 保存特征: 将每个用户的姓名和其平均特征向量写入 face_encodings.csv 文件。

2. 完整代码

import face_recognition
import os
import numpy as np
import csv

CSV_FILE = 'face_encodings.csv'
FACES_DIR = 'faces'

def generate_encodings():
    print("正在扫描人脸图片...")
    known_face_encodings = []
    known_face_names = []

    if not os.path.exists(FACES_DIR):
        print(f"错误: 找不到 '{FACES_DIR}' 文件夹。请创建该文件夹并放入人脸图片。")
        return

    for name in os.listdir(FACES_DIR):
        person_dir = os.path.join(FACES_DIR, name)
        if os.path.isdir(person_dir):
            print(f"正在处理 {name} 的照片...")
            encodings_for_person = []
            
            for filename in os.listdir(person_dir):
                if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
                    image_path = os.path.join(person_dir, filename)
                    image = face_recognition.load_image_file(image_path)
                    
                    face_locations = face_recognition.face_locations(image, model="hog")
                    if not face_locations:
                        print(f"⚠️ {filename} 没检测到人脸")
                        continue
                    face_encodings = face_recognition.face_encodings(image, model="large")
                    
                    if face_encodings:
                        encodings_for_person.append(face_encodings[0])
            
            if encodings_for_person:
                average_encoding = np.mean(encodings_for_person, axis=0)
                known_face_encodings.append(average_encoding)
                known_face_names.append(name)
                print(f"已提取 {name} 的平均特征向量。")
            else:
                print(f"警告: 在 {name} 的文件夹中没有找到人脸。")

    with open(CSV_FILE, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(['name', 'encoding'])
        for name, encoding in zip(known_face_names, known_face_encodings):
            writer.writerow([name, list(encoding)])
    
    print(f"\n人脸特征已成功保存到 '{CSV_FILE}'。")

if __name__ == "__main__":
    generate_encodings()

三、实时识别脚本

这个脚本是门禁系统的“眼睛”,它会调用摄像头,实时捕捉画面中的人脸,并与我们之前注册好的“人脸指纹”进行比对。

1. 核心功能与解释

  • 加载特征:程序启动时,首先会从 face_encodings.csv 文件中加载所有已注册用户的姓名和特征向量。

  • 实时捕捉:它会持续从摄像头读取视频帧。

  • 人脸比对:对于每一帧画面中的人脸,程序会提取其特征,并与数据库中的所有特征进行比对,找出最相似的人名。

  • 中文显示:为了解决中文乱码问题,脚本使用 Pillow 库来绘制识别出的中文姓名。

CSV_FILE = 'face_encodings.csv': 指定了包含注册人脸特征的数据库文件。

load_encodings() 函数:

  • 读取 face_encodings.csv 文件,加载所有已注册用户的姓名和对应的 128 维特征向量。

  • ast.literal_eval() 是一个安全地解析字符串中 Python 字面量(如列表、字典、数字等)的函数,用于将 CSV 中存储的特征向量字符串转换回 NumPy 数组。

  • 如果文件不存在,会提示用户先运行 enroll_faces.py

draw_chinese_text() 函数:

  • 这个函数是专门用来在图片上绘制中文文本的。

  • 它接收一个 PIL 图像对象、要绘制的文本、文本位置、字体文件路径和字体大小。

  • ImageFont.truetype() 用于加载指定的 TTF 或 OTF 字体文件。encoding="utf-8" 确保了支持中文。

  • ImageDraw.Draw() 创建一个绘图对象,然后 draw.text() 在图像上绘制文本。

recognize_faces_in_video(font_path) 函数:

  • 加载数据库: 首先调用 load_encodings() 加载所有已知人脸数据。

  • 打开摄像头: cv2.VideoCapture(0) 打开默认摄像头。

  • 视频循环: 进入一个无限循环,逐帧处理视频流。

  • 帧缩放与颜色转换:

    • cv2.resize(frame, (0, 0), fx=0.25, fy=0.25): 将视频帧缩小到四分之一大小。这是为了显著提升识别速度,因为在小尺寸图像上进行人脸检测和特征提取更快。

    • cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB): OpenCV 默认读取的图像格式是 BGR,而 face_recognition 库需要 RGB 格式,所以需要进行颜色空间转换。

  • 人脸检测与特征提取:

    • face_recognition.face_locations(rgb_small_frame): 在缩小的 RGB 帧中检测人脸。

    • face_recognition.face_encodings(rgb_small_frame, face_locations, model='small'): 基于检测到的人脸位置,提取 128 维特征向量。model='small' 是一个更快的模型,适合实时视频处理。

  • 特征比对:

    • face_recognition.compare_faces(known_face_encodings, face_encoding): 将当前帧中提取的 face_encoding 与数据库中所有已知人脸的特征进行比对,返回一个布尔值列表(True 表示匹配)。

    • face_recognition.face_distance(known_face_encodings, face_encoding): 计算当前人脸特征与已知人脸特征之间的欧氏距离(距离越小越相似)。

    • np.argmin(face_distances): 找到距离最小(最相似)的那个已知人脸的索引。

    • 如果 compare_faces 返回 True(表示匹配成功),则使用该最相似用户的姓名;否则,姓名设为“未知人员”。

  • 绘制结果:

    • 缩放坐标: 由于之前缩放了图片,这里需要将检测到的人脸坐标乘以 4,恢复到原始帧的尺寸。

    • 绘制边界框: draw.rectangle() 在 PIL 图像上绘制人脸的边界框。

    • 绘制姓名标签: 使用 draw_chinese_text() 函数,将识别出的姓名绘制在人脸下方。

  • 显示视频:

    • cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR): 将 PIL 图像(RGB格式)转换回 OpenCV 格式(BGR),以便 cv2.imshow() 显示。

    • cv2.imshow('Result', cv2_image): 在窗口中显示处理后的视频帧。

    • cv2.waitKey(30) & 0xFF == ord('q'): 每 30 毫秒检查一次键盘输入。如果按下 'q' 键,则退出循环。

  • 资源释放: video_capture.release()cv2.destroyAllWindows() 用于释放摄像头资源并关闭所有 OpenCV 窗口。

2. 完整代码

import face_recognition
import cv2
import numpy as np
import csv
import ast
import os
from PIL import Image, ImageDraw, ImageFont

# 存储人名和人脸特征向量的 CSV 文件
CSV_FILE = 'face_encodings.csv'

def load_encodings():
    """从 CSV 文件加载人名和人脸特征向量"""
    known_face_encodings = []
    known_face_names = []
    try:
        with open(CSV_FILE, 'r', encoding='utf-8') as f:
            reader = csv.reader(f)
            # 跳过表头
            next(reader)
            for row in reader:
                name = row[0]
                # ast.literal_eval 将字符串列表转换为实际的列表
                encoding = np.array(ast.literal_eval(row[1]))
                known_face_names.append(name)
                known_face_encodings.append(encoding)
        print(f"已从 '{CSV_FILE}' 文件加载 {len(known_face_names)} 个已知人脸特征。")
        return known_face_encodings, known_face_names
    except FileNotFoundError:
        print(f"错误: 找不到 '{CSV_FILE}' 文件。请先运行 'enroll_face.py'。")
        return [], []

def draw_chinese_text(image, text, position, font_path, font_size=20):
    """
    使用 Pillow 在图片上绘制中文文本
    :param image: PIL 图像对象
    :param text: 待绘制的文本
    :param position: 文本绘制位置 (x, y)
    :param font_path: 中文字体文件路径
    :param font_size: 字体大小
    """
    try:
        font = ImageFont.truetype(font_path, font_size, encoding="utf-8")
    except IOError:
        print(f"警告: 找不到字体文件 {font_path},使用默认字体。")
        font = ImageFont.load_default()

    draw = ImageDraw.Draw(image)
    draw.text(position, text, font=font, fill=(255, 255, 255))

def recognize_faces_in_images(image_paths, font_path, save=False, show=True):
    """
    在单张或多张图片中进行人脸识别
    :param image_paths: 图片文件路径列表
    :param font_path: 中文字体文件路径
    """
    known_face_encodings, known_face_names = load_encodings()
    
    if not known_face_encodings:
        return

    for image_path in image_paths:
        if not os.path.exists(image_path):
            print(f"警告: 找不到图片文件: {image_path}")
            continue
            
        print(f"\n正在处理图片: {image_path}")
        image = face_recognition.load_image_file(image_path)
        
        face_locations = face_recognition.face_locations(image)
        face_encodings = face_recognition.face_encodings(image, face_locations)
        
        # 将 RGB 格式的 numpy 数组转换为 PIL 图像对象
        pil_image = Image.fromarray(image)
        draw = ImageDraw.Draw(pil_image)

        for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
            matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
            name = "未知人员"
            
            face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
            best_match_index = np.argmin(face_distances)
            if matches[best_match_index]:
                name = known_face_names[best_match_index]
            
            print(f"在 {image_path} 中识别到人脸: {name}")

            # 在 PIL 图像上绘制方框
            draw.rectangle([(left, top), (right, bottom)], outline=(220, 110, 10), width=2)
            draw.rectangle([(left, bottom - 35), (right, bottom)], fill=(220, 110, 10))

            # 绘制姓名标签
            draw_chinese_text(pil_image, name, (left + 6, bottom - 30), font_path, font_size=25)
            
        # 将 PIL 图像转换回 numpy 数组以供 OpenCV 显示
        cv2_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)

        if show:
            cv2.imshow("Result", cv2_image)
            cv2.waitKey(0)  # 按任意键关闭图片窗口
            cv2.destroyAllWindows()

        if save:
            basename = os.path.splitext(image_path)[0]
            save_path = os.path.join(os.path.dirname(image_path), basename +"_recognized.png")
            cv2.imwrite(save_path, cv2_image)

def recognize_faces_in_video(font_path):
    """
    实时人脸识别
    :param font_path: 中文字体文件路径
    """
    known_face_encodings, known_face_names = load_encodings()
    
    if not known_face_encodings:
        return

    video_capture = cv2.VideoCapture(0)
    
    print("\n人脸识别系统已启动,按 'q' 退出...")
    
    while True:
        ret, frame = video_capture.read()
        
        small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
        small_frame = small_frame.astype(np.uint8)
        rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)
        
        face_locations = face_recognition.face_locations(rgb_small_frame)
        face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations, model='small')

        face_names = []
        for face_encoding in face_encodings:
            matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
            name = "未知人员"
            
            face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
            best_match_index = np.argmin(face_distances)
            if matches[best_match_index]:
                name = known_face_names[best_match_index]
            
            face_names.append(name)

        # 将 OpenCV 图像(BGR)转换为 PIL 图像(RGB)
        pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_image)

        for (top, right, bottom, left), name in zip(face_locations, face_names):
            top *= 4
            right *= 4
            bottom *= 4
            left *= 4
            
            draw.rectangle([(left, top), (right, bottom)], outline=(220, 110, 10), width=2)
            draw.rectangle([(left, bottom - 35), (right, bottom)], fill=(220, 110, 10))
            
            draw_chinese_text(pil_image, name, (left + 6, bottom - 30), font_path, font_size=25)

        # 将 PIL 图像转换回 OpenCV 格式以供显示
        cv2_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
        cv2.imshow('Result', cv2_image)
        
        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    video_capture.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    # --- 配置你的中文字体路径 ---
    # Windows 用户:
    CHINESE_FONT_PATH = 'C:/Windows/Fonts/simhei.ttf'
    # macOS 用户:
    # CHINESE_FONT_PATH = '/System/Library/Fonts/STHeiti Light.ttc'
    # Linux 用户:
    # CHINESE_FONT_PATH = '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc'

    # --- 示例用法 ---
    
    # 示例1: 对单张图片进行人脸识别
    # 将你的图片文件放在与此脚本同级的目录中
    recognize_faces_in_images(['./test.png'], font_path=CHINESE_FONT_PATH, save=True)
    
    # 示例2: 实时视频流人脸识别
    # recognize_faces_in_video(font_path=CHINESE_FONT_PATH)

四、代码运行与演示

已从 'face_encodings.csv' 文件加载 2 个已知人脸特征。

正在处理图片: ./test.png
在 ./test.png 中识别到人脸: 刘亦菲

五、总结

通过这两个脚本,我们成功搭建了一个基础的人脸识别门禁系统。enroll_faces.py 负责数据录入和特征生成,main.py 则负责实时视频流的处理和人脸匹配。Pillow 库的引入,保证了中文姓名的正确显示,使得系统更加友好和专业。

希望这个详细的教程能帮助你深入理解并成功实现你的人脸识别项目!如果你在实现过程中遇到任何问题,欢迎在评论区交流。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐