简介:面向OpenCV开发者的年龄与性别预测配套资源包,基于DNN部署CNN模型完成人脸检测、年龄与性别识别。资源内含可直接运行的Visual Studio工程,包含C++源码、解决方案与项目配置,并备好Caffe模型文件(age_net.caffemodel、gender_net.caffemodel)、对应prototxt部署配置以及Haar级联人脸检测XML,下载后即可编译体验完整流程。整个压缩包共53个文件,大小约106.68MB,除核心模型与源码外,还包含可执行程序、示例图片、调试及构建记录(TLOG、PDB、IPCH等),工程结构清晰,便于边看边改。目前已有335人学习下载,适合想要快速上手OpenCV DNN,或在现有项目中加入年龄性别预测能力的初中级开发者参考使用。
1. 用 OpenCV 做年龄和性别预测:先搞清楚这个 demo 够不够你用
如果你手头有一批照片、一段监控录像、或者只想在自己的笔记本摄像头前快速验证“OpenCV 能猜出人的年龄和性别”,那这个基于 OpenCV DNN 模块的年龄和性别预测项目就是最典型的切入点。它不需要你懂深度学习训练,也不需要 GPU,直接加载两个预训练好的 Caffe 模型,配合一个人脸检测器,就能在 CPU 上跑出结果。很多人以为这类功能必须自己收集人脸数据集、训练几周才能做出来,实际上 OpenCV 生态里早就有公开的年龄网络和性别网络,你要做的只是把模型加载进来,写好预处理和后处理流程。这篇笔记我把整套实现拆开讲清楚:模型为什么这么选、代码每个参数怎么设、常见的坑在哪,新手照着跑一遍能出结果,熟手也能快速定位问题。
2. 模型选型与推理原理:为什么是 Caffe 预训练模型而不是自己训一个
2.1 选型的底层逻辑:训练成本高,但推理路径已经很成熟
年龄预测和性别预测从原理上都是图像分类任务。性别是二分类,年龄是回归或者离散年龄段分类,这个项目用的就是 8 个年龄段离散分类。理论上你可以用 PyTorch 或者 TensorFlow 自己训练一个分类网络,但现实是:高质量的人脸年龄数据集不容易拿,标注成本高,而且年龄本身是主观标签——不同人对同一张脸的年龄判断可以差好几岁,模型训练出来往往也不稳定。
OpenCV 给出的解决方案不是让你从零训练,而是用 DNN 模块直接读取别人训练好的模型。OpenCV 从 3.3 版本开始内置了 cv2.dnn 模块,它能加载 Caffe、TensorFlow、ONNX 格式的模型文件,并且把推理过程放到自己内部完成。你的 Python 环境里只要装了 opencv-python,不需要额外安装 PyTorch、TensorFlow 这些深度学习框架,就能跑神经网络推理。对于年龄性别预测这类任务,官方仓库里预训练好的两个模型属于 ResNet 变体结构,虽然精度不是学术界最新水平,但在 CPU 上单帧推理也就几十毫秒,放在实际应用里属于“够用”的层级。
选择 Caffe 格式还有一个务实原因:prototxt 文件是明文结构,你可以直接打开看网络每一层的输入输出维度。这个特性在排查问题时特别有用,比如你怀疑输入尺寸搞错了,打开 prototxt 看 data 层就能确认是 227x227 还是 224x224。TensorFlow 的冻结图就没这么直观,ONNX 也需要额外工具查看。对初学者来讲,Caffe 这种“配置文件可见、模型权重分离”的方式是最友好的。
2.2 两个模型的关键参数:输入尺寸、均值、输出类别
年龄网络和性别网络虽然结构相似,但输出维度完全不同。年龄网络 age_net 的输出是 1x8 的向量,对应 8 个年龄段;性别网络 gender_net 的输出是 1x2 的向量,对应男性和女性。这两个模型在 OpenCV 的 DNN 模块里用 readNetFromCaffe 加载,第一个参数是网络结构的 prototxt 文件,第二个参数是权重 caffemodel 文件。
输入图像在进入网络前要经过一个非常关键的预处理步骤。年龄性别网络训练时使用的均值是 BGR 三个通道分别为 78.4263377603、87.7689143744、114.895847746,这个均值必须原样设置,否则预测结果会明显偏移,后面避坑章节详细说。输入尺寸是 227x227,不是 ImageNet 常用的 224x224,这个数字要对上。cv2.dnn.blobFromImage 函数负责完成缩放、减均值、通道转换等一系列操作,下表列出这个函数在本项目中每个参数的含义。
| 参数 | 本项目取值 | 说明 |
|---|---|---|
| image | 裁剪后的人脸 BGR 图像 | 先做人脸检测,再把人脸区域单独裁剪出来 |
| scalefactor | 1.0 | 像素值缩放系数,这里不缩放,因为均值减完就够 |
| size | (227, 227) | 网络输入尺寸,必须与 prototxt 里的 data 层一致 |
| mean | (78.43, 87.77, 114.90) | 训练时统计的 BGR 均值,按顺序减 |
| swapRB | False | Caffe 模型本身就是 BGR 通道序,不做交换 |
| crop | False | 不中心裁剪,直接拉伸填充,人脸区域已经对齐好 |
预处理完成后,两个网络各自做一次前向推理。年龄网络的输出经过 softmax 后得到 8 个年龄段各自的概率,取概率最大的下标,对应下面的年龄段列表:0-2、4-6、8-13、15-20、25-32、38-43、48-53、60+。性别网络同理,输出两类概率,取最大下标对应 Male 或 Female。这里要特别说明的是,年龄预测的结果本身就是模糊的——它预测的是年龄段而不是具体岁数,实际使用时不要把结果当作精确年龄,而是当成一个粗粒度的区间判断来用。
整个推理链路是:先用一个人脸检测器在完整图像上找到人脸位置,再把人脸区域裁剪出来,送进年龄和性别网络。所以这套系统的准确率上限不光取决于年龄性别模型本身,更取决于人脸检测器能不能稳定框住人脸。如果人脸检测框偏了,裁出来的图像里有大量背景,年龄性别预测结果就会变得很随机。这也是为什么很多人在自己照片上测试时发现结果飘忽不定的主要原因。
3. 落地实现:人脸检测、blob 预处理与前向解码三步走
3.1 环境准备与模型文件组织
动手之前先把环境准备好。本项目的依赖非常少,核心就是 opencv-python,推荐 4.x 版本。安装命令很简单,但要注意不要同时装 opencv-python 和 opencv-contrib-python 两个包,否则会出现 cv2 模块加载冲突,后面避坑章节会讲。
# 创建虚拟环境后执行,Python 3.7 以上都支持 pip install opencv-python opencv-contrib-python # 验证安装 python -c "import cv2; print(cv2.__version__)"openv-contrib-python 里有额外的人脸检测器模块,后面想换 YuNet 检测器会用到。如果只是跑通基础版,装一个 opencv-python 其实就够。模型文件方面,你需要准备四个文件:age_deploy.prototxt、age_net.caffemodel、gender_deploy.prototxt、gender_net.caffemodel,外加一个人脸检测用的 haarcascade_frontalface_default.xml。这些文件在 OpenCV 官方仓库 opencv_extra 的 samples/data 目录下都有,本地如果装了 opencv-python,Haar 级联分类器通常在 site-packages/cv2/data/ 目录下能找到。
age_gender_demo/ ├── models/ │ ├── age_deploy.prototxt │ ├── age_net.caffemodel │ ├── gender_deploy.prototxt │ └── gender_net.caffemodel ├── haarcascade_frontalface_default.xml ├── test.jpg └── demo.py我一般习惯把模型文件统一放在 models 子目录里,代码只依赖相对路径,避免在不同的机器上因为绝对路径问题翻车。注意目录结构里不要有中文路径,OpenCV 在某些版本里读取带中文路径的文件会直接失败,这是一个很隐蔽的坑。
3.2 对人脸检测器做选型和参数调优
人脸检测是年龄性别预测的前置步骤。项目最常用的方案是 Haar 级联分类器,它从 OpenCV 1.x 时代就存在,经过这么多年依然是轻量级场景的默认选择。Haar 检测器用 cv2.CascadeClassifier 加载 XML 文件,检测时调用 detectMultiScale 方法。这个方法的参数直接影响检测效果,下面代码里标出了关键参数。
# 加载人脸检测器 detector = cv2.CascadeClassifier("haarcascade_frontalface_default.xml") # 转换成灰度图检测,Haar 特征只计算亮度信息 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 关键参数:scaleFactor 和 minNeighbors 决定检测的灵敏度和误检率 faces = detector.detectMultiScale( gray, scaleFactor=1.1, # 每次缩放比例,越小检测越慢但越准 minNeighbors=5, # 候选区域需要被多少个邻近框命中才保留 minSize=(80, 80), # 人脸最小尺寸,小于这个尺寸直接忽略 )scaleFactor 控制检测过程中的图像金字塔缩放步长。取值 1.1 表示每次将图像缩小 10%,这是一个比较稳妥的平衡点。如果检测不到人脸,可以把 scaleFactor 调到 1.05,代价是检测时间会明显变长。minNeighbors 控制误检抑制强度,值越大误检越少,但太小的人脸会被过滤掉。在单人照片场景,minNeighbors 用 5 到 6 没有问题;多人密集场景建议降到 3,否则遮挡面积大的人脸会被漏掉。minSize 这个参数在实践中非常重要,如果你不设置它,检测器会在整张图像里扫描各种尺寸的窗口,很容易把纹理复杂区域误判成人脸。对于年龄性别预测任务,太小的人脸区域本身就没有足够细节供网络判断,设一个 80x80 的下限是合理的。
3.3 完整推理代码:检测、裁剪、预处理的顺序不能乱
下面是一份完整可运行的 demo.py 代码,包含人脸检测、年龄预测、性别预测和结果绘制。这个版本没有做任何花哨优化,逻辑顺序清晰,适合用来理解整个链路。
import cv2 # 年龄段标签,顺序必须与模型输出一一对应 AGE_LIST = ['(0-2)', '(4-6)', '(8-13)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60+)'] GENDER_LIST = ['Male', 'Female'] # 模型文件路径,按自己的目录结构调整 AGE_PROTO = "models/age_deploy.prototxt" AGE_MODEL = "models/age_net.caffemodel" GENDER_PROTO = "models/gender_deploy.prototxt" GENDER_MODEL = "models/gender_net.caffemodel" # 加载两个网络 age_net = cv2.dnn.readNetFromCaffe(AGE_PROTO, AGE_MODEL) gender_net = cv2.dnn.readNetFromCaffe(GENDER_PROTO, GENDER_MODEL) # 加载人脸检测器 detector = cv2.CascadeClassifier("haarcascade_frontalface_default.xml") def predict_age_gender(face_img): # 构建网络输入 blob blob = cv2.dnn.blobFromImage( face_img, 1.0, (227, 227), (78.4263377603, 87.7689143744, 114.895847746), swapRB=False, crop=False ) # 性别推理 gender_net.setInput(blob) gender_out = gender_net.forward() gender_idx = gender_out[0].argmax() # 年龄推理 age_net.setInput(blob) age_out = age_net.forward() age_idx = age_out[0].argmax() return AGE_LIST[age_idx], GENDER_LIST[gender_idx] img = cv2.imread("test.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80)) for (x, y, w, h) in faces: # 裁剪人脸区域,注意坐标越界保护 x0, y0 = max(0, x), max(0, y) x1, y1 = min(img.shape[1], x + w), min(img.shape[0], y + h) face = img[y0:y1, x0:x1] if face.size == 0: continue age_label, gender_label = predict_age_gender(face) text = f"{gender_label}, {age_label}" cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(img, text, (x, max(0, y - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("result", img) cv2.imwrite("result.jpg", img) cv2.waitKey(0) cv2.destroyAllWindows()代码的核心逻辑集中在 predict_age_gender 函数里。blobFromImage 生成的是一个 1x3x227x227 的四维数组,对应批量大小 1、3 个通道、高和宽都是 227。性别网络前向推理后输出的 gender_out 是一个 1x2 的数组,argmax 取概率大的位置,0 表示 Male,1 表示 Female。年龄网络同理,输出 1x8,argmax 后对应的索引映射到 AGE_LIST。这里要注意裁剪人脸后必须检查 face.size 是否为零,坐标越界会导致空图,空图送入神经网络虽然有输出但结果毫无意义。
参数上需要留意的是,Haar 检测器返回的检测框坐标以灰度图为基准,而 gray 是从原图转来的,尺寸一致,不需要换算。如果检测框明显偏大或者偏小,可以把检测框向外扩展 padding——比如 x 和 y 各扩大 20%,让人脸区域包含少量额头和头发,这些边缘信息实际上对性别判断有帮助。不过扩展后要确保不超出图像边界,否则会用黑边填充,反而干扰预测。
3.4 跑在视频流上:连续帧的处理逻辑与资源控制
图像单帧跑通之后,很多人会想直接拿摄像头或者视频文件来试。视频流和单张图片的处理逻辑本质相同,差别只在帧的获取和推理频率控制上。如果每一帧都执行完整的检测加两次神经网络推理,普通 CPU 机器很容易掉到 10 FPS 以下,看起来非常卡。
cap = cv2.VideoCapture(0) # 摄像头输入;填视频路径则读取视频文件 if not cap.isOpened(): print("camera open failed") exit(1) frame_idx = 0 while True: ret, frame = cap.read() if not ret: # 视频流拉流偶发中断,跳过这一帧继续读 continue frame_idx += 1 # 每 5 帧做一次完整推理,其余帧直接显示上一帧的结果框 if frame_idx % 5 != 0: cv2.imshow("demo", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break continue # 检测和预测逻辑与单张图像一致,只是把 img 换成 frame gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80)) for (x, y, w, h) in faces: face = frame[max(0, y):y + h, max(0, x):x + w] age_label, gender_label = predict_age_gender(face) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"{gender_label}, {age_label}", (x, max(0, y - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("demo", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()抽帧推理是实际项目里最常用的降负载方案。上面的代码每 5 帧触发一次检测加预测,中间 4 帧直接显示视频画面,不绘制新框。如果你同时跑多个摄像头或者更大的分辨率,可以进一步把检测帧间隔调到 10,或者先把帧缩小到宽度 640 再送检测器。注意 waitKey(1) 的返回值,大于等于 0 时表示按键的 ASCII 码,ord('q') 是退出条件。摄像头读取偶尔会返回 ret=False,这并不代表摄像头断开,简单地跳过当前帧继续循环即可,后面避坑章节会对拉流中断做更详细的重连处理。
4. 常见问题避坑:模型加载、cv2 导入、预测结果异常排查实录
4.1 模型加载报错:prototxt 与 caffemodel 顺序写反,或者路径不存在
现象:执行 readNetFromCaffe 时报错,提示找不到文件,或者报出类似 “Could not open file” 的错误;还有人遇到 cv2.error 提示 prototxt 解析失败。
原因:最常见的是文件路径拼接出了问题。我见过有人直接把模型文件放在和脚本同一个目录,但代码里写的是 models/ 前缀,运行目录不对就找不到文件。另一个非常容易翻车的点是 readNetFromCaffe 的第一个参数应该是网络结构文件 prototxt,第二个参数是权重文件 caffemodel,这两个顺序一旦写反,OpenCV 不会给出明确的“参数顺序错误”提示,而是报解析失败,看起来像模型文件损坏。
解决:统一用 os.path 构造绝对路径,并且打印出来确认存在。自己写一个检查函数,启动时校验四个模型文件都存在,不存在就提前退出,不要在推理到一半才报错。顺序问题上,如果报错信息里出现在某个 layer 解析失败,先检查 prototxt 里是否有 py_str 这类 Python 层——OpenCV 的 DNN 模块不支持自定义 Python 层,遇到这类 prototxt 需要换标准版本。官方的 age_deploy.prototxt 是干净的,但网上流传的某些改版会包含不支持的层。
4.2 opencv 安装成功却找不到 cv2 模块:装错包名和版本冲突的锅
现象:在终端里执行 pip list 能看到 opencv 或者 opencv-python,但运行 import cv2 时直接报 ModuleNotFoundError: No module named 'cv2'。还有人遇到 VSCode 里能运行、终端里不能运行,或者反过来。
原因:这种情况 80% 是环境里装了 opencv-python-headless 或者 conda 的 opencv 包。headless 版本不带 GUI 支持,import 时报错往往更隐蔽;conda 的 opencv 包有时候不会创建 cv2 的 Python 绑定目录。另一个常见原因是 pip 装进了当前用户目录,但 VSCode 解释器指向的是系统全局 Python,两边不是同一个环境,自然找不到。
解决:用 python -c "import sys; print(sys.executable)" 确认当前解释器路径,然后在同一个终端里执行 pip install --upgrade opencv-python。如果之前装过 opencv-python-headless,先 pip uninstall opencv-python-headless 再装。VSCode 用户要确认右下角解释器选的是虚拟环境,不是系统全局。验证是否安装成功,一行代码就够了:
python -c "import cv2; print(cv2.__version__, cv2.__file__)"如果打印出了版本号和路径,说明环境完全没问题。我排查过很多次这类问题,绝大多数时候问题都不在安装本身,而是解释器路径没对齐。这个坑对新手特别不友好,因为报错信息看起来像是 opencv 没装,实际上系统里装了好几个互相冲突的版本。
4.3 预测结果全是 Male / 全是 25-32:很大概率是均值没减或者尺寸设错
现象:代码能跑通,检测框也正常,但无论输入什么人脸,性别永远是 Male,年龄永远是 25-32 这个中间值。换照片也没变化,就像模型是个摆设。
原因:年龄性别模型对输入预处理极其敏感。blobFromImage 里的 mean 参数如果设置成默认值 0,或者写成 (0, 0, 0),网络输入就相当于在训练好的分布上做了偏移,推理输出会退化成某个固定类别。另一个高频错误是把网络输入尺寸写成 224x224,这个模型训练用的就是 227x227,尺寸差 3 个像素会触发 resize,看起来不影响运行,但效果上等于把图像缩放得和训练分布不一致,结果也会偏向中间年龄段。
解决:严格按照前面参数表里的值设置。mean 那三个浮点数要完整抄下去,少一位小数都可能造成偏差。设置完之后可以做个快速验证:找一张明显的大龄男性照片和一张年轻女性照片,跑一遍看输出是否有区分。如果两张照片的输出完全一样,优先检查 mean 和 size 两项。顺便说一个血泪经验:改完参数后要重新加载模型,OpenCV 的 DNN 模块在 setInput 之后不会再读取 prototxt 里的配置,如果你在同一个进程里改了 blobFromImage 参数但没重建 net,跑的还是旧的中间结果缓存。
4.4 contourArea 未定义标识符:接口调用方式与 OpenCV 版本要求错位
现象:在代码里直接写 contourArea(contour) 报错 “contourArea 未定义标识符”,或者写 cv2.contourArea 时提示 module 'cv2' has no attribute 'contourArea'。还有人遇到只报 C++ 层面的编译错误,根本定位不到 Python 代码。
原因:contourArea 是 OpenCV 的 C++ 接口函数,在 Python 里必须写 cv2.contourArea,不能省略 cv2 前缀。如果连 cv2.contourArea 都找不到,说明 OpenCV 版本太旧或者安装有问题。另外年龄性别预测这个项目本身用不到 contourArea,很多人是在做人脸检测后想画轮廓框或者算人脸面积时顺手调用了这个函数。OpenCV 4.2 以上版本对一些接口做了重新组织,部分符号从 imgproc 移到了新模块,如果代码是照着老教程抄的,很容易踩到版本错位的坑。
解决:统一用 cv2.contourArea 的完整调用方式,不要省略模块前缀。如果版本确实太老,用 pip install --upgrade opencv-python 升级到 4.x。针对本项目,建议直接锁定 opencv-python 在 4.5 以上版本,因为后面要做的 YuNet 人脸检测器需要 4.5.4 才支持。升级后如果出现 cv2 导入异常,先卸载再装一次,避免升级过程残留旧文件。另外在 VSCode 里配置时,注意打开的是 Python 文件而不是 C++ 文件,有些报错信息其实是 IDE 的 C++ 插件在解析头文件时给出的,并非运行时错误。
4.5 视频流拉流中断和 CUDA 加速轮子带来的连锁问题
现象:用 VideoCapture 读摄像头或者网络视频流时,跑到一半 ret 一直返回 False,程序陷入死循环;或者界面卡顿明显,画面像幻灯片。还有人为了提速装了带 CUDA 的 opencv-python 预编译轮子,结果 import cv2 直接崩溃。
原因:VideoCapture 读流本身的机制容易出现瞬时失败,尤其是网络摄像头,某个瞬间网络抖动或者带宽不足都会让 read 返回 False,这不代表摄像头物理断开。卡顿问题是每一帧都做完整推理导致的,CPU 在检测和两次网络前向推理上耗时太长时间。CUDA 预编译轮子的问题在于,它的二进制是针对特定版本的 CUDA 和 cuDNN 编译的,你的系统环境如果对不上,加载时会直接报 DLL 加载失败。
解决:为 VideoCapture 写一个简单的重连逻辑,read 返回 False 时不要 break,先尝试重新 open。代码里加一个计数器,连续失败超过比如 30 次才真正退出,避免无限循环。卡顿问题用前面讲的抽帧策略解决,检测间隔拉大一点就行。CUDA 轮子这块,我一般不建议在 CPU 演示阶段碰,因为收益有限但引入的坑很多。等你在 CPU 上把完整链路跑通了,确实需要提速,再去找与本地 CUDA 版本严格匹配的预编译包,装完后立刻用 cv2.getBuildInformation() 确认 CUDA 是否真的被启用。
cap = cv2.VideoCapture(0) fail_count = 0 while True: ret, frame = cap.read() if not ret: fail_count += 1 if fail_count > 30: print("camera lost, give up") break # 尝试重连 cap.open(0) continue fail_count = 0 # 成功读帧后重置计数 # 正常推理逻辑...拉流中断这个坑,最核心的思路是不要让一次临时性失败拖垮整个程序。重连间隔可以按需调整,如果是网络流,建议加一个 sleep 防止频繁重连打爆带宽。记住一点:VideoCapture 不是一种流媒体协议,它只是一个统一的读取接口,底层 RTSP 连接断没断,你只有读到 ret 才知道。所以把重连逻辑当成标配,而不是异常处理。
5. 进阶与验证:从 Haar 换到 YuNet 检测器、抽帧优化与稳定输出
5.1 检测器升级:YuNet 在侧脸和遮挡场景的明显优势
Haar 检测器胜在轻量,但对侧脸、戴口罩、光线复杂的人脸检测效果确实不稳定。年龄性别预测本身的误差已经不小,如果检测框再不准,结果就更没法看。OpenCV 官方 Zoo 里提供的人脸检测模型 YuNet,是一个 ONNX 格式的深度神经网络检测器,从 4.5.4 开始可以通过 cv2.FaceDetectorYN_create 直接调用,不需要额外的依赖。它对人脸关键点也有输出,虽然本项目只用检测框,但关键点信息可以辅助做人脸对齐,进一步提升年龄性别的预测稳定性。
在 CPU 上,YuNet 的单帧检测速度大约比 Haar 慢 10 到 20 毫秒,但换来的准确率提升在复杂场景下非常明显。如果只是对着一张证件照测试,Haar 就够用;如果你要处理的是监控画面、多人聚会照片,我建议直接用 YuNet 替换掉 Haar。替换代码非常简单,关键就在模型的初始化和调用方式上。
# 初始化 YuNet 检测器 face_detector = cv2.FaceDetectorYN_create( "face_detection_yunet_2023mar.onnx", # ONNX 模型文件 "", # 配置文件不需要 (320, 320), # 网络输入尺寸 score_threshold=0.9, # 置信度阈值,过滤低分框 nms_threshold=0.3, # NMS 后的保留策略 top_k=5000 # 最多保留的检测框数量 ) # 推理前先设置输入尺寸为当前图像的实际尺寸 face_detector.setInputSize((img.shape[1], img.shape[0])) # 检测结果 faces 是 Nx15 的数组 num_faces, faces = face_detector.detect(img) if num_faces and faces is not None: for face in faces: # 前四个值是 x, y, w, h x, y, w, h = face[:4].astype(int) # 后续的推理逻辑与 Haar 版完全一致 face_img = img[max(0, y):y + h, max(0, x):x + w] age_label, gender_label = predict_age_gender(face_img)score_threshold 设成 0.9 是为了只保留高置信度的人脸框,对于模糊场景可以降到 0.8。num_faces 为 0 时不执行后续逻辑,避免对空数组取下标。YuNet 的检测框比 Haar 更贴合人脸轮廓,不需要向外扩 padding,直接裁剪就行。换到 YuNet 之后,年龄性别预测的准确率通常会有小幅提升,主要原因是检测框更准,人脸区域里的背景干扰更少。你可以做一个简单的对比测试:同一张照片分别用两个检测器跑一遍,打印出年龄预测结果和置信度,你很快能看出差异。
5.2 让输出更稳定:滑动窗口化处理和多帧决策
视频流里直接逐帧显示预测结果,你会发现结果跳来跳去——同一张脸在这一帧预测成 25-32,下一帧变成 38-43。这是单帧模型的正常表现,解决方法是把小时间窗口内的预测结果聚合,取多帧的统计结果再绘制。性别用多数投票,年龄用出现次数最多的年龄段,窗口大小取 10 到 15 帧比较合理。
我自己的做法是给每张人脸维护一个环形缓冲区,保留最近 10 帧的性别和年龄预测值。显示时统计每个类别的出现次数,最多的作为最终显示结果。这样即使某几帧预测偏差很大,也不会让画面上的标签频繁跳动。值得强调的是,性别预测的稳定性通常好于年龄预测,因为年龄分类的边界本身是模糊的——25 岁和 32 岁在视觉上很难区分。理解这一点能帮你合理设定预期:这个系统的价值在于粗粒度的人脸属性分析,而不是精确定年。
验证整套流水线是否正常,我的习惯是拿一组包含不同年龄段、不同性别的照片做一次批量测试,统计每个人脸预测结果的分布。如果某张照片连续多次跑出的性别结果相反,可以先检查检测框是否准确框住人脸,再检查光照和图像质量。从那以后我每次跑这套 demo,都会先做一轮 3 秒的冒烟验证:用一张自己拍的正脸照片,确认输出不是固定值,再切到视频流。这个小习惯帮我避掉了至少一半的预处理参数错误,希望帮到你。
本文还有配套的精品资源,点击获取