机器视觉:基于MTCNN与Caffe模型的人脸性别年龄统计系统实现

MTCNN 检测 + Caffe 性别/年龄模型批量统计人脸属性,输出性别与年龄段分布、平均年龄及多脸图片路径报告。


一、前言

在计算机视觉领域,人脸属性分析(如性别识别、年龄估算)是重要的研究方向,广泛应用于智能监控、用户画像构建、零售客流分析等场景。传统的人脸属性分析方案常面临检测精度低、模型部署复杂等问题,尤其是在非受控环境下的人脸检测效果往往不尽如人意。
最近在进行数据集规模统计的工作,在上一章进行人脸识别与聚类去除重复Speaker之后,进一步工作是统计样本人脸的性别与年龄分布数据,于是做了一套这样的系统。
本文将对该系统进行详尽的介绍并附上源码,该系统通过MTCNN(Multi-Task Cascaded CNN) 实现高精度人脸检测,结合预训练的Caffe模型完成性别识别与年龄估算,并最终生成可视化统计报告。系统支持批量处理图片文件夹中的所有图像,自动统计人脸总数、性别分布、年龄段分布及估算平均年龄,同时记录包含多张人脸的图片路径,为后续分析提供数据支撑。无论是用于个人学习计算机视觉技术,还是企业级的客流属性分析需求,这套系统都具备良好的实用性和可扩展性。

二、模型介绍

本系统采用“人脸检测 + 属性预测”的两阶段架构,分别使用MTCNN和Caffe预训练模型完成对应任务,两种模型在各自领域均具备成熟、高效的特点。

2.1 MTCNN:高精度人脸检测模型

MTCNN(Multi-Task Cascaded Convolutional Neural Networks)是2016年提出的多任务级联CNN模型,核心优势在于同时优化人脸检测、人脸关键点定位两个任务,在保证检测速度的同时大幅提升了非受控环境下的检测精度。

  • 核心特点

    1. 级联结构:分为P-Net(Proposal Network)、R-Net(Refine Network)、O-Net(Output Network)三阶段,逐步筛选和优化人脸框,减少误检与漏检。
    2. 多任务学习:在检测人脸的同时预测5/68个人脸关键点(双眼、鼻尖、两角嘴角),为后续人脸对齐提供支持(本系统暂未用到关键点,但保留扩展能力)。
    3. 轻量级:模型参数少,推理速度快,适合批量图片处理场景。
  • 本系统应用:替代传统的OpenCV DNN人脸检测器,解决侧脸、遮挡、小尺寸人脸检测效果差的问题,检测置信度阈值设为0.6(可根据需求调整)。

2.2 Caffe预训练模型:性别与年龄预测

性别和年龄预测采用两个独立的Caffe预训练模型,均基于经典的CNN架构设计,在公开数据集(如IMDB-WIKI)上训练得到,具备良好的泛化能力。

模型类型输入要求输出结果核心用途
性别预测模型227×227×3 RGB图像(需减去均值)二分类概率(Female/Male)判断人脸性别
年龄预测模型227×227×3 RGB图像(需减去均值)8个年龄段概率估算人脸所属年龄段
  • 年龄分段说明:模型将年龄划分为8个区间,每个区间对应一个“中间值”用于计算平均年龄,具体映射如下:
    • 年龄段:(0-2) → 中间值1;(4-6) → 中间值5;(8-12) → 中间值10
    • (15-20) → 中间值18;(25-32) → 中间值28;(38-43) → 中间值40
    • (48-53) → 中间值50;(60-100) → 中间值70

三、模型原理解释

3.1 MTCNN人脸检测原理

MTCNN通过三阶段级联网络逐步优化人脸检测结果,每一步都基于前一步的输出进行更精细的处理,具体流程如下:

  1. P-Net(Proposal Network)

    • 输入:原始图片(通过图像金字塔生成不同尺度的图片)。
    • 功能:快速生成大量“候选人脸框”,同时预测每个候选框的置信度(是否为人脸)和边界框回归值(用于调整框的位置)。
    • 输出:经过置信度筛选和非极大值抑制(NMS)后的候选人脸框。
  2. R-Net(Refine Network)

    • 输入:P-Net输出的候选人脸框( resize 为24×24)。
    • 功能:进一步筛选错误的候选框,修正边界框位置,提升检测精度。
    • 输出:再次经过置信度筛选和NMS后的人脸框,数量比P-Net大幅减少。
  3. O-Net(Output Network)

    • 输入:R-Net输出的人脸框( resize 为48×48)。
    • 功能:最终确认人脸框,输出更高精度的边界框回归值和5个人脸关键点坐标。
    • 输出:最终的人脸检测框(本系统仅使用框坐标,关键点可用于后续扩展)。

通过这种“粗筛→精筛→最终确认”的流程,MTCNN既能保证检测速度,又能有效避免误检和漏检,尤其适合复杂场景下的人脸检测。

3.2 Caffe模型属性预测原理

性别和年龄预测模型均基于CNN的特征提取与分类逻辑,核心流程一致,仅在输出层的任务定义上有所区别:

通用流程(性别/年龄预测):

  1. 图像预处理

    • 将MTCNN检测到的人脸框裁剪为独立的“人脸图像”,并 resize 为227×227(模型要求输入尺寸)。
    • 减去像素均值:对RGB三个通道分别减去固定均值(78.426337, 87.768914, 114.895847),消除光照变化对模型的影响。
    • 生成Blob:将处理后的图像转换为Caffe模型要求的Blob格式(批量维度×通道维度×高度×宽度)。
  2. 特征提取

    • 通过多层卷积层(Convolution)、激活函数(如ReLU)和池化层(Pooling)提取人脸图像的高层特征。
    • 例如:卷积层通过卷积核捕捉边缘、纹理等低级特征,深层卷积层则整合低级特征形成“人脸部件”(如眼睛、鼻子)等高级特征。
  3. 属性预测

    • 性别预测:输出层为2个神经元(对应Female和Male),采用Softmax激活函数,输出两个类别的概率,概率最大的类别即为预测性别。
    • 年龄预测:输出层为8个神经元(对应8个年龄段),同样采用Softmax激活函数,概率最大的神经元对应的年龄段即为预测结果,再通过“中间值”映射用于平均年龄计算。

四、代码解释

4.1 整体结构

代码分为路径配置、模型加载、核心功能函数、主流程五个模块,以下按模块逐一解析:

4.2 路径配置与依赖导入

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 人脸检测库
from tqdm import tqdm  # 进度条库

# -------- 路径配置 --------
THIS_DIR = os.path.dirname(__file__)  # 当前脚本所在目录
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))  # 项目根目录
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')  # 性别/年龄模型目录
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')  # 待处理图片目录
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性别及年龄统计.txt')  # 结果输出文件

# -------- 模型文件路径 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')  # 年龄模型配置文件
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')  # 年龄模型权重
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')  # 性别模型配置文件
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')  # 性别模型权重

# 年龄分段与中间值配置
AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用于计算平均年龄
GENDER_LIST = ['Female', 'Male']  # 性别选项

# 全局MTCNN检测器(避免重复初始化,提升效率)
mtcnn_detector = MTCNN()
  • 核心作用:定义项目目录结构、模型文件路径、业务配置(年龄分段),并初始化全局MTCNN检测器(减少重复创建实例的开销)。
  • 依赖说明:需提前安装mtcnnpip install mtcnn)、opencv-pythonpip install opencv-python)、tqdmpip install tqdm)等库。

4.3 模型加载函数(load_models

def load_models():
    # 仅加载年龄/性别模型;人脸检测改为全局MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一个位置返回None,兼容旧调用逻辑
  • 功能:通过OpenCV的dnn模块加载Caffe格式的年龄和性别模型。
  • 兼容性处理:返回值第一个位置为None(原逻辑中用于返回OpenCV人脸检测模型,现替换为MTCNN,保留返回格式避免报错)。

4.4 人脸检测函数(detect_faces

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用MTCNN进行人脸检测
    参数:
        face_net: 兼容旧参数(现未使用)
        img: 输入图像(BGR格式,OpenCV默认读取格式)
        conf_thresh: 检测置信度阈值
    返回:
        boxes: 人脸框列表,格式为[(x1,y1,x2,y2), ...](左上角(x1,y1),右下角(x2,y2))
    """
    results = mtcnn_detector.detect_faces(img)  # MTCNN检测结果
    boxes = []
    h, w = img.shape[:2]  # 图像高度和宽度
    for r in results:
        confidence = r.get('confidence', 0)  # 检测置信度
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']  # MTCNN返回的框:x,y为左上角坐标,w_box/h_box为宽高
            # 修正框坐标(避免超出图像边界)
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            # 确保框为有效区域(宽高均大于0)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes
  • 核心逻辑:调用MTCNN检测器获取人脸检测结果,过滤低置信度(<0.6)的框,并修正坐标避免超出图像边界。
  • 参数兼容face_net参数保留(原逻辑中用于传递OpenCV人脸检测模型),现未使用,确保旧代码调用不报错。

4.5 性别年龄预测函数(predict_age_gender

def predict_age_gender(age_net, gender_net, face_img):
    """
    预测单张人脸的性别和年龄
    参数:
        age_net: 年龄预测模型
        gender_net: 性别预测模型
        face_img: 裁剪后的人脸图像(BGR格式)
    返回:
        gender: 预测性别(Female/Male)
        age: 预测年龄段(如(25-32))
        age_mid: 年龄段中间值(如28)
    """
    # 图像预处理:转换为Blob格式并减去均值
    blob = cv2.dnn.blobFromImage(
        face_img, 
        1.0,  # 缩放因子
        (227, 227),  # 模型输入尺寸
        (78.426337, 87.768914, 114.895847),  # 各通道均值(BGR顺序)
        swapRB=False  # 无需交换R和B通道(OpenCV读取为BGR,模型要求BGR)
    )
    
    # 性别预测
    gender_net.setInput(blob)  # 设置模型输入
    gender_pred = gender_net.forward()  # 推理得到预测结果
    gender = GENDER_LIST[gender_pred[0].argmax()]  # 取概率最大的类别
    
    # 年龄预测
    age_net.setInput(blob)  # 设置模型输入
    age_pred = age_net.forward()  # 推理得到预测结果
    age_idx = age_pred[0].argmax()  # 取概率最大的年龄段索引
    age = AGE_LIST[age_idx]  # 年龄段标签
    age_mid = AGE_MID[age_idx]  # 年龄段中间值
    
    return gender, age, age_mid
  • 关键步骤
    1. blobFromImage:将人脸图像转换为模型可接受的Blob格式,同时完成缩放、尺寸调整和均值减法。
    2. 模型推理:通过setInput设置输入,forward执行推理,得到预测概率分布。
    3. 结果解析:通过argmax获取概率最大的类别索引,映射为性别标签和年龄段。

4.6 图片遍历函数(iter_images

def iter_images(root):
    """
    遍历目录下所有支持的图片文件(递归遍历子目录)
    参数:
        root: 根目录路径
    返回:
        图片文件路径生成器
    """
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}  # 支持的图片格式
    for dirpath, _, filenames in os.walk(root):  # 递归遍历目录
        for f in filenames:
            # 检查文件后缀是否在支持的格式中(不区分大小写)
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)  # 返回完整文件路径
  • 功能:递归遍历指定目录下的所有图片文件,仅保留支持的格式(JPG、PNG等),通过生成器(yield)减少内存占用。

4.7 主函数(main

主函数是系统的核心流程入口,负责串联所有模块,完成“图片加载→人脸检测→属性预测→统计分析→结果输出”的全流程:

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 现在恒为 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 预收集所有图片路径用于进度条
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何图片,目录:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='处理图片', unit='张'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 单张失败不影响整体
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("统计时间: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"扫描图片数: {total_images}")
    lines.append(f"检测到人脸总数: {total_faces}")
    lines.append("性别统计:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年龄段统计:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年龄: {avg_age}")
    lines.append("(文件级人脸计数仅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("统计完成,结果写入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

4.8 完整代码

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 新增:使用与 face_check.py 相同的 MTCNN 检测
from tqdm import tqdm  # 进度条

# -------- 路径配置 --------
THIS_DIR = os.path.dirname(__file__)
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性别及年龄统计.txt')

# -------- 模型文件 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')
# 下面两个(OpenCV DNN人脸检测)已不再使用,保留不报错;如需可删除
# FACE_PROTO = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector.pbtxt')
# FACE_MODEL = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector_uint8.pb')

AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用于估算平均年龄
GENDER_LIST = ['Female', 'Male']
# 新增:全局 MTCNN 检测器,与 face_check.py 保持一致
mtcnn_detector = MTCNN()

def load_models():
    # 仅加载年龄/性别模型;人脸检测改为全局 MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一个位置兼容旧调用

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用与 face_check.py 相同的 MTCNN 进行检测
    返回: [(x1,y1,x2,y2), ...]
    """
    results = mtcnn_detector.detect_faces(img)
    boxes = []
    h, w = img.shape[:2]
    for r in results:
        confidence = r.get('confidence', 0)
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes

def predict_age_gender(age_net, gender_net, face_img):
    blob = cv2.dnn.blobFromImage(face_img, 1.0, (227, 227), (78.426337, 87.768914, 114.895847), swapRB=False)
    gender_net.setInput(blob)
    gender_pred = gender_net.forward()
    gender = GENDER_LIST[gender_pred[0].argmax()]
    age_net.setInput(blob)
    age_pred = age_net.forward()
    age_idx = age_pred[0].argmax()
    age = AGE_LIST[age_idx]
    age_mid = AGE_MID[age_idx]
    return gender, age, age_mid

def iter_images(root):
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}
    for dirpath, _, filenames in os.walk(root):
        for f in filenames:
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 现在恒为 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 预收集所有图片路径用于进度条
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何图片,目录:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='处理图片', unit='张'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 单张失败不影响整体
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("统计时间: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"扫描图片数: {total_images}")
    lines.append(f"检测到人脸总数: {total_faces}")
    lines.append("性别统计:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年龄段统计:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年龄: {avg_age}")
    lines.append("(文件级人脸计数仅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("统计完成,结果写入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

if __name__ == "__main__":
    main()

五、总结

本文介绍的人脸性别与年龄统计系统,聚焦人脸属性分析需求,采用 “MTCNN 人脸检测 + Caffe 预训练模型属性预测” 的两阶段架构,攻克传统方案检测精度低、非受控环境适配差的问题。系统可批量处理指定目录图片,通过 MTCNN 精准筛选人脸(过滤低置信度框并修正边界),再借助 Caffe 模型完成性别分类与 8 个年龄段估算,最终输出人脸总数、性别 / 年龄段分布、平均年龄及含多张人脸的图片路径,同时生成结构化文本报告。代码模块化设计清晰(路径配置、模型加载、核心功能函数、主流程),兼顾技术学习与企业级客流分析等实际场景,具备良好的实用性与可扩展性。

機器視覺:基於MTCNN與Caffe模型的人臉性別年齡統計系統實現

MTCNN 檢測 + Caffe 性別/年齡模型批次統計人臉屬性,輸出性別與年齡段分布、平均年齡及多臉圖片路徑報告。

來源:https://blog.csdn.net/2403_87969572/article/details/152230636

抓取時間(ISO本地):2026-05-18 05:16:54


文章目錄

一、前言

在計算機視覺領域,人臉屬性分析(如性別識別、年齡估算)是重要的研究方向,廣泛應用於智能監控、用戶畫像構建、零售客流分析等場景。傳統的人臉屬性分析方案常面臨檢測精度低、模型部署複雜等問題,尤其是在非受控環境下的人臉檢測效果往往不盡如人意。
最近在進行數據集規模統計的工作,在上一章進行人臉識別與聚類去除重複Speaker之後,進一步工作是統計樣本人臉的性別與年齡分佈數據,於是做了一套這樣的系統。
本文將對該系統進行詳盡的介紹並附上源碼,該系統通過MTCNN(Multi-Task Cascaded CNN) 實現高精度人臉檢測,結合預訓練的Caffe模型完成性別識別與年齡估算,並最終生成可視化統計報告。系統支持批量處理圖片文件夾中的所有圖像,自動統計人臉總數、性別分佈、年齡段分佈及估算平均年齡,同時記錄包含多張人臉的圖片路徑,為後續分析提供數據支撐。無論是用於個人學習計算機視覺技術,還是企業級的客流屬性分析需求,這套系統都具備良好的實用性和可擴展性。

二、模型介紹

本系統採用“人臉檢測 + 屬性預測”的兩階段架構,分別使用MTCNN和Caffe預訓練模型完成對應任務,兩種模型在各自領域均具備成熟、高效的特點。

2.1 MTCNN:高精度人臉檢測模型

MTCNN(Multi-Task Cascaded Convolutional Neural Networks)是2016年提出的多任務級聯CNN模型,核心優勢在於同時優化人臉檢測、人臉關鍵點定位兩個任務,在保證檢測速度的同時大幅提升了非受控環境下的檢測精度。

  • 核心特點

    1. 級聯結構:分為P-Net(Proposal Network)、R-Net(Refine Network)、O-Net(Output Network)三階段,逐步篩選和優化人臉框,減少誤檢與漏檢。
    2. 多任務學習:在檢測人臉的同時預測5/68個人臉關鍵點(雙眼、鼻尖、兩角嘴角),為後續人臉對齊提供支持(本系統暫未用到關鍵點,但保留擴展能力)。
    3. 輕量級:模型參數少,推理速度快,適合批量圖片處理場景。
  • 本系統應用:替代傳統的OpenCV DNN人臉檢測器,解決側臉、遮擋、小尺寸人臉檢測效果差的問題,檢測置信度閾值設為0.6(可根據需求調整)。

2.2 Caffe預訓練模型:性別與年齡預測

性別和年齡預測採用兩個獨立的Caffe預訓練模型,均基於經典的CNN架構設計,在公開數據集(如IMDB-WIKI)上訓練得到,具備良好的泛化能力。

模型類型輸入要求輸出結果核心用途
性別預測模型227×227×3 RGB圖像(需減去均值)二分類概率(Female/Male)判斷人臉性別
年齡預測模型227×227×3 RGB圖像(需減去均值)8個年齡段概率估算人臉所屬年齡段
  • 年齡分段說明:模型將年齡劃分為8個區間,每個區間對應一個“中間值”用於計算平均年齡,具體映射如下:
    • 年齡段:(0-2) → 中間值1;(4-6) → 中間值5;(8-12) → 中間值10
    • (15-20) → 中間值18;(25-32) → 中間值28;(38-43) → 中間值40
    • (48-53) → 中間值50;(60-100) → 中間值70

三、模型原理解釋

3.1 MTCNN人臉檢測原理

MTCNN通過三階段級聯網絡逐步優化人臉檢測結果,每一步都基於前一步的輸出進行更精細的處理,具體流程如下:

  1. P-Net(Proposal Network)

    • 輸入:原始圖片(通過圖像金字塔生成不同尺度的圖片)。
    • 功能:快速生成大量“候選人臉框”,同時預測每個候選框的置信度(是否為人臉)和邊界框迴歸值(用於調整框的位置)。
    • 輸出:經過置信度篩選和非極大值抑制(NMS)後的候選人臉框。
  2. R-Net(Refine Network)

    • 輸入:P-Net輸出的候選人臉框( resize 為24×24)。
    • 功能:進一步篩選錯誤的候選框,修正邊界框位置,提升檢測精度。
    • 輸出:再次經過置信度篩選和NMS後的人臉框,數量比P-Net大幅減少。
  3. O-Net(Output Network)

    • 輸入:R-Net輸出的人臉框( resize 為48×48)。
    • 功能:最終確認人臉框,輸出更高精度的邊界框迴歸值和5個人臉關鍵點座標。
    • 輸出:最終的人臉檢測框(本系統僅使用框座標,關鍵點可用於後續擴展)。

通過這種“粗篩→精篩→最終確認”的流程,MTCNN既能保證檢測速度,又能有效避免誤檢和漏檢,尤其適合複雜場景下的人臉檢測。

3.2 Caffe模型屬性預測原理

性別和年齡預測模型均基於CNN的特徵提取與分類邏輯,核心流程一致,僅在輸出層的任務定義上有所區別:

通用流程(性別/年齡預測):

  1. 圖像預處理

    • 將MTCNN檢測到的人臉框裁剪為獨立的“人臉圖像”,並 resize 為227×227(模型要求輸入尺寸)。
    • 減去像素均值:對RGB三個通道分別減去固定均值(78.426337, 87.768914, 114.895847),消除光照變化對模型的影響。
    • 生成Blob:將處理後的圖像轉換為Caffe模型要求的Blob格式(批量維度×通道維度×高度×寬度)。
  2. 特徵提取

    • 通過多層卷積層(Convolution)、激活函數(如ReLU)和池化層(Pooling)提取人臉圖像的高層特徵。
    • 例如:卷積層通過卷積核捕捉邊緣、紋理等低級特徵,深層卷積層則整合低級特徵形成“人臉部件”(如眼睛、鼻子)等高級特徵。
  3. 屬性預測

    • 性別預測:輸出層為2個神經元(對應Female和Male),採用Softmax激活函數,輸出兩個類別的概率,概率最大的類別即為預測性別。
    • 年齡預測:輸出層為8個神經元(對應8個年齡段),同樣採用Softmax激活函數,概率最大的神經元對應的年齡段即為預測結果,再通過“中間值”映射用於平均年齡計算。

四、代碼解釋

4.1 整體結構

代碼分為路徑配置、模型加載、核心功能函數、主流程五個模塊,以下按模塊逐一解析:

4.2 路徑配置與依賴導入

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 人臉檢測庫
from tqdm import tqdm  # 進度條庫

# -------- 路徑配置 --------
THIS_DIR = os.path.dirname(__file__)  # 當前腳本所在目錄
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))  # 項目根目錄
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')  # 性別/年齡模型目錄
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')  # 待處理圖片目錄
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性別及年齡統計.txt')  # 結果輸出文件

# -------- 模型文件路徑 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')  # 年齡模型配置文件
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')  # 年齡模型權重
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')  # 性別模型配置文件
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')  # 性別模型權重

# 年齡分段與中間值配置
AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用於計算平均年齡
GENDER_LIST = ['Female', 'Male']  # 性別選項

# 全局MTCNN檢測器(避免重複初始化,提升效率)
mtcnn_detector = MTCNN()
  • 核心作用:定義項目目錄結構、模型文件路徑、業務配置(年齡分段),並初始化全局MTCNN檢測器(減少重複創建實例的開銷)。
  • 依賴說明:需提前安裝mtcnnpip install mtcnn)、opencv-pythonpip install opencv-python)、tqdmpip install tqdm)等庫。

4.3 模型加載函數(load_models

def load_models():
    # 僅加載年齡/性別模型;人臉檢測改為全局MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一個位置返回None,兼容舊調用邏輯
  • 功能:通過OpenCV的dnn模塊加載Caffe格式的年齡和性別模型。
  • 兼容性處理:返回值第一個位置為None(原邏輯中用於返回OpenCV人臉檢測模型,現替換為MTCNN,保留返回格式避免報錯)。

4.4 人臉檢測函數(detect_faces

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用MTCNN進行人臉檢測
    參數:
        face_net: 兼容舊參數(現未使用)
        img: 輸入圖像(BGR格式,OpenCV默認讀取格式)
        conf_thresh: 檢測置信度閾值
    返回:
        boxes: 人臉框列表,格式為[(x1,y1,x2,y2), ...](左上角(x1,y1),右下角(x2,y2))
    """
    results = mtcnn_detector.detect_faces(img)  # MTCNN檢測結果
    boxes = []
    h, w = img.shape[:2]  # 圖像高度和寬度
    for r in results:
        confidence = r.get('confidence', 0)  # 檢測置信度
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']  # MTCNN返回的框:x,y為左上角座標,w_box/h_box為寬高
            # 修正框座標(避免超出圖像邊界)
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            # 確保框為有效區域(寬高均大於0)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes
  • 核心邏輯:調用MTCNN檢測器獲取人臉檢測結果,過濾低置信度(<0.6)的框,並修正座標避免超出圖像邊界。
  • 參數兼容face_net參數保留(原邏輯中用於傳遞OpenCV人臉檢測模型),現未使用,確保舊代碼調用不報錯。

4.5 性別年齡預測函數(predict_age_gender

def predict_age_gender(age_net, gender_net, face_img):
    """
    預測單張人臉的性別和年齡
    參數:
        age_net: 年齡預測模型
        gender_net: 性別預測模型
        face_img: 裁剪後的人臉圖像(BGR格式)
    返回:
        gender: 預測性別(Female/Male)
        age: 預測年齡段(如(25-32))
        age_mid: 年齡段中間值(如28)
    """
    # 圖像預處理:轉換為Blob格式並減去均值
    blob = cv2.dnn.blobFromImage(
        face_img, 
        1.0,  # 縮放因子
        (227, 227),  # 模型輸入尺寸
        (78.426337, 87.768914, 114.895847),  # 各通道均值(BGR順序)
        swapRB=False  # 無需交換R和B通道(OpenCV讀取為BGR,模型要求BGR)
    )
    
    # 性別預測
    gender_net.setInput(blob)  # 設置模型輸入
    gender_pred = gender_net.forward()  # 推理得到預測結果
    gender = GENDER_LIST[gender_pred[0].argmax()]  # 取概率最大的類別
    
    # 年齡預測
    age_net.setInput(blob)  # 設置模型輸入
    age_pred = age_net.forward()  # 推理得到預測結果
    age_idx = age_pred[0].argmax()  # 取概率最大的年齡段索引
    age = AGE_LIST[age_idx]  # 年齡段標籤
    age_mid = AGE_MID[age_idx]  # 年齡段中間值
    
    return gender, age, age_mid
  • 關鍵步驟
    1. blobFromImage:將人臉圖像轉換為模型可接受的Blob格式,同時完成縮放、尺寸調整和均值減法。
    2. 模型推理:通過setInput設置輸入,forward執行推理,得到預測概率分佈。
    3. 結果解析:通過argmax獲取概率最大的類別索引,映射為性別標籤和年齡段。

4.6 圖片遍歷函數(iter_images

def iter_images(root):
    """
    遍歷目錄下所有支持的圖片文件(遞歸遍歷子目錄)
    參數:
        root: 根目錄路徑
    返回:
        圖片文件路徑生成器
    """
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}  # 支持的圖片格式
    for dirpath, _, filenames in os.walk(root):  # 遞歸遍歷目錄
        for f in filenames:
            # 檢查文件後綴是否在支持的格式中(不區分大小寫)
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)  # 返回完整文件路徑
  • 功能:遞歸遍歷指定目錄下的所有圖片文件,僅保留支持的格式(JPG、PNG等),通過生成器(yield)減少內存佔用。

4.7 主函數(main

主函數是系統的核心流程入口,負責串聯所有模塊,完成“圖片加載→人臉檢測→屬性預測→統計分析→結果輸出”的全流程:

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 現在恆為 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 預收集所有圖片路徑用於進度條
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何圖片,目錄:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='處理圖片', unit='張'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 單張失敗不影響整體
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("統計時間: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"掃描圖片數: {total_images}")
    lines.append(f"檢測到人臉總數: {total_faces}")
    lines.append("性別統計:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年齡段統計:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年齡: {avg_age}")
    lines.append("(文件級人臉計數僅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("統計完成,結果寫入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

4.8 完整代碼

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 新增:使用與 face_check.py 相同的 MTCNN 檢測
from tqdm import tqdm  # 進度條

# -------- 路徑配置 --------
THIS_DIR = os.path.dirname(__file__)
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性別及年齡統計.txt')

# -------- 模型文件 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')
# 下面兩個(OpenCV DNN人臉檢測)已不再使用,保留不報錯;如需可刪除
# FACE_PROTO = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector.pbtxt')
# FACE_MODEL = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector_uint8.pb')

AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用於估算平均年齡
GENDER_LIST = ['Female', 'Male']
# 新增:全局 MTCNN 檢測器,與 face_check.py 保持一致
mtcnn_detector = MTCNN()

def load_models():
    # 僅加載年齡/性別模型;人臉檢測改為全局 MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一個位置兼容舊調用

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用與 face_check.py 相同的 MTCNN 進行檢測
    返回: [(x1,y1,x2,y2), ...]
    """
    results = mtcnn_detector.detect_faces(img)
    boxes = []
    h, w = img.shape[:2]
    for r in results:
        confidence = r.get('confidence', 0)
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes

def predict_age_gender(age_net, gender_net, face_img):
    blob = cv2.dnn.blobFromImage(face_img, 1.0, (227, 227), (78.426337, 87.768914, 114.895847), swapRB=False)
    gender_net.setInput(blob)
    gender_pred = gender_net.forward()
    gender = GENDER_LIST[gender_pred[0].argmax()]
    age_net.setInput(blob)
    age_pred = age_net.forward()
    age_idx = age_pred[0].argmax()
    age = AGE_LIST[age_idx]
    age_mid = AGE_MID[age_idx]
    return gender, age, age_mid

def iter_images(root):
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}
    for dirpath, _, filenames in os.walk(root):
        for f in filenames:
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 現在恆為 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 預收集所有圖片路徑用於進度條
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何圖片,目錄:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='處理圖片', unit='張'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 單張失敗不影響整體
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("統計時間: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"掃描圖片數: {total_images}")
    lines.append(f"檢測到人臉總數: {total_faces}")
    lines.append("性別統計:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年齡段統計:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年齡: {avg_age}")
    lines.append("(文件級人臉計數僅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("統計完成,結果寫入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

if __name__ == "__main__":
    main()

五、總結

本文介紹的人臉性別與年齡統計系統,聚焦人臉屬性分析需求,採用 “MTCNN 人臉檢測 + Caffe 預訓練模型屬性預測” 的兩階段架構,攻克傳統方案檢測精度低、非受控環境適配差的問題。系統可批量處理指定目錄圖片,通過 MTCNN 精準篩選人臉(過濾低置信度框並修正邊界),再借助 Caffe 模型完成性別分類與 8 個年齡段估算,最終輸出人臉總數、性別 / 年齡段分佈、平均年齡及含多張人臉的圖片路徑,同時生成結構化文本報告。代碼模塊化設計清晰(路徑配置、模型加載、核心功能函數、主流程),兼顧技術學習與企業級客流分析等實際場景,具備良好的實用性與可擴展性。

Computer Vision: Face Gender & Age Statistics with MTCNN and Caffe Models

Batch face gender/age stats with MTCNN detection and Caffe classifiers, exporting distributions, mean age, and multi-face file lists.

Captured at (local ISO): 2026-05-18 05:16:54


I. Preface

Face-attribute analysis—gender, age, and more—is a core CV topic for surveillance, user profiling, retail footfall analytics, etc. Classical pipelines often fail on wild faces: weak detectors, heavy deploy friction.

I built this while scaling a dataset: after face clustering to dedupe speakers, the next step was gender/age distribution stats. This note documents that system end-to-end with source.

The stack uses MTCNN (Multi-task Cascaded CNN) for robust detection plus off-the-shelf Caffe nets for gender and 8-bin age estimation, then writes a structured text report. It batch-processes every image under a folder, tallying face count, gender histogram, age-bin histogram, mean age (mid-bin proxy), and paths with multiple faces. Useful for learning CV or production-style footfall analytics.

II. Model overview

Two stages: detect, then classify.

2.1 MTCNN: high-accuracy face detection

MTCNN (Multi-task Cascaded Convolutional Neural Networks, 2016) jointly optimizes face detection and landmark localization—fast in the wild.

  • Highlights
    1. Cascade: P-Net → R-Net → O-Net progressively refine boxes—fewer FPs/FNs.
    2. Multi-task: predicts 5 (or 68) landmarks (eyes, nose, mouth corners)—handy for alignment (unused here but easy to add).
    3. Light: small weights, good throughput for batch jobs.
  • In this repo: replaces a vanilla OpenCV DNN face detector for profile, occlusion, tiny faces; default confidence cutoff 0.6 (tune as needed).

2.2 Caffe pretrained models: gender & age

Two separate Caffe nets—classic CNN heads trained on public corpora (e.g., IMDB–WIKI), decent out-of-domain behavior.

ModelInputOutputRole
Gender227×227×3 RGB (mean-subtracted)2-way softmax (Female/Male)Gender label
Agesame8 age-bin logitsAge bin + mid value for averaging
  • Bins → mid values for mean-age math:
    • (0-2)→1; (4-6)→5; (8-12)→10; (15-20)→18; (25-32)→28; (38-43)→40; (48-53)→50; (60-100)→70

III. How the models work

3.1 MTCNN pipeline

Three stages, each refining the last:

  1. P-Net
    • In: image pyramid.
    • Job: cheap candidate boxes + face scores + box regressions.
    • Out: NMS-filtered candidates.
  2. R-Net
    • In: P-Net crops resized to 24×24.
    • Job: reject bad proposals, refine boxes.
    • Out: fewer, tighter boxes.
  3. O-Net
    • In: R-Net crops to 48×48.
    • Job: final box regression + 5 landmarks.
    • Out: final boxes (we consume boxes only; landmarks are free for later).

Coarse → fine → final keeps speed and robustness in clutter.

3.2 Caffe attribute heads

Same recipe for gender and age:

  1. Preprocess
    • Crop MTCNN box → 227×227.
    • Subtract per-channel means (78.426337, 87.768914, 114.895847) in BGR.
    • Pack a blob N×C×H×W.
  2. Features
    • Convs / ReLU / pools build hierarchical cues—edges/textures low level; parts high level.
  3. Heads
    • Gender: 2-way softmax → argmax label.
    • Age: 8-way softmax → bin label → mid value for averages.

IV. Code walkthrough

4.1 Overall layout

Modules: paths, loaders, core functions, main.

4.2 Paths & imports

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 人脸检测库
from tqdm import tqdm  # 进度条库

# -------- 路径配置 --------
THIS_DIR = os.path.dirname(__file__)  # 当前脚本所在目录
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))  # 项目根目录
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')  # 性别/年龄模型目录
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')  # 待处理图片目录
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性别及年龄统计.txt')  # 结果输出文件

# -------- 模型文件路径 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')  # 年龄模型配置文件
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')  # 年龄模型权重
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')  # 性别模型配置文件
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')  # 性别模型权重

# 年龄分段与中间值配置
AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用于计算平均年龄
GENDER_LIST = ['Female', 'Male']  # 性别选项

# 全局MTCNN检测器(避免重复初始化,提升效率)
mtcnn_detector = MTCNN()
  • Role: wire directories, Caffe artifacts, age metadata, singleton MTCNN (avoid per-image init).
  • Deps: pip install mtcnn opencv-python tqdm.

4.3 load_models

def load_models():
    # 仅加载年龄/性别模型;人脸检测改为全局MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一个位置返回None,兼容旧调用逻辑
  • Loads age/gender via cv2.dnn.readNet.
  • Leading None preserves older tuple unpacking when OpenCV supplied a face net—now unused.

4.4 detect_faces

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用MTCNN进行人脸检测
    参数:
        face_net: 兼容旧参数(现未使用)
        img: 输入图像(BGR格式,OpenCV默认读取格式)
        conf_thresh: 检测置信度阈值
    返回:
        boxes: 人脸框列表,格式为[(x1,y1,x2,y2), ...](左上角(x1,y1),右下角(x2,y2))
    """
    results = mtcnn_detector.detect_faces(img)  # MTCNN检测结果
    boxes = []
    h, w = img.shape[:2]  # 图像高度和宽度
    for r in results:
        confidence = r.get('confidence', 0)  # 检测置信度
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']  # MTCNN返回的框:x,y为左上角坐标,w_box/h_box为宽高
            # 修正框坐标(避免超出图像边界)
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            # 确保框为有效区域(宽高均大于0)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes
  • Filters by confidence, clamps to frame, drops degenerate boxes.
  • face_net remains for backward-compatible calls.

4.5 predict_age_gender

def predict_age_gender(age_net, gender_net, face_img):
    """
    预测单张人脸的性别和年龄
    参数:
        age_net: 年龄预测模型
        gender_net: 性别预测模型
        face_img: 裁剪后的人脸图像(BGR格式)
    返回:
        gender: 预测性别(Female/Male)
        age: 预测年龄段(如(25-32))
        age_mid: 年龄段中间值(如28)
    """
    # 图像预处理:转换为Blob格式并减去均值
    blob = cv2.dnn.blobFromImage(
        face_img, 
        1.0,  # 缩放因子
        (227, 227),  # 模型输入尺寸
        (78.426337, 87.768914, 114.895847),  # 各通道均值(BGR顺序)
        swapRB=False  # 无需交换R和B通道(OpenCV读取为BGR,模型要求BGR)
    )
    
    # 性别预测
    gender_net.setInput(blob)  # 设置模型输入
    gender_pred = gender_net.forward()  # 推理得到预测结果
    gender = GENDER_LIST[gender_pred[0].argmax()]  # 取概率最大的类别
    
    # 年龄预测
    age_net.setInput(blob)  # 设置模型输入
    age_pred = age_net.forward()  # 推理得到预测结果
    age_idx = age_pred[0].argmax()  # 取概率最大的年龄段索引
    age = AGE_LIST[age_idx]  # 年龄段标签
    age_mid = AGE_MID[age_idx]  # 年龄段中间值
    
    return gender, age, age_mid

Steps: blobFromImageforward twice → argmax maps.

4.6 iter_images

def iter_images(root):
    """
    遍历目录下所有支持的图片文件(递归遍历子目录)
    参数:
        root: 根目录路径
    返回:
        图片文件路径生成器
    """
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}  # 支持的图片格式
    for dirpath, _, filenames in os.walk(root):  # 递归遍历目录
        for f in filenames:
            # 检查文件后缀是否在支持的格式中(不区分大小写)
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)  # 返回完整文件路径

Recursive generator—low memory.

4.7 main

Orchestrates enumerate → detect → infer → aggregate → save.

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 现在恒为 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 预收集所有图片路径用于进度条
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何图片,目录:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='处理图片', unit='张'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 单张失败不影响整体
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("统计时间: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"扫描图片数: {total_images}")
    lines.append(f"检测到人脸总数: {total_faces}")
    lines.append("性别统计:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年龄段统计:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年龄: {avg_age}")
    lines.append("(文件级人脸计数仅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("统计完成,结果写入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

4.8 Full listing

import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from datetime import datetime
from mtcnn import MTCNN   # 新增:使用与 face_check.py 相同的 MTCNN 检测
from tqdm import tqdm  # 进度条

# -------- 路径配置 --------
THIS_DIR = os.path.dirname(__file__)
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, '..'))
AGE_GENDER_DIR = os.path.join(PROJECT_ROOT, 'age_gender')
FACE_DIR = os.path.join(PROJECT_ROOT, 'face', 'face_dir')
RESULT_TXT = os.path.join(PROJECT_ROOT, 'face', '性别及年龄统计.txt')

# -------- 模型文件 --------
AGE_PROTO = os.path.join(AGE_GENDER_DIR, 'age_deploy.prototxt')
AGE_MODEL = os.path.join(AGE_GENDER_DIR, 'age_net.caffemodel')
GENDER_PROTO = os.path.join(AGE_GENDER_DIR, 'gender_deploy.prototxt')
GENDER_MODEL = os.path.join(AGE_GENDER_DIR, 'gender_net.caffemodel')
# 下面两个(OpenCV DNN人脸检测)已不再使用,保留不报错;如需可删除
# FACE_PROTO = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector.pbtxt')
# FACE_MODEL = os.path.join(AGE_GENDER_DIR, 'opencv_face_detector_uint8.pb')

AGE_LIST = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
AGE_MID = [1, 5, 10, 18, 28, 40, 50, 70]  # 用于估算平均年龄
GENDER_LIST = ['Female', 'Male']
# 新增:全局 MTCNN 检测器,与 face_check.py 保持一致
mtcnn_detector = MTCNN()

def load_models():
    # 仅加载年龄/性别模型;人脸检测改为全局 MTCNN
    age_net = cv2.dnn.readNet(AGE_MODEL, AGE_PROTO)
    gender_net = cv2.dnn.readNet(GENDER_MODEL, GENDER_PROTO)
    return None, age_net, gender_net  # 第一个位置兼容旧调用

def detect_faces(face_net, img, conf_thresh=0.6):
    """
    使用与 face_check.py 相同的 MTCNN 进行检测
    返回: [(x1,y1,x2,y2), ...]
    """
    results = mtcnn_detector.detect_faces(img)
    boxes = []
    h, w = img.shape[:2]
    for r in results:
        confidence = r.get('confidence', 0)
        if confidence >= conf_thresh and 'box' in r:
            x, y, w_box, h_box = r['box']
            x1 = max(0, x)
            y1 = max(0, y)
            x2 = min(x + w_box, w - 1)
            y2 = min(y + h_box, h - 1)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    return boxes

def predict_age_gender(age_net, gender_net, face_img):
    blob = cv2.dnn.blobFromImage(face_img, 1.0, (227, 227), (78.426337, 87.768914, 114.895847), swapRB=False)
    gender_net.setInput(blob)
    gender_pred = gender_net.forward()
    gender = GENDER_LIST[gender_pred[0].argmax()]
    age_net.setInput(blob)
    age_pred = age_net.forward()
    age_idx = age_pred[0].argmax()
    age = AGE_LIST[age_idx]
    age_mid = AGE_MID[age_idx]
    return gender, age, age_mid

def iter_images(root):
    exts = {'.jpg', '.jpeg', '.png', '.bmp'}
    for dirpath, _, filenames in os.walk(root):
        for f in filenames:
            if os.path.splitext(f.lower())[1] in exts:
                yield os.path.join(dirpath, f)

def main():
    if not os.path.exists(FACE_DIR):
        print("face_dir 不存在:", FACE_DIR)
        return
    face_net, age_net, gender_net = load_models()  # face_net 现在恒为 None
    gender_counter = Counter()
    age_counter = Counter()
    age_mid_values = []
    file_face_count = defaultdict(int)
    total_faces = 0

    # 预收集所有图片路径用于进度条
    image_paths = list(iter_images(FACE_DIR))
    total_images = len(image_paths)
    if total_images == 0:
        print("未找到任何图片,目录:", FACE_DIR)
        return

    for img_path in tqdm(image_paths, desc='处理图片', unit='张'):
        img = cv2.imread(img_path)
        if img is None:
            continue
        boxes = detect_faces(face_net, img)
        for (x1, y1, x2, y2) in boxes:
            face = img[y1:y2, x1:x2]
            if face.size == 0:
                continue
            try:
                gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)
            except Exception as e:
                # 单张失败不影响整体
                continue
            gender_counter[gender] += 1
            age_counter[age_range] += 1
            age_mid_values.append(age_mid)
            file_face_count[img_path] += 1
            total_faces += 1

    avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0

    lines = []
    lines.append("统计时间: " + datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    lines.append(f"扫描图片数: {total_images}")
    lines.append(f"检测到人脸总数: {total_faces}")
    lines.append("性别统计:")
    for g in GENDER_LIST:
        lines.append(f"  {g}: {gender_counter.get(g,0)}")
    lines.append("年龄段统计:")
    for a in AGE_LIST:
        lines.append(f"  {a}: {age_counter.get(a,0)}")
    lines.append(f"估算平均年龄: {avg_age}")
    lines.append("(文件级人脸计数仅列出>1者)")
    for fp, c in file_face_count.items():
        if c > 1:
            lines.append(f"  {fp}: {c}")

    os.makedirs(os.path.dirname(RESULT_TXT), exist_ok=True)
    with open(RESULT_TXT, 'w', encoding='utf-8') as f:
        f.write('\n'.join(lines))

    print("统计完成,结果写入:", RESULT_TXT)
    print('\n'.join(lines[:10]), '...')

if __name__ == "__main__":
    main()

V. Summary

This pipeline targets face-attribute analytics with a detector + classifier split: MTCNN for robust proposals (score gating + box clamping) and Caffe nets for gender plus 8 age bins (mid values for mean age). Batch-scan a directory, emit totals, histograms, mean age, multi-face paths, and a UTF-8 report file. Modular layout—paths, loaders, helpers, main—is easy to read for study or to extend for footfall / demographic style deployments.