☰
OpenCV DNN年龄性别预测:人脸检测到实时客流统计
2026/10/27 21:49:18 网站建设 项目流程

简介:这份资源面向希望用 OpenCV 快速实现人脸属性分析的开发者,聚焦通过 DNN 模块部署卷积网络,从图像或摄像头画面中检测人脸并预测年龄区间与性别,适合入门实践、课程演示和轻量级应用搭建。包内提供年龄、性别两类模型权重与网络配置,附带级联人脸检测器、C++ 示例源码和工程文件,可直接编译运行,并含有测试图片便于对照复现实验。压缩包共 53 个文件,以模型权重、网络配置、级联检测器、源码、工程文件及调试中间产物为主,整体约 106.68MB。已有 338 人学习,读者可据此理清人脸检测、模型加载、输入预处理、前向推理和结果展示的完整链路,也能观察年龄区间与性别概率的输出形式,并在此基础上替换模型、调整参数或迁移到其他视觉任务。

1. 门店客流看板的那个下午:OpenCV 年龄和性别预测能覆盖什么范围

去年帮朋友做一个线下门店的客流看板,需求朴素得不行:不用认人,屏幕上能滚动显示“进来的是男是女、大概多大”就够。试了几种方案,最后落地的是 OpenCV 年龄和性别预测这条链路——DNN 模块加载三个现成的 Caffe 模型,人脸检测先出框,框里抠出来的脸送进两个分类头,一个出年龄段,一个出性别。普通笔记本 CPU 就能跑,不用装深度学习框架,也不用显卡。这份资源把整条流水线拆得比较完整:摄像头、单张图片、视频文件三种输入都能喂,输出是带框带标签的画面。适合手上有摄像头想做实时统计的人,也适合刚啃完 OpenCV 基础 API、想找个完整 demo 练手的人。丑话说前面,它不是人脸识别系统,别指望它认清“这是谁”。

2. 技术选型与目录结构:三个模型怎么串成一条推理流水线

这套东西的骨架其实很轻,全部重量都在三个模型文件上。人脸检测一个 SSD,性别分类一个 Caffe 分类网,年龄分类再一个 Caffe 分类网。它们之间没有共享层,也没有端到端的联合训练,就是最朴素的串行调用:检测器给框,裁剪器按框抠图,两个分类头各自跑一遍前向。理解这一点,后面所有代码都好读了——它更像三条独立的推理,被一段业务代码粘起来。

2.1 为什么用 OpenCV DNN 而不是自己训一个网络

最直接的理由是部署成本。自己训练一个多任务网络,哪怕只是微调,也要面对框架依赖、权重格式转换、推理后端适配这一串问题,最后交付到别人机器上还得打包一堆运行库。OpenCV 的dnn模块提供readNetFromCaffe、readNetFromONNX这些入口,把训练好的模型直接读进来前向,运行期只依赖opencv-python一个包,这对“装个 Python 就能跑”的场景太友好了。

代价也要讲明白。现成模型的精度是有天花板的:年龄段输出的是 8 个离散区间而不是连续年龄,遇到侧脸、遮挡、强逆光,结果会明显漂移。我做门店那个项目时就发现,戴帽子的顾客年龄普遍被预测偏大一档,这是训练数据分布决定的,调参救不回来。所以这套东西适合做“分布统计”而不是“个体判断”——统计一百个人的性别比例大致靠谱,但盯着某一个人看,翻车概率不低。

另一层考虑是硬件。dnn默认走 CPU,也支持切到 CUDA 或 OpenVINO 后端。一块常见的边缘盒子跑 640×480 的画面,检测加两个分类能到十几帧,做人流统计完全够。真要上高分辨率多路视频,再考虑setPreferableBackend换后端,这个后面第 6 章会展开。

2.2 目录结构:把检测、裁剪、推理、绘制拆成四件事

这类 demo 最常见的写法是把所有逻辑塞进一个main.py,两百行流水账,改起来很痛苦。这份资源的划分方式我比较认可,按职责拆成四块,每块单独能测:

路径职责关键内容
models/模型文件存放人脸检测deploy.prototxt+res10_300x300_ssd_iter_140000.caffemodel;性别deploy_gender.prototxt+gender_net.caffemodel;年龄deploy_age.prototxt+age_net.caffemodel
src/detector.py只负责人脸检测封装FaceDetector,输入一帧,输出若干归一化转像素坐标的框
src/predictor.py只负责属性推理封装AgeGenderPredictor,输入一张裁剪好的人脸图,输出性别、年龄段及各自置信度
src/pipeline.py粘合层按框图裁剪、加边距、结果平滑、多目标追踪的占位逻辑
src/visualize.py绘制画框、写标签、叠 FPS,中文字体兜底
configs/default.yaml参数外置置信度阈值、padding 比例、平滑窗口大小、跳帧间隔
app.py入口解析命令行参数,按图片/视频/摄像头三种模式分流

这种拆法的好处是调试时能逐段验证。人脸框不准,只改detector.py的阈值;年龄乱跳,只动pipeline.py的平滑窗口,互不干扰。我一般还会在src/下额外放一个bench.py,用一段固定视频跑循环计时,改完参数先看这个数,比肉眼盯着画面猜快得多。

2.3 依赖与最小可跑环境

依赖少是这套方案最大的优点,requirements.txt里基本就三行:

opencv-python>=4.5 numpy pyyaml

opencv-python是带 DNN 模块的完整包,别装成opencv-python-headless,后者在部分平台上裁掉了一些 GUI 相关的绑定,imshow会直接报错。如果你想在服务端跑批处理不弹窗,那反而应该用 headless 版本。这台机器上要跑起来,Python 3.7 以上、内存 2GB 以上就够,三个模型加起来体积不大,加载时占几百兆内存。

摄像头模式需要一个能被系统识别的视频设备,Linux 下通常是/dev/video0,Windows 下是设备索引 0。如果是在容器里跑,记得把设备挂进去,否则VideoCapture(0)会静默返回空帧——这个坑后面第 5 章单独说。

3. 核心链路拆解:从一帧原图到“Male (25-32)”的完整代码

这一章是整份资源最有价值的部分。三段代码分别对应检测、裁剪推理、结果聚合,每段都能独立跑,也能拼起来。写的时候我尽量保留原实现的字段命名,方便你对照着改。

3.1 人脸检测:SSD 出框与归一化坐标还原

检测器用的是单阶段 SSD,输入固定 300×300,输出是一串[batch, 1, N, 7]的张量,最后一维的 7 个数依次是:图像 ID、类别 ID、置信度、x1、y1、x2、y2(后四个是相对原图的归一化坐标)。很多人第一次跑会画出一堆乱七八糟的框,多半就是忘了把归一化坐标乘回原图宽高。

import cv2 class FaceDetector: """SSD 人脸检测器:只出框,不做识别""" def __init__(self, prototxt_path, model_path, conf_threshold=0.6): # readNetFromCaffe 读的是 Caffe 的 deploy 结构和权重 self.net = cv2.dnn.readNetFromCaffe(prototxt_path, model_path) self.conf_threshold = conf_threshold self.input_size = (300, 300) def detect(self, frame): h, w = frame.shape[:2] # blobFromImage 自己会做 resize,不需要提前 cv2.resize 一遍 blob = cv2.dnn.blobFromImage( frame, scalefactor=1.0, size=self.input_size, mean=(104.0, 177.0, 123.0), # SSD 训练时的 BGR 均值,写死不要改 swapRB=False, crop=False, ) self.net.setInput(blob) detections = self.net.forward() boxes = [] for i in range(detections.shape[2]): confidence = float(detections[0, 0, i, 2]) if confidence < self.conf_threshold: continue # 归一化坐标乘以原图尺寸,再夹到图像范围内 x1 = int(detections[0, 0, i, 3] * w) y1 = int(detections[0, 0, i, 4] * h) x2 = int(detections[0, 0, i, 5] * w) y2 = int(detections[0, 0, i, 6] * h) boxes.append(( max(0, min(x1, w - 1)), max(0, min(y1, h - 1)), max(0, min(x2, w - 1)), max(0, min(y2, h - 1)), confidence, )) return boxes

几个参数值得单独说。conf_threshold默认给 0.6,是个偏保守的值:调低到 0.4,远处的小脸能检出来,但误检会明显变多,经常把衣领、手掌框成脸;调到 0.8 以上,画面里人一多就会漏。门店那种半身入镜的场景,0.5 到 0.6 之间最稳。mean=(104.0, 177.0, 123.0)是 SSD 作者训练时用的通道均值,这三数字不是随便填的,换成 ImageNet 的均值,置信度会整体往下掉一截,很玄学但确实这样。

还有一个细节:blobFromImage内部已经包含 resize,外面再套一层cv2.resize是多余的,不但慢,还可能因为插值方式不同导致框位置有细微偏差。我在早期版本里就犯过这个错,框总是比脸大一圈,查了半天才发现是双重缩放。

3.2 预处理:227×227 与那组不能改的均值

抠出来的脸不能直接送进分类网。两个 Caffe 模型的输入是 227×227 的 BGR 图,减均值、不做通道交换、不做中心裁剪,这三件事必须和训练时完全一致。性别和年龄这两个模型的均值是另一组数,和检测器那组不一样,混用是新手最容易踩的坑。

import numpy as np AGE_BUCKETS = ["(0-2)", "(4-6)", "(8-12)", "(15-20)", "(25-32)", "(38-43)", "(48-53)", "(60-100)"] GENDER_LIST = ["Male", "Female"] # 分类模型专用均值,和检测器的 (104,177,123) 不是一回事 MODEL_MEAN = (78.4263377603, 87.7689143744, 114.895847746) class AgeGenderPredictor: def __init__(self, age_prototxt, age_model, gender_prototxt, gender_model): self.age_net = cv2.dnn.readNetFromCaffe(age_prototxt, age_model) self.gender_net = cv2.dnn.readNetFromCaffe(gender_prototxt, gender_model) def predict(self, face_roi): if face_roi is None or face_roi.size == 0: return None blob = cv2.dnn.blobFromImage( face_roi, scalefactor=1.0, size=(227, 227), mean=MODEL_MEAN, swapRB=False, # 模型按 BGR 训练,千万别设成 True crop=False, # 直接拉伸,不做中心裁剪 ) # 性别:2 类,取 argmax self.gender_net.setInput(blob) gender_preds = self.gender_net.forward()[0] gender_idx = int(np.argmax(gender_preds)) # 年龄:8 个区间,同样是 argmax self.age_net.setInput(blob) age_preds = self.age_net.forward()[0] age_idx = int(np.argmax(age_preds)) return { "gender": GENDER_LIST[gender_idx], "gender_conf": float(gender_preds[gender_idx]), "age_range": AGE_BUCKETS[age_idx], "age_conf": float(age_preds[age_idx]), }

AGE_BUCKETS的顺序绝对不能动,它对应模型输出层 0 到 7 号神经元,改一个顺序,整片结果全错。这个顺序来自训练集的年龄分组,跨度不均匀是正常的:0-2 和 4-6 这种小孩子区间切得细,因为儿童面部特征变化快;60-100 直接糊成一大段,因为老年人特征区分度低,模型本身也分不出来。

swapRB=False这行我见过太多次被改成True。改了之后代码不报错,跑得也欢,就是结果离谱——男性大面积被判成女性。因为 OpenCV 读进来是 BGR,模型训练时也是 BGR,中间转一次 RGB 等于把红蓝通道对调了,模型看到的是“蓝色的人脸”。

3.3 裁剪加边距:框太紧会让分类精度掉一档

检测器给出的框是贴着五官的,直接拿去裁剪,额头和下巴会被切掉一部分。分类网训练时看到的样本通常带一点背景,框太紧反而是分布外输入。实际做法是按框的宽高各外扩一定比例,再夹到图像边界内。

def crop_with_padding(frame, box, pad_ratio=0.15): """按检测框裁剪人脸,四周外扩 pad_ratio,返回裁剪图和修正后的框""" h, w = frame.shape[:2] x1, y1, x2, y2, conf = box bw, bh = x2 - x1, y2 - y1 px, py = int(bw * pad_ratio), int(bh * pad_ratio) nx1, ny1 = max(0, x1 - px), max(0, y1 - py) nx2, ny2 = min(w, x2 + px), min(h, y2 + py) if nx2 - nx1 < 20 or ny2 - ny1 < 20: return None, None # 太小的脸直接放弃,硬推也是垃圾结果 return frame[ny1:ny2, nx1:nx2], (nx1, ny1, nx2, ny2)

pad_ratio给 0.15 是我在几个场景里试出来的折中值。给 0 的话,框紧贴脸颊,年龄预测经常偏小;给到 0.4,背景占比太高,模型会开始受环境影响,白墙前面的人容易全被判成同一档。0.1 到 0.2 之间差异不大,选一个固定住就行,别每帧动态变,那样平滑逻辑会失效。

那个尺寸下限也重要。视频里偶尔会检出十几个像素的小脸,这种图送进 227×227 的网络等于把噪点放大,输出基本是随机的,还会把整帧的统计带偏。直接丢掉比硬推划算。

3.4 结果平滑与绘制:让年龄别每帧乱跳

如果只用单帧 argmax,你会看到一个人的年龄标签在两三档之间疯狂横跳。这不是模型坏了,是相邻类别的概率本来就很接近。工程上的解法很土但有效:给每个目标维护一个滑动窗口,取窗口内出现次数最多的类别。

from collections import deque, Counter class ResultSmoother: """按目标 ID 维护滑动窗口,取众数,压住标签抖动""" def __init__(self, window=12): self.window = window self.buffers = {} def update(self, track_id, result): buf = self.buffers.setdefault(track_id, deque(maxlen=self.window)) buf.append((result["gender"], result["age_range"])) gender = Counter(g for g, _ in buf).most_common(1)[0][0] age = Counter(a for _, a in buf).most_common(1)[0][0] return {"gender": gender, "age_range": age, "stable": len(buf) >= self.window // 2}

window=12意味着大约最近 12 次推理参与投票。太小压不住,太大则响应迟钝,一个人走出画面了标签还挂着。stable这个字段是给自己看的:窗口没攒够一半数据之前,标签在 UI 上可以浅色显示,避免刚出现就给出一个不可靠的结论。

track_id从哪来是个问题。完整方案要接一个多目标跟踪器,但这份资源里给了个轻量替代:用上一帧的中心点距离做最近邻匹配,实现二十来行,够用。如果场景里人不多于三五个,这个土办法的准确率完全能接受。

绘制部分没什么好讲的,cv2.rectangle加cv2.putText就完事。唯一要提醒的是中文标签。OpenCV 内置字体不支持中文,直接写会变成一堆问号。常见做法是用PIL的ImageFont.truetype加载一个中文字体文件,把 BGR 转成 RGB 交给 PIL 画完再转回来;或者干脆全用英文标签,省事。我一般选后者,Male (25-32) 0.87这种格式业务方也完全能读。

4. 跑起来:三种输入模式的启动方式与结果解读

代码看完了,接下来是让它动起来。这一章按“准备模型 → 选输入模式 → 读输出”的顺序走,每一步都给可复制的命令。

4.1 模型文件摆放与加载检查

模型是这套方案唯一的硬依赖,缺一个都跑不起来。三个 prototxt 定义网络结构,三个 caffemodel 存权重,必须成对存在,版本也要匹配——拿 A 版本的 prototxt 配 B 版本的权重,readNetFromCaffe在加载阶段不一定报错,但forward()时会抛出维度不匹配的异常,错误信息还相当晦涩。

我一般会在入口加一段启动自检:

import os MODEL_FILES = [ "models/deploy.prototxt", "models/res10_300x300_ssd_iter_140000.caffemodel", "models/deploy_gender.prototxt", "models/gender_net.caffemodel", "models/deploy_age.prototxt", "models/age_net.caffemodel", ] def check_models(base_dir="."): missing = [p for p in MODEL_FILES if not os.path.isfile(os.path.join(base_dir, p))] if missing: raise FileNotFoundError("缺少模型文件:\n" + "\n".join(missing))

这个检查看起来啰嗦,但省下来的调试时间很值。模型文件下载不全的情况太常见了,尤其是 caffemodel,动辄几十兆,网络一抖就是个残缺文件,加载时才报“Unsupported Lua type”之类的怪错。

路径尽量用相对项目根目录的写法,别写绝对路径。我见过把/home/xxx/Downloads/...写死在代码里的版本,换台机器直接全线崩溃,属于典型的血泪教训。

4.2 图片、视频、摄像头三种模式的参数设计

入口脚本用argparse分三种模式,靠--source一个参数区分:传数字当摄像头索引,传文件路径当图片或视频。判断逻辑是看扩展名,图片走单帧流程,其余走循环读帧。

import argparse import cv2 def build_parser(): p = argparse.ArgumentParser() p.add_argument("--source", default="0", help="0 表示默认摄像头;也可传图片或视频文件路径") p.add_argument("--conf", type=float, default=0.6, help="人脸检测置信度阈值") p.add_argument("--pad", type=float, default=0.15, help="人脸框外扩比例") p.add_argument("--skip", type=int, default=1, help="每 N 帧推理一次,1 表示不跳帧") p.add_argument("--save", default="", help="输出视频保存路径,留空则不保存") return p def open_source(source): # 纯数字当成摄像头索引,否则当成文件 if source.isdigit(): cap = cv2.VideoCapture(int(source)) else: cap = cv2.VideoCapture(source) if not cap.isOpened(): raise RuntimeError(f"无法打开输入源:{source}") return cap

参数含义逐个说清。--conf就是 3.1 里那个检测阈值,画面里人多或者光线差的时候往下调一点。--pad对应 3.3 的裁剪外扩,一般不用动。--skip是个性能杠杆:设成 3 表示每三帧才跑一次完整推理,中间帧沿用上一次的框和标签,帧率能翻两三倍,代价是快速移动时框会滞后。做人流统计设 3 到 5 完全够用,做实时交互就得设 1。

摄像头模式记得设分辨率。默认的 640×480 在有些设备上会给出 1920×1080 的画面,检测器内部虽然会缩到 300×300,但每帧的blobFromImage和绘制开销都会上去,白白浪费算力:

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

4.3 输出怎么读:年龄段是区间统计,性别看的是置信度

跑起来之后画面上的标签长这样:Male (25-32) 0.87。后面那个数字是性别分类的 softmax 概率,不是年龄的。年龄的置信度我没往画面上放,因为它常年偏低且不具备解释性,放上去只会让人误以为“0.3 就是不准”。

年龄段要按区间理解,(25-32)不是“这个人 28 岁”,而是“模型认为他落在 25 到 32 这一段”。实际用的时候,我一般把 8 档再归并成 4 组:未成年、青年、中年、老年,统计粒度粗一点,但稳定性高得多。门店客流看板就是按这四组出图的,业务方看着也直观。

性别的置信度低于 0.7 时,建议在 UI 上标注为“不确定”而不是硬给一个结论。侧脸、戴口罩、逆光这几类画面里,置信度普遍掉到 0.6 以下,这时候的 argmax 基本是抛硬币。加一个阈值兜底,整体观感会好很多。

如果开了--save,输出视频的帧率和输入保持一致,但要注意:跳帧模式写出去的是“每帧都画框、部分帧才推理”的结果,视觉上会有轻微滞后,别以为是编码问题。

5. 避坑与排查:跑不通、框不准、结果乱跳的现场处理

这一章记录的是实际调试里反复出现的几类问题,按启动阶段、运行阶段、结果层三个层面分开。每条都按“现象 → 原因 → 解决”写,遇到对得上的直接照着改。

5.1 启动阶段:模型加载失败与设备打不开

现象一:cv2.error: Unsupported Lua type或者Cannot determine the file type。

原因是 prototxt 和 caffemodel 版本不匹配,或者下载下来的 caffemodel 是个残缺文件。解决方式是先比对各文件的实际大小,几个 caffemodel 加起来的体积和模型来源页面标注的数量级应该对得上,明显偏小就是没下完。也可以直接用cv2.dnn.readNetFromCaffe单独加载某一对文件做隔离测试,逐个排除。

现象二:RuntimeError: 无法打开输入源:0。

在容器或远程环境里跑摄像头时最常见。容器默认拿不到宿主机的视频设备,VideoCapture(0)会静默失败。解决方式是启动容器时把设备映射进去,命令行里加--device=/dev/video0,然后确认容器内用ls /dev/video*能看到设备节点。另一个可能是索引不对,笔记本上外接 USB 摄像头时索引常常从 1 开始,用--source 1试一下。

现象三:AttributeError: module 'cv2' has no attribute 'dnn'。

装成了精简版或者版本太老的 opencv 包。解决方式是卸掉重装opencv-python,别装opencv-contrib-python-headless之外的那些变体。装完python -c "import cv2; print(hasattr(cv2, 'dnn'))"打一下,True才算对。

5.2 运行阶段:卡顿、坐标错位与内存增长

现象四:帧率只有个位数,风扇狂转。

不一定是模型太重。先确认是不是每帧都在重复调用readNetFromCaffe——把网络加载写在处理循环里的代码我见过不止一次,那等于每帧都从磁盘读模型。解决办法是把FaceDetector和AgeGenderPredictor的实例化提到循环外面,只做一次。其次确认画面分辨率,1080p 的输入比 480p 慢三倍以上,而检测器最终还是要缩到 300×300,中间那步纯属浪费。

现象五:框画偏了,整体往左上或右下平移。

坐标系混了。检测器输出的框是相对原图归一化的,但如果在推理前把帧缩放过,框就得按缩放后的尺寸还原,而裁剪取的是原图。解决方式是只保留一处缩放,检测器内部由blobFromImage负责,外层代码始终面对原图,这样框的坐标和裁剪的坐标天然一致。这个坑我在第 3.1 节提过,值得再强调一遍。

现象六:跑十来分钟内存涨到几个 G。

滑动窗口的字典只增不减。ResultSmoother.buffers里每个track_id都持有一个固定长度的deque,人走后不清理,条目越攒越多。解决方式是加一个简易的过期机制:记录每个 ID 最后一次出现的时间,每隔一段时间扫一遍,超过三十秒没更新的直接删掉。

5.3 结果层:年龄横跳与性别判反

现象七:年龄标签在两三档之间来回跳。

单帧 argmax 的正常表现,相邻类别概率接近时必然抖动。解决方式就是 3.4 节的滑动窗口取众数,窗口给 10 到 15。如果加了平滑还跳,检查一下track_id是不是每帧都在变,ID 一换窗口就重置了,等于没平滑。

现象八:明显是男性被判成 Female,而且置信度很高。

通道顺序搞反了。检查blobFromImage里是不是写了swapRB=True,改成False。另一可能是均值写错,把检测器那组(104.0, 177.0, 123.0)用到了分类模型上,这会让输入分布严重偏离训练分布,输出的置信度还特别“自信”,属于典型的黑匣子式错误。

现象九:一张图里三个人,只有两个出标签。

三种可能。一是--conf阈值太高,把侧脸漏掉了,往下调到 0.45 试试;二是小脸被 3.3 节那个尺寸下限过滤掉了,这个是有意为之,不建议为了多检出而放开;三是检测框之间有重叠,可视化时后画的框盖住了前面的标签,改一下标签的纵向偏移量就行。

6. 二次开发:从能跑的 demo 到勉强能交付的工具

把这个 demo 改造成真正能用的东西,我走过两条路线,一条是性能路线,一条是精度路线,方向完全相反,先想清楚要哪头。

性能这块最见效的是跳帧加跟踪。--skip 3配合中心点最近邻匹配,帧率能从 8 帧推到 20 帧以上,而统计结果的偏差几乎看不出来,因为统计本来就是按分钟聚合的。再进一步是把dnn后端切到 OpenVINO,net.setPreferableBackend(cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE)加net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU),在 Intel 平台上通常还有明显提升。不过这条路径绑定硬件,换到别的芯片上就得重来,做产品前先确认部署环境。

精度这块有两条我试过有效的改动。一是把 SSD 检测器换成一个更稳的人脸检测方案,检测质量直接决定后面两个分类的上限——框歪了,再好的分类头也救不回来。二是把 8 档年龄归并成 4 组之后再输出,同时在 UI 上只展示性别比例和年龄分布饼图,不展示个体判断。这种“把不确定性藏起来”的做法听起来像在回避问题,但对统计类场景来说,结论的可信度反而更高。

还有一个容易被忽略的改造点是配置外置。configs/default.yaml里那几个参数——置信度、padding、跳帧、平滑窗口——不同场景的最优值差别很大。门店入口光比较亮,阈值可以给 0.65;地下车库光线差,0.45 才检得出来。把这些做成配置文件,现场调试时改文件重启就行,不用重新打包。

detector: conf_threshold: 0.6 input_size: [300, 300] crop: pad_ratio: 0.15 min_face_px: 20 predict: smooth_window: 12 skip_frames: 3 output: show_fps: true label_lang: en

最后给个验证方法。改完参数别只看一帧画面,用一段固定的视频反复跑,输出一个统计文本:总帧数、平均推理耗时、检出人脸的帧占比、性别比例。这四组数字对比着看,比肉眼判断靠谱得多。我在门店项目里就是靠这个表格发现,把阈值从 0.6 降到 0.5 之后检出率涨了 18%,但性别比例从 61:39 变成 55:45,明显是误检把人脸框到了背景上,最后又调回去了。

这类模型驱动的项目,参数之间全是耦合的,改一个地方经常会牵动另一头。从那以后我每次调完参都强制走一遍固定素材的回归对比,宁可多花二十分钟,也不想在上线当天才发现比例全歪了。这套源码和模型放在一起就能跑,拿去对着自己的场景慢慢磨,比从零搭要省太多事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询