机器视觉:基于MTCNN与Caffe模型的人脸性别年龄统计系统实现
文章目录
- 一、前言
- 二、模型介绍
- 2.1 MTCNN:高精度人脸检测模型
- 2.2 Caffe预训练模型:性别与年龄预测
- 三、模型原理解释
- 3.1 MTCNN人脸检测原理
- 3.2 Caffe模型属性预测原理
- 四、代码解释
- 4.1 整体结构
- 4.2 路径配置与依赖导入
- 4.3 模型加载函数(`load_models`)
- 4.4 人脸检测函数(`detect_faces`)
- 4.5 性别年龄预测函数(`predict_age_gender`)
- 4.6 图片遍历函数(`iter_images`)
- 4.7 主函数(`main`)
- 4.8 完整代码
- 五、总结
一、前言
在计算机视觉领域,人脸属性分析(如性别识别、年龄估算)是重要的研究方向,广泛应用于智能监控、用户画像构建、零售客流分析等场景。传统的人脸属性分析方案常面临检测精度低、模型部署复杂等问题,尤其是在非受控环境下的人脸检测效果往往不尽如人意。
最近在进行数据集规模统计的工作,在上一章进行人脸识别与聚类去除重复Speaker之后,进一步工作是统计样本人脸的性别与年龄分布数据,于是做了一套这样的系统。
本文将对该系统进行详尽的介绍并附上源码,该系统通过MTCNN(Multi-Task Cascaded CNN) 实现高精度人脸检测,结合预训练的Caffe模型完成性别识别与年龄估算,并最终生成可视化统计报告。系统支持批量处理图片文件夹中的所有图像,自动统计人脸总数、性别分布、年龄段分布及估算平均年龄,同时记录包含多张人脸的图片路径,为后续分析提供数据支撑。无论是用于个人学习计算机视觉技术,还是企业级的客流属性分析需求,这套系统都具备良好的实用性和可扩展性。
二、模型介绍
本系统采用“人脸检测 + 属性预测”的两阶段架构,分别使用MTCNN和Caffe预训练模型完成对应任务,两种模型在各自领域均具备成熟、高效的特点。
2.1 MTCNN:高精度人脸检测模型
MTCNN(Multi-Task Cascaded Convolutional Neural Networks)是2016年提出的多任务级联CNN模型,核心优势在于同时优化人脸检测、人脸关键点定位两个任务,在保证检测速度的同时大幅提升了非受控环境下的检测精度。
-
核心特点:
- 级联结构:分为P-Net(Proposal Network)、R-Net(Refine Network)、O-Net(Output Network)三阶段,逐步筛选和优化人脸框,减少误检与漏检。
- 多任务学习:在检测人脸的同时预测5/68个人脸关键点(双眼、鼻尖、两角嘴角),为后续人脸对齐提供支持(本系统暂未用到关键点,但保留扩展能力)。
- 轻量级:模型参数少,推理速度快,适合批量图片处理场景。
-
本系统应用:替代传统的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通过三阶段级联网络逐步优化人脸检测结果,每一步都基于前一步的输出进行更精细的处理,具体流程如下:
-
P-Net(Proposal Network):
- 输入:原始图片(通过图像金字塔生成不同尺度的图片)。
- 功能:快速生成大量“候选人脸框”,同时预测每个候选框的置信度(是否为人脸)和边界框回归值(用于调整框的位置)。
- 输出:经过置信度筛选和非极大值抑制(NMS)后的候选人脸框。
-
R-Net(Refine Network):
- 输入:P-Net输出的候选人脸框( resize 为24×24)。
- 功能:进一步筛选错误的候选框,修正边界框位置,提升检测精度。
- 输出:再次经过置信度筛选和NMS后的人脸框,数量比P-Net大幅减少。
-
O-Net(Output Network):
- 输入:R-Net输出的人脸框( resize 为48×48)。
- 功能:最终确认人脸框,输出更高精度的边界框回归值和5个人脸关键点坐标。
- 输出:最终的人脸检测框(本系统仅使用框坐标,关键点可用于后续扩展)。
通过这种“粗筛→精筛→最终确认”的流程,MTCNN既能保证检测速度,又能有效避免误检和漏检,尤其适合复杂场景下的人脸检测。
3.2 Caffe模型属性预测原理
性别和年龄预测模型均基于CNN的特征提取与分类逻辑,核心流程一致,仅在输出层的任务定义上有所区别:
通用流程(性别/年龄预测):
-
图像预处理:
- 将MTCNN检测到的人脸框裁剪为独立的“人脸图像”,并 resize 为227×227(模型要求输入尺寸)。
- 减去像素均值:对RGB三个通道分别减去固定均值(78.426337, 87.768914, 114.895847),消除光照变化对模型的影响。
- 生成Blob:将处理后的图像转换为Caffe模型要求的Blob格式(批量维度×通道维度×高度×宽度)。
-
特征提取:
- 通过多层卷积层(Convolution)、激活函数(如ReLU)和池化层(Pooling)提取人脸图像的高层特征。
- 例如:卷积层通过卷积核捕捉边缘、纹理等低级特征,深层卷积层则整合低级特征形成“人脸部件”(如眼睛、鼻子)等高级特征。
-
属性预测:
- 性别预测:输出层为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检测器(减少重复创建实例的开销)。
- 依赖说明:需提前安装
mtcnn
(pip install mtcnn
)、opencv-python
(pip install opencv-python
)、tqdm
(pip install tqdm
)等库。
4.3 模型加载函数(load_models
)
def load_models():# 仅加载年龄/性别模型;人脸检测改为全局MTCNNage_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
- 关键步骤:
blobFromImage
:将人脸图像转换为模型可接受的Blob格式,同时完成缩放、尺寸调整和均值减法。- 模型推理:通过
setInput
设置输入,forward
执行推理,得到预测概率分布。 - 结果解析:通过
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)returnface_net, age_net, gender_net = load_models() # face_net 现在恒为 Nonegender_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)returnfor img_path in tqdm(image_paths, desc='处理图片', unit='张'):img = cv2.imread(img_path)if img is None:continueboxes = detect_faces(face_net, img)for (x1, y1, x2, y2) in boxes:face = img[y1:y2, x1:x2]if face.size == 0:continuetry:gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)except Exception as e:# 单张失败不影响整体continuegender_counter[gender] += 1age_counter[age_range] += 1age_mid_values.append(age_mid)file_face_count[img_path] += 1total_faces += 1avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0lines = []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():# 仅加载年龄/性别模型;人脸检测改为全局 MTCNNage_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 boxesdef 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_middef 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)returnface_net, age_net, gender_net = load_models() # face_net 现在恒为 Nonegender_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)returnfor img_path in tqdm(image_paths, desc='处理图片', unit='张'):img = cv2.imread(img_path)if img is None:continueboxes = detect_faces(face_net, img)for (x1, y1, x2, y2) in boxes:face = img[y1:y2, x1:x2]if face.size == 0:continuetry:gender, age_range, age_mid = predict_age_gender(age_net, gender_net, face)except Exception as e:# 单张失败不影响整体continuegender_counter[gender] += 1age_counter[age_range] += 1age_mid_values.append(age_mid)file_face_count[img_path] += 1total_faces += 1avg_age = round(sum(age_mid_values) / len(age_mid_values), 2) if age_mid_values else 0.0lines = []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 个年龄段估算,最终输出人脸总数、性别 / 年龄段分布、平均年龄及含多张人脸的图片路径,同时生成结构化文本报告。代码模块化设计清晰(路径配置、模型加载、核心功能函数、主流程),兼顾技术学习与企业级客流分析等实际场景,具备良好的实用性与可扩展性。