学习笔记--MTCNN人脸数据采集实现

概述

这是一个基于MTCNN(Multi-task Cascaded Convolutional Networks)算法的人脸捕获程序,用于收集训练人脸识别模型所需的人脸图像数据。程序使用facenet-pytorch库实现的MTCNN检测器。

思维导图

核心功能

1. **人脸检测**:使用MTCNN算法检测图像中的人脸

2. **图像捕获**:自动或手动捕获人脸图像

3. **中心区域限制**:限定人脸检测在画面中心区域,提高检测准确性

4. **中文路径支持**:使用imencode方法解决OpenCV对中文路径的支持问题

5. **交互式操作**:通过键盘控制图像捕获过程

技术要点

1.MTCNN人脸检测

MTCNN是一种多任务级联卷积神经网络,包含三个阶段:

1. P-Net(Proposal Network):快速生成人脸候选区域

2. R-Net(Refine Network):过滤掉大量非人脸区域

3. O-Net(Output Network):输出人脸关键点和更精确的人脸边界框

def create_mtcnn_detector():
    """
    创建MTCNN人脸检测器,使用facenet-pytorch实现
    
    Returns:
        MTCNN检测器对象或None(如果无法创建)
    """
    try:
        # 使用facenet-pytorch实现的MTCNN
        from facenet_pytorch import MTCNN
        # 使用CPU模式,keep_all=True表示检测所有 faces
        mtcnn = MTCNN(keep_all=True, device='cpu')
        print("使用facenet-pytorch的MTCNN实现")
        return mtcnn
    except ImportError:
        print("错误: 未找到facenet-pytorch库,请安装:")
        print("  pip install facenet-pytorch")
        return None

'''
'''
# 使用MTCNN检测人脸
# 返回boxes, probs
boxes, probs = face_detector.detect(image)

2.中文路径处理

为了解决OpenCV对中文路径支持不佳的问题,程序使用以下方法:

# 使用imencode和tofile方法保存包含中文的路径
# cv2.imencode(保存格式, 保存图片)[1].tofile(保存路径)
cv2.imencode('.jpg', face_img)[1].tofile(filepath)
# 使用isfile方法获取保存文件状态是否成功
success = os.path.isfile(filepath)

另注:关于在cv2的putText方法中使用中文会出现乱码的问题,可以通过以下方法解决

# 安装这个扩展之后puttext的中文显示问题就解决啦
# 但是文件中文名写入不使用上面的imencode方法还是会显示乱码
pip install opencv-python-rolling

3.边界框格式处理

MTCNN返回的边界框可能有两种格式:

1. `(x1, y1, x2, y2)`:矩形对角点坐标

2. `(x, y, w, h)`:左上角坐标和宽高

# 使用坐标比较判断边界框格式
# 如果是(x1, y1, x2, y2)格式,其中x2 > x1且y2 > y1
if box[2] > box[0] and box[3] > box[1]:
    x1, y1, x2, y2 = box
    w, h = x2 - x1, y2 - y1
    faces.append((int(x1), int(y1), int(w), int(h)))
else:
    # 可能已经是(x, y, w, h)格式
    faces.append((int(box[0]), int(box[1]), int(box[2]), int(box[3])))

思路:

实际上,很难通过返回的boxes的长度来判断格式,因为不同的库和版本可能返回不同的格式。

理想的方法是明确知道所使用的库和版本,并参考其文档来确定返回的边界框格式,但程序运行在不同的环境中,一般难以确定使用的库和版本。

根据坐标值的大小来判断?

如果boxes的长度为4且所有值均为正数且x2 > x1且y2 > y1,

不能判断为(x1, y1, x2, y2)格式的主要原因是(x, y, w, h)格式也可能满足这些条件。

比如:人脸贴摄像头比较近的时候,w和h可能会比较大,此时w和h的值可能会大于x1和y1。

探究靠得太近的本质,在于x,y的坐标,也就是左上角的坐标,距离边界的距离太小。

那么该如何控制人脸框和边界的距离大小呢?

有丰富生活经验的朋友应该已经想到了,就是人脸识别的时候经常会出现的人脸框。

在反馈的图像中标划出一个人脸区域,然后让用户调整摄像头位置,使得人脸框和边界的距离适中。这样就可以避免人脸框过大或过小的问题,同时也能确保人脸框的格式正确。

4.中心区域检测

# 定义中心检测区域(画面中心宽30%,高80%的区域)
# 计算中心区域的坐标
center_x, center_y = frame_width // 2, frame_height // 2
# 计算中心区域的宽度和高度
# 这里使用30%的宽度和80%的高度
crop_width, crop_height = int(frame_width * 0.3), int(frame_height * 0.8)
# 获取中心区域四个角坐标
crop_x1 = center_x - crop_width // 2
crop_y1 = center_y - crop_height // 2
crop_x2 = crop_x1 + crop_width
crop_y2 = crop_y1 + crop_height

这里使用30%的宽度和80%的高度的主要原因

1.人脸是个竖立的椭圆,需要的人脸框width小height大。

2.想要区分(x1, y1, x2, y2)和(x, y, w, h)格式,实际上只需要保证w总小于x1就够了。

因此,高度的限制可以放宽成80%H,宽度则严格设定为30%W,以此保证它始终小于x1的min值35%W。

项目流程图

项目代码

由于是学习代码,注释写得比较细琐,跟着debug一遍基本上都能理解了,大概。

import cv2
import os
import sys
import numpy as np

def create_mtcnn_detector():
    """
    创建MTCNN人脸检测器,使用facenet-pytorch实现
    
    Returns:
        MTCNN检测器对象或None(如果无法创建)
    """
    try:
        # 使用facenet-pytorch实现的MTCNN
        from facenet_pytorch import MTCNN
        # 使用CPU模式,keep_all=True表示检测所有 faces
        mtcnn = MTCNN(keep_all=True, device='cpu')
        print("使用facenet-pytorch的MTCNN实现")
        return mtcnn
    except ImportError:
        print("错误: 未找到facenet-pytorch库,请安装:")
        print("  pip install facenet-pytorch")
        return None

def capture_faces(name, num_images=50, camera_id=0, output_dir="known_faces"):
    """
    使用MTCNN捕获人脸图像用于训练人脸识别模型,在画面中心区域检测人脸
    
    Args:
        name (str): 人员姓名,将用作保存图像文件名的前缀
        num_images (int): 要捕获的图像数量,默认为50张
        camera_id (int): 摄像头设备ID,默认为0(第一个摄像头)
        output_dir (str): 图像保存的目录路径,默认为"known_faces"
    """
    # 创建MTCNN检测器
    face_detector = create_mtcnn_detector()
    if face_detector is None:
        return
    
    
    print(f"开始捕获人脸图像: {name}")
    print(f"目标数量: {num_images} 张")
    print(f"输出目录: {output_dir}")
    print("=" * 50)
    
    
    # 初始化摄像头设备
    cap = cv2.VideoCapture(camera_id)
    if not cap.isOpened():
        print(f"错误: 无法打开摄像头 {camera_id}")
        # 尝试其他设备ID (1-4),在某些系统中摄像头可能分配了不同的ID
        for i in range(1, 5):
            print(f"尝试摄像头 {i}...")
            cap = cv2.VideoCapture(i)
            if cap.isOpened():
                print(f"成功打开摄像头 {i}")
                camera_id = i
                break
        if not cap.isOpened():
            print("无法打开任何摄像头")
            return
    
    # 设置摄像头分辨率,提高图像质量
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
    
    # 初始化计数器
    # captured_count: 已成功捕获并保存的图像数量
    # frame_count: 处理的视频帧总数,用于控制自动捕获频率
    captured_count = 0
    frame_count = 0
    
    print("\n操作说明:")
    print("- 确保人脸正对摄像头")
    print("- 保持不同角度和表情")
    print("- 确保光线充足且均匀")
    print("- 按 'q' 或 'ESC' 键退出")
    print("- 按 'c' 键捕获当前帧中的人脸")
    print("- 自动捕获模式下会自动保存检测到的人脸")
    print("- 人脸应出现在画面中心区域(红色框内)")
    print("\n准备开始...")
    
    # 等待2秒让用户准备
    cv2.waitKey(2000)
    
    try:
        # 主循环:持续从摄像头读取帧并检测人脸
        while True:
            # 从摄像头读取一帧图像
            ret, frame = cap.read()
            if not ret:
                print("无法获取摄像头画面")
                break
            
            # 帧计数器递增
            frame_count += 1
            
            # 获取帧的尺寸
            frame_height, frame_width = frame.shape[:2]
            
            # 定义中心检测区域(画面中心宽30%,高80%的区域)
            # 计算中心区域的坐标
            center_x, center_y = frame_width // 2, frame_height // 2
            # 计算中心区域的宽度和高度
            # 这里使用30%的宽度和80%的高度
            crop_width, crop_height = int(frame_width * 0.3), int(frame_height * 0.8)
            # 获取中心区域四个角坐标
            crop_x1 = center_x - crop_width // 2
            crop_y1 = center_y - crop_height // 2
            crop_x2 = crop_x1 + crop_width
            crop_y2 = crop_y1 + crop_height
            
            
            # 使用MTCNN检测人脸(仅在中心区域)
            faces = detect_faces_mtcnn(face_detector, frame)
            
            
            # 在显示帧上绘制中心检测区域(红色框)
            display_frame = frame.copy()
            cv2.rectangle(display_frame, (crop_x1, crop_y1), (crop_x2, crop_y2), (0, 0, 255), 2)
            cv2.putText(display_frame, "人脸检测区域", (crop_x1, crop_y1 - 10), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
            
            # 在显示帧上绘制绿色矩形框标记检测到的人脸
            for (x, y, w, h) in faces:
                cv2.rectangle(display_frame, (int(x), int(y)), (int(x+w), int(y+h)), (0, 255, 0), 2)
            
            # 在显示帧上添加文字信息
            cv2.putText(display_frame, f"已捕获: {captured_count}/{num_images}", 
                       (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
            cv2.putText(display_frame, f"按 'c' 捕获, 'q' 退出", 
                       (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
            
            # 显示处理后的帧
            cv2.imshow('人脸捕获 (MTCNN-中心区域)', display_frame)
            
            # 自动捕获模式(每30帧检查一次,且至少检测到一个人脸)
            # 这样可以避免保存过多重复的人脸图像
            if frame_count % 30 == 0 and len(faces) > 0 and captured_count < num_images:
                # 遍历这一帧中的所有检测到的人脸
                for i, (x, y, w, h) in enumerate(faces):
                    # 如果已达到目标数量则停止捕获
                    if captured_count >= num_images:
                        break
                    #检测采集的人脸图像是否在中心区域
                    if not (crop_x1 <= x <= crop_x2 and crop_y1 <= y <= crop_y2):
                        print(f"人脸不在中心区域内,跳过保存")
                        break

                    # 提取人脸区域(裁剪出人脸部分)
                    face_img = frame[int(y):int(y+h), int(x):int(x+w)]
                    
                    # 生成文件名,格式为姓名+编号,如GUO01.jpg, GUO02.jpg
                    filename = f"{name.upper()}{captured_count+1:02d}.jpg"
                    filepath = os.path.join(output_dir, filename)
                    
                    # 保存图像到指定目录
                    #success = cv2.imwrite(filepath, face_img)
                    cv2.imencode('.jpg', face_img)[1].tofile(filepath)
                    success = os.path.isfile(filepath)
                    if success:
                        captured_count += 1
                        print(f"已保存: {filename} (共{captured_count}张)")
                    else:
                        print(f"保存失败: {filename}")
            
            # 等待按键输入(1毫秒)
            key = cv2.waitKey(1) & 0xFF
            
            # 按 'q' 或 ESC 键退出程序
            if key == ord('q') or key == 27:  # 27 is ESC key
                break
            
            # 按 'c' 键手动捕获当前帧中的人脸(立即保存当前检测到的人脸)
            elif key == ord('c') and len(faces) > 0:
                # 遍历这一帧中的所有检测到的人脸
                for i, (x, y, w, h) in enumerate(faces):
                    # 如果已达到目标数量则停止捕获
                    if captured_count >= num_images:
                        break
                    #检测采集的人脸图像是否在中心区域
                    if not (crop_x1 <= x <= crop_x2 and crop_y1 <= y <= crop_y2):
                        print(f"人脸不在中心区域内,跳过保存")
                        break

                    # 提取人脸区域
                    face_img = frame[int(y):int(y+h), int(x):int(x+w)]
                    
                    # 生成文件名
                    filename = f"{name.upper()}{captured_count+1:02d}.jpg"
                    filepath = os.path.join(output_dir, filename)
                    
                    # 保存图像
                    #success = cv2.imwrite(filepath, face_img)
                    cv2.imencode('.jpg', face_img)[1].tofile(filepath)
                    success = os.path.isfile(filepath)
                    # 检查保存是否成功
                    if success:
                        captured_count += 1
                        print(f"已保存: {filename} (共{captured_count}张)")
                    else:
                        print(f"保存失败: {filename}")
            
            # 检查是否达到目标数量
            if captured_count >= num_images:
                print(f"\n已完成! 成功捕获 {captured_count} 张人脸图像")
                break
    
    except KeyboardInterrupt:
        # 处理用户按Ctrl+C中断程序的情况
        print("\n用户中断程序")
    finally:
        # 释放摄像头资源并关闭所有OpenCV窗口
        cap.release()
        cv2.destroyAllWindows()
    
    # 输出最终统计信息
    print(f"\n捕获完成!")
    print(f"目标数量: {num_images}")
    print(f"实际捕获: {captured_count}")
    print(f"保存位置: {os.path.abspath(output_dir)}")

def detect_faces_mtcnn(face_detector, image):
    """
    使用MTCNN检测图像中的人脸
    
    Args:
        face_detector: MTCNN人脸检测器对象
        image: 要检测的图像
        
    Returns:
        list: 人脸边界框列表,每个元素为(x, y, w, h)格式
    """
    try:
        # 使用MTCNN检测人脸
        # 返回boxes, probs
        boxes, probs = face_detector.detect(image)
        
        # 如果没有检测到人脸
        if boxes is None or len(boxes) == 0:
            return []
            
        # 确保boxes是numpy数组
        if not isinstance(boxes, np.ndarray):
            boxes = np.array(boxes)
            
        # 如果是三维数组,取第一个元素
        if boxes.ndim == 3:
            boxes = boxes[0]
            
        # 转换边界框格式为(x, y, w, h)
        faces = []
        for box in boxes:
            if len(box) == 4:
                # 使用坐标比较判断边界框格式
                # 如果是(x1, y1, x2, y2)格式,其中x2 > x1且y2 > y1
                if box[2] > box[0] and box[3] > box[1]:
                    x1, y1, x2, y2 = box
                    w, h = x2 - x1, y2 - y1
                    faces.append((int(x1), int(y1), int(w), int(h)))
                else:
                    # 可能已经是(x, y, w, h)格式
                    faces.append((int(box[0]), int(box[1]), int(box[2]), int(box[3])))
            elif len(box) == 5:
                # 有些版本可能返回5个值,前4个是边界框
                x1, y1, x2_or_w, y2_or_h = box[:4]
                # 使用同样的判断逻辑
                if x2_or_w > x1 and y2_or_h > y1:
                    w, h = x2_or_w - x1, y2_or_h - y1
                    faces.append((int(x1), int(y1), int(w), int(h)))
                else:
                    faces.append((int(x1), int(y1), int(x2_or_w), int(y2_or_h)))
                
        return faces
    except Exception as e:
        print(f"人脸检测出错: {e}")
        return []

def get_user_input():
    """
    获取用户输入的姓名和捕获数量
    
    Returns:
        tuple: (姓名, 数量) 或 (None, None)(输入格式错误时)
    """
    # 提示用户输入姓名和数量
    user_input = input("请输入姓名,并指定捕获数量。示例格式:GUO+50\n")
    try:
        # 解析用户输入,按"+"分割姓名和数量
        name, count_str = user_input.split('+')
        count = int(count_str)
        return name.strip(), count
    except ValueError:
        # 处理输入格式错误的情况
        print("输入格式错误,请使用格式:姓名+数量,例如:GUO+50")
        return None, None

def main():
    """
    主函数:获取用户输入并调用捕获人脸图像功能
    """
    print("人脸图像捕获工具 (MTCNN版本-中心区域检测)")
    print("=" * 40)
    
    # 获取用户输入的姓名和数量
    name, count = get_user_input()
    if name is None or count is None:
        return
    
    # 设置输出目录为 data/name_face 格式,如 data/GUO_face/
    output_dir = os.path.join("data", f"{name}_face")
    # 创建输出目录,如果目录不存在则创建
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"创建目录: {output_dir}")
    
    # 调用捕获人脸图像的函数
    capture_faces(name, count, 0, output_dir)

if __name__ == "__main__":
    # 程序入口点
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值