news 2026/11/6 3:55:27

OpenCV人脸识别实战:从Haar/DNN检测到LBPH训练与实时识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV人脸识别实战:从Haar/DNN检测到LBPH训练与实时识别

简介:这份压缩包面向具备Python基础、希望动手掌握OpenCV人脸检测与识别流程的学习者,围绕明星人脸图像展开实战演练,帮助理解Haar级联分类器、预处理、特征提取与匹配等机器视觉关键环节。包内共120个文件,约51.99MB,以110张jpg和5张jpeg人脸样本图片为主,另含2个xml级联分类器文件、1个py脚本、1个txt说明和1个png辅助图,可用于加载模型、批量测试与参数对比。已有464人学习,说明其在入门实战场景中具有一定参考价值。读者可借助脚本串联灰度化、直方图均衡化、detectMultiScale检测等步骤,结合样本图片观察不同参数下的检测效果,并进一步参考LBPH、EigenFace、FisherFace等特征提取与匹配思路,完成从检测到识别的实践闭环。

1. 从一张糊掉的抓拍说起:opencv 人脸识别这套包到底解决什么问题

上个月帮一个做园区考勤的朋友看现场,摄像头装在门口斜上方,逆光,人走过去只留半张侧脸。他们拿网上一份 demo 改了两天,检测框一会儿有一会儿没有,识别结果随机跳号。问题不在于算法多高级,而在于整条链路没人拆开看过:检测用哪个模型、样本怎么采、标签怎么对、阈值定在哪一步。这份 opencv 人脸识别实战操作包,价值就在于把这条链路摊平了——采集、训练、识别三个脚本各自独立,Haar 级联和 DNN 两条检测路线都保留,识别端用 LBPH,纯 CPU 就能跑,不用显卡。适合三类人:要交课程设计或毕设的学生、需要给小场景快速搭离线原型的工程师、以及想搞明白机器视觉里人脸从像素走到 ID 这条线的初学者。python 环境装好就能动手,但想跑出能用的结果,后面几章的参数和坑得先过一遍。

2. 环境与目录先理顺:两条检测路线选错了后面全是白干

拿到压缩包之后最忌讳的动作是双击解压、直接python xxx.py。人脸识别这套东西对环境比一般脚本敏感得多,cv2装错一个包,跑到训练那一步才报错,前面采集的一百多张样本全白采。这一章先把依赖装对、把目录职责看清楚,再决定检测走哪条路线,后面所有操作都建立在这个基础上。

2.1 装 opencv-contrib 而不是 opencv-python

很多人第一反应是pip install opencv-python,装完cv2.imshow能用,就以为环境好了。但 LBPH 识别器在cv2.face这个子模块里,它属于 contrib 扩展包,主包根本没有。我见过最快的翻车方式是:主包和 contrib 包同时装进同一个环境,后装的把先装的覆盖掉一半,cv2.__version__看着正常,cv2.face时有时无。

# 先清干净,两个包不能共存 pip uninstall -y opencv-python opencv-python-headless opencv-contrib-python # 只装 contrib 版本,它已经包含主包全部功能 pip install opencv-contrib-python # 顺手把科学计算和可视化依赖补齐 pip install numpy matplotlib

装完必须验证,别凭感觉。下面这段检查脚本我每次搭环境都会跑一遍,它同时确认版本、cv2.face是否可用、Haar 模型目录是否存在,三个条件缺一个都说明环境有问题。

import cv2 import os print("cv2 版本:", cv2.__version__) print("cv2 安装路径:", cv2.__file__) # 关键检查:LBPH 识别器所在模块 assert hasattr(cv2, "face"), "缺少 cv2.face,说明装的不是 opencv-contrib-python" recognizer = cv2.face.LBPHFaceRecognizer_create() print("LBPH 创建成功:", recognizer) # Haar 级联模型随包分发,路径由 cv2.data 提供 haar_dir = cv2.data.haarcascades print("级联模型目录:", haar_dir) print("正面人脸模型存在:", os.path.exists(haar_dir + "haarcascade_frontalface_default.xml"))

cv2.__file__那一行是关键,输出的路径里如果带contrib字样基本就对了;如果指向一个叫cv2的普通目录而你又装过两个包,那就得回退重装。cv2.data.haarcascades是 OpenCV 内置的模型目录常量,它会跟着安装位置走,比手写绝对路径靠谱得多,换台机器也不用改代码。

虚拟环境这一步别省。人脸识别项目往往要配numpy、matplotlib、可能还有flask做接口,跟系统里其他项目的依赖打架是常事。python -m venv venv建一个,激活之后再装上面那些包,出问题直接删目录重来,成本最低。

2.2 目录职责拆开看:三个脚本各管一段

这份包的结构是典型的「三段式」:采集负责把摄像头画面切成人脸小图落盘,训练负责把小图读进来喂给 LBPH 生成模型文件,识别负责加载模型做实时推理。三条链路之间只通过磁盘文件耦合,这个设计看着土,实际非常好用——采集中断了不用重训,模型效果差可以单独换数据集重训,识别端可以脱离摄像头用图片跑回归测试。

脚本角色输入输出出错时的典型症状
样本采集摄像头实时帧每人一个子目录,内含灰度小图样本糊、人脸框偏、目录为空
模型训练数据集目录树.yml模型文件 + 标签映射表训练报类型错、识别永远返回同一 ID
实时识别模型文件 + 摄像头画面叠加姓名框框在但名字错、帧率掉到个位数
单图测试模型文件 + 静态图片终端打印 ID 与置信度同一张图两次结果不同

我要强调标签映射表这一项。很多人训练完只存.yml,标签和姓名的对应关系靠代码里写死一个列表。下次数据集目录顺序变了,模型认的人就全错位,而且错得很安静——不报错,只是把 A 认成 B。常见做法是训练时顺手把一个id -> 姓名的 JSON 落盘,识别时读它,这是整套流程里最便宜的一颗后悔药。

2.3 Haar 与 DNN 两条检测路线的取舍

包里同时留了两套检测代码,不是冗余。Haar 级联是 OpenCV 自带的经典方案,靠积分图加速的矩形特征加 AdaBoost 级联分类器,优点是模型小、CPU 上快、开箱即用;缺点是侧脸和遮挡基本没戏,逆光容易漏检。DNN 那套是 SSD 结构的人脸检测网络,输入固定 300×300,对角度和光照的鲁棒性明显好一截,代价是要额外加载两个模型文件,跑起来更吃 CPU。

维度Haar 级联DNN SSD
模型来源随 OpenCV 分发,路径由cv2.data给出需要 prototxt 与 caffemodel 两个文件
正面清晰人脸快且准准,但慢 2~5 倍
侧脸、低头漏检率高明显更好
小脸(小于 60 像素)需调minSize,容易漏缩放输入后仍可检出
参数调试成本scaleFactor/minNeighbors影响大主要调置信度阈值
适用场景门禁、考勤、固定机位近距离抓拍、监控、角度不固定的画面

我的建议是:先用 Haar 把全流程跑通,确认数据集、训练、识别三段没问题,再换 DNN 提升鲁棒性。反过来做,一旦结果不准,你分不清是检测框歪了还是 LBPH 样本不够,排错会非常痛苦。

DNN 那套的加载方式如下,注意blobFromImage的第三个参数必须和模型训练时的输入尺寸一致,均值减掉的三个数也不能改,改了准确率会莫名其妙往下掉。

import cv2 # 两个模型文件放在包内指定目录,按实际路径改 net = cv2.dnn.readNetFromCaffe( "models/deploy.prototxt", "models/res10_300x300_ssd_iter_140000.caffemodel" ) def detect_dnn(frame, conf_thres=0.6): h, w = frame.shape[:2] # 1.0 是缩放系数,不在这里做归一化;(300,300) 是网络固定输入 # 后三个数是 BGR 三通道均值,必须与训练时保持一致 blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections = net.forward() boxes = [] for i in range(detections.shape[2]): conf = detections[0, 0, i, 2] if conf < conf_thres: continue # 网络输出是 0~1 归一化坐标,乘回原图尺寸 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((x1, y1, x2 - x1, y2 - y1)) return boxes

conf_thres默认 0.6 是我在一个室内光照场景下调出来的,逆光严重的画面可以降到 0.5 换召回率,代价是误检变多。检测框坐标一定要裁剪到图像范围内,SSD 偶尔会输出负数或越界的坐标,直接拿去切 ROI 会抛异常。

提示:DNN 模型的输入尺寸写死在网络结构里,改成 640×640 不会更快也不会更准,只会报维度不匹配。

3. 采集自己的样本:从摄像头读帧到 200×200 灰度图落盘

识别准不准,八成取决于这一步。LBPH 本质上是拿局部纹理的直方图做比对,样本里光照、角度、尺度越统一,比对的分布越稳定。我见过有人拿公开人脸库混着自己的摄像头样本一起训,结果现场一个都认不出来——不是算法不行,是两种来源的图像分布差太远。

3.1 读帧、检测、裁 ROI、落盘

采集脚本的核心逻辑是:读一帧,检测人脸,裁出 ROI,缩放成固定尺寸,按序号存盘。中间加一个时间间隔控制,避免连续几帧几乎是同一张图,浪费样本配额。

import cv2 import os import time SAVE_DIR = "dataset" PERSON_ID = "u001" # 每个人一个子目录,名字就是标签来源 MAX_NUM = 100 # 每人采集张数 INTERVAL = 0.15 # 采样间隔(秒),避免连续重复帧 SIZE = (200, 200) # 统一尺寸,训练时不再缩放 os.makedirs(os.path.join(SAVE_DIR, PERSON_ID), exist_ok=True) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) count = 0 last_t = 0.0 while count < MAX_NUM: ok, frame = cap.read() if not ok: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # scaleFactor 1.2 比 1.1 快很多,minNeighbors 5 能压掉大部分误检 faces = cascade.detectMultiScale( gray, scaleFactor=1.2, minNeighbors=5, minSize=(80, 80) ) now = time.time() for (x, y, w, h) in faces: if now - last_t < INTERVAL: continue roi = gray[y:y + h, x:x + w] roi = cv2.resize(roi, SIZE, interpolation=cv2.INTER_AREA) cv2.imwrite(os.path.join(SAVE_DIR, PERSON_ID, f"{count:04d}.jpg"), roi) count += 1 last_t = now cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"{count}/{MAX_NUM}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow("collect", frame) if cv2.waitKey(1) & 0xFF == 27: # ESC 提前结束 break cap.release() cv2.destroyAllWindows()

几个参数值得掰开说。INTERVAL = 0.15意味着每秒采 6 到 7 张,100 张大概 15 秒,这个节奏够你缓慢转头、改变表情,又不至于采到一堆重复帧。minSize=(80, 80)是过滤远处小脸,低于这个尺寸裁出来的图糊得没法用,训练进去只会污染直方图。scaleFactor从 1.1 提到 1.2,检测耗时能降三成左右,代价是极小的人脸会漏,固定机位场景完全划算。

cv2.resize用INTER_AREA而不是默认的双线性,缩小图像时它的抗锯齿效果更好,直方图特征更干净。这一行不影响速度,但影响后面识别的稳定性,属于典型的「改了没感觉、不改有差别」的细节。

3.2 样本规格:数量、光照、角度怎么配

采集最容易犯的错是坐在同一个位置、同一个姿势连拍一百张,然后抱怨模型换个角度就不认。LBPH 没有数据增强,样本的多样性完全靠采集阶段补。

维度建议原因
单人张数60~200 张太少直方图不稳,太多训练变慢且边际收益低
光照条件至少 3 种(顺光、侧光、室内灯)灰度直方图对亮度分布极敏感
头部角度正脸、左右各偏 15~30 度侧脸时 Haar 本身就容易漏,样本里得有
表情自然、微笑、闭嘴各来一些嘴部纹理变化会显著影响局部特征
遮挡眼镜戴与不戴分开采镜框边缘是强纹理,混采会让特征发散
背景有变化更好裁出来的 ROI 边缘可能带一点背景纹理

我的习惯是一个人采三轮:第一轮正对镜头把基础样本打底,第二轮左右转头各采二十张,第三轮换个房间再补一批。这样训出来的模型,在现场换个站位也不会立刻失效。

命名规范也得提前定死。用子目录名 = 人员标识、文件名 = 序号.jpg这种结构,训练脚本一读目录就能拿到标签,不需要额外的标注文件。目录名直接用人名或工号都行,但别用中文路径——某些环境下cv2.imread读中文路径会返回None,排查起来很浪费时间。

3.3 采集完先自检,别急着训练

一百张图里混进几张糊的、过曝的、只剩半张脸的,训练时不会报错,只会让识别在特定角度下莫名其妙地失败。采完先跑一遍体检脚本,把异常样本挑出来删掉,比事后调参数有效得多。

import cv2 import os DATA_DIR = "dataset" for person in sorted(os.listdir(DATA_DIR)): person_dir = os.path.join(DATA_DIR, person) if not os.path.isdir(person_dir): continue sizes, brights, blurs = set(), [], [] for fn in os.listdir(person_dir): img = cv2.imread(os.path.join(person_dir, fn), cv2.IMREAD_GRAYSCALE) if img is None: print(f"[读取失败] {person}/{fn}") continue sizes.add(img.shape) brights.append(float(img.mean())) # 平均亮度 blurs.append(cv2.Laplacian(img, cv2.CV_64F).var()) # 清晰度,越大越清楚 print(f"{person}: 数量={len(brights)} 尺寸集合={sizes}") print(f" 亮度 min/max = {min(brights):.1f} / {max(brights):.1f}") print(f" 清晰度 min = {min(blurs):.1f}")

判断标准很直接:尺寸集合必须只有一个元素,出现两个说明混进了没缩放的图;亮度最低值低于 30 基本是欠曝,超过 220 是过曝,这两类图在识别时会表现为「同一个人换个位置就不认」;清晰度低于 20 的图可以直接删,那是严重散焦或者运动模糊。

注意:自检脚本不要写成自动删除。先打印出来人工过一眼,我吃过一次亏——把整组低对比度的样本误删了,而那恰好是现场真实光照条件。

4. 训练 LBPH 识别器:三个参数与一条拒识阈值

数据集就位之后,训练本身只有几行代码,真正花时间的是想清楚三件事:LBPH 的三个参数怎么定、标签怎么和姓名对上、置信度阈值卡在哪。这三件事任何一件没想明白,模型跑起来都能出结果,但结果没法用。

4.1 LBPH 在算什么,三个参数各管什么

LBPH 全称局部二值模式直方图。它的流程是:对每个像素,取它周围一个半径为radius的圆环上的neighbors个采样点,把采样点的灰度跟中心像素比大小,得到一串 0/1 二进制数,这就是一个 LBP 编码;整幅图算完之后,把图像划分成grid_x × grid_y个格子,每个格子单独统计 LBP 编码的直方图,最后把所有格子的直方图拼起来,就是这张人脸的特征向量。识别时比较两张图的直方图距离,距离越小越像。

radius决定采样环的大小,值越大捕捉的纹理尺度越粗。neighbors是环上的采样点数,8 是标准配置。grid_x和grid_y决定分块粒度,分得越细,对局部结构越敏感,但也越容易受姿态变化干扰。

参数常用值调大的效果调小的效果
radius1关注更大尺度纹理,对噪声更宽容抓细节,但对轻微模糊敏感
neighbors8编码更丰富,计算变慢,超过 16 边际收益很低特征变粗,区分度下降
grid_x/grid_y8 / 8局部区分力强,姿态一变容易崩全局性强,不同人之间容易混
图像尺寸200×200细节多,训练慢,样本少时过拟合特征不足,认不准

我一般从radius=1, neighbors=8, grid_x=8, grid_y=8起步,这个组合在 200×200 的输入上是个比较稳的平衡点。如果你的人脸样本尺寸改成了 100×100,grid别超过 6,否则每个格子太小,直方图统计没有意义。

4.2 训练脚本:标签映射必须落盘

训练这段代码不长,但有两个地方特别容易写错:labels的类型和目录遍历的顺序。

import cv2 import os import json import numpy as np DATA_DIR = "dataset" MODEL_PATH = "trainer.yml" LABEL_MAP = "labels.json" SIZE = (200, 200) faces, labels = [], [] name_map = {} # sorted 保证每次遍历顺序一致,否则标签会错位 for idx, person in enumerate(sorted(os.listdir(DATA_DIR))): person_dir = os.path.join(DATA_DIR, person) if not os.path.isdir(person_dir): continue name_map[idx] = person # 整数 ID -> 人员标识 for fn in sorted(os.listdir(person_dir)): img = cv2.imread(os.path.join(person_dir, fn), cv2.IMREAD_GRAYSCALE) if img is None: continue if img.shape != SIZE: img = cv2.resize(img, SIZE) faces.append(img) labels.append(idx) # labels 必须是 int32 的 numpy 数组,传 list 会直接报错 recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8 ) recognizer.train(faces, np.array(labels, dtype=np.int32)) recognizer.save(MODEL_PATH) with open(LABEL_MAP, "w", encoding="utf-8") as f: json.dump(name_map, f, ensure_ascii=False, indent=2) print(f"训练完成:{len(set(labels))} 人 / {len(faces)} 张样本") print("标签映射:", name_map)

sorted(os.listdir(...))这两处是重点。os.listdir返回的顺序由文件系统决定,同一台机器上删除再新建一个目录,顺序就可能变。训练时标签是 0、1、2,识别时如果按另一套顺序去解释,人会整体错位,而且不报错。

np.array(labels, dtype=np.int32)里的dtype别省。train()对标签类型有要求,传 float 或者 Python list 在某些版本上会抛类型错误,报错信息还不直观。

标签映射写进 JSON 之后,识别端就只依赖这一个文件,换机器、改目录名都不影响。这是我在这类项目里坚持的一个习惯:凡是跨进程传递的对应关系,一律落盘,不放在代码里。

4.3 置信度不是百分比,阈值要自己扫

recognizer.predict()返回两个值:标签和 confidence。这里有个巨大的认知陷阱——confidence 不是「相似度百分比」,它是直方图距离,越小表示越像,0 是完全一致。很多人看到 confidence 是 30 就以为「相似度 30%」,逻辑整个反了。

因此判断「是不是这个人」不能靠 confidence 大小直接下结论,得先定一个阈值:超过阈值判为未知人。阈值定多少,取决于你的样本质量,常见范围在 50 到 80 之间,需要拿数据扫出来。

import cv2 import json import os recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("trainer.yml") with open("labels.json", encoding="utf-8") as f: name_map = {int(k): v for k, v in json.load(f).items()} def predict_with_threshold(gray_face, threshold=70.0): label, confidence = recognizer.predict(gray_face) if confidence <= threshold: return name_map.get(label, "unknown"), confidence return "unknown", confidence

阈值扫描的做法是:从数据集里留出每人 10 张没参与训练的图,统一跑一遍,统计「同人样本的 confidence 分布」和「异人样本的 confidence 分布」。两条分布的交叠区域就是阈值该落的位置。如果交叠区很宽,说明样本区分度不够,回头补样本比调阈值有效。

提示:阈值调低(更严格)能减少误认,但会把本人挡在门外;调高则相反。考勤这种场景宁可严格,宁可让人多刷一次,也别把 A 记成 B。

5. 避坑与排查:五个把我卡到凌晨的现场问题

这一章是我自己踩过、也帮别人排查过的真实故障类型,按现象、原因、解决三段写。这些问题有个共同点:程序不崩,只是结果不对,所以特别耗时间。

5.1 依赖与环境的坑

第一条:AttributeError: module 'cv2' has no attribute 'face'

现象很明确,训练脚本跑到cv2.face.LBPHFaceRecognizer_create()就挂。多数人第一反应是「版本太老」,于是升级 OpenCV,升完还是报同样的错。

原因基本是两个:装的是opencv-python而不是opencv-contrib-python;或者两个包都装过,后装的把先装的覆盖了,cv2目录里只剩主包的内容。这个错误最坑的地方在于cv2.imshow、cv2.VideoCapture全都正常,你会误以为环境没问题。

解决方式是彻底卸载再单独装 contrib:

pip uninstall -y opencv-python opencv-python-headless opencv-contrib-python pip install opencv-contrib-python python -c "import cv2; print(cv2.__file__); print(hasattr(cv2,'face'))"

如果第二条输出True就对了。输出False,去看cv2.__file__打印的路径,那个目录下如果没有cv2的.so文件而是一堆.py,说明环境里有残留,直接删掉整个site-packages/cv2目录重装。

5.2 数据与训练的坑

第二条:训练时报标签类型错误,或者train()参数不匹配

现象是recognizer.train(faces, labels)抛出类型相关的异常,换成 numpy 数组之后又提示尺寸不一致。

原因有两层。一是labels传了 Python list 或 float 数组,LBPH 要求整型数组;二是faces里的图像尺寸不统一,采集时缩放过了,但中间手动补进去几张原图,混在一起训练就会崩。

解决办法是把两件事都做死:训练前统一resize,标签统一转int32。前面 4.2 的脚本里两处都做了。额外加一句断言更保险:

assert all(f.shape == SIZE for f in faces), "存在尺寸不一致的样本" assert len(faces) == len(labels), "图像与标签数量不匹配"

第三条:识别永远返回同一个人,或者所有人被认成 ID 0

现象是模型能加载,预测也返回值,但不管谁站到镜头前,输出的都是同一个名字。

原因是标签映射错位。训练时用os.listdir的自然顺序分配 0、1、2,识别时又用另一次os.listdir的结果去反查姓名,两次顺序不一致,模型输出的 0 就被解释成了另一个人。或者更常见的:训练脚本里enumerate的起点被改过,从 1 开始编号,而识别端默认从 0 开始。

解决办法是标签映射必须落盘、必须用sorted、识别端只读 JSON 不自己推。如果已经训好了模型但映射丢了,可以重新训一次,成本也就几十秒,比反推映射快得多。

5.3 运行时的坑

第四条:实时画面卡成幻灯片,帧率掉到个位数

现象是采集阶段挺流畅,识别阶段一开摄像头,画面一秒跳两三下,鼠标都拖不动。

原因是每一帧都在原分辨率上跑完整检测加识别。640×480 的帧跑一次 Haar 检测大概几十毫秒,加上 LBPH 预测和多脸循环,一帧轻松超过 200 毫秒。如果换成 DNN,情况更糟。

解决办法是降采样加隔帧检测。检测前把帧缩到一半甚至三分之一,坐标再按比例放大回去;检测不必每帧都做,每 3 帧做一次,中间帧沿用上一次的框。人脸在画面里不会一帧移动半个屏幕,这个近似完全够用。另外detectMultiScale的scaleFactor从 1.1 调到 1.2 或 1.3,耗时能再降一截。

第五条:confidence 只有二十几,还是认错人

现象是阈值设了 70,预测出来的 confidence 是 25,按理说非常像,但名字是错的。

原因是把 LBPH 的 confidence 当成了绝对可信度。它本质是直方图距离,会受到样本数量、图像尺寸、grid 划分的多重影响。样本少的人,直方图稀疏,跟谁都容易算出小距离;某个人的样本光照特别统一,也会让他的类内距离异常小,把别人吸过来。

解决办法是不要迷信单一阈值,做两件事:一是保证每人样本量大致均衡,别出现一个人 200 张、另一个 30 张;二是用留出集统计每个 ID 的类内距离分布,按人设定阈值,而不是全局一个数。工程上更稳的做法是取 Top-2 结果,如果第一名和第二名的距离差小于某个 margin,直接判未知,不硬给结论。

注意:第五条这类问题在现场排查时最容易被误判成「模型不行」,实际上是数据分布不均。先把每个 ID 的样本量和平均类内距离打出来看一眼,往往一眼就能定位。

6. 实时识别收尾:多脸降采样与一套能自证的验证流程

识别脚本的骨架跟采集脚本差不多,差别在于每帧要做检测加预测两件事,还要处理多个脸、画框、写日志。下面这段是隔帧检测的写法,实测在普通笔记本上能把帧率从 5fps 拉回 20fps 以上。

import cv2 import json recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("trainer.yml") with open("labels.json", encoding="utf-8") as f: name_map = {int(k): v for k, v in json.load(f).items()} cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) THRESHOLD = 70.0 SCALE = 0.5 # 检测降采样比例 SKIP = 3 # 每 3 帧检测一次 cap = cv2.VideoCapture(0) frame_id, boxes = 0, [] while True: ok, frame = cap.read() if not ok: break frame_id += 1 if frame_id % SKIP == 0: small = cv2.resize(frame, (0, 0), fx=SCALE, fy=SCALE) gray_small = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) raw = cascade.detectMultiScale(gray_small, 1.2, 5, minSize=(40, 40)) # 坐标还原回原图尺度 boxes = [tuple(int(v / SCALE) for v in (x, y, w, h)) for (x, y, w, h) in raw] gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) for (x, y, w, h) in boxes: roi = cv2.resize(gray[y:y + h, x:x + w], (200, 200)) label, conf = recognizer.predict(roi) name = name_map.get(label, "unknown") if conf <= THRESHOLD else "unknown" color = (0, 255, 0) if name != "unknown" else (0, 0, 255) cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2) cv2.putText(frame, f"{name} {conf:.0f}", (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imshow("recognize", frame) if cv2.waitKey(1) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()

坐标还原那一行是新加的坑点:检测在缩放图上做,框的坐标是缩放图的坐标系,直接拿去切原图 ROI 会整体偏移。除以SCALE再取整,误差在一两个像素内,肉眼看不出。SKIP设成 3 是个经验值,人在画面里移动慢的话可以设 5,检测开销再降一半。

模型到底行不行,不能靠站在摄像头前反复试。一套能自证的验证流程是这样的:从采集数据里每人随机留出 20% 不参与训练,作为测试集;对这些图逐张跑预测,记录真实标签、预测标签、confidence;最后算准确率和拒识率。

指标计算方式关注点
识别准确率正确识别数 / 测试总数整体水平,低于 85% 说明样本或参数有问题
误认率认错人的次数 / 测试总数最不能接受的错误,考勤场景要压到 1% 以下
拒识率判为 unknown 的次数 / 测试总数太高说明阈值过严或样本不足
类内平均距离同人样本的 confidence 均值判断某个人的样本一致性

我一般会先跑这个测试集,把误认的那几张图单独挑出来看。十次里有八次,问题出在样本上:那张图本身是侧脸,而训练集里没有类似角度。这时候补样本比调任何参数都快。

从那以后我每次交付这类识别项目,都强制走一遍「留出集 + 混淆矩阵 + 阈值扫描」三步,不管现场演示多顺利。演示是骗人的,留出集不会。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:55:01

MFC实战工程集:从VC6到VS2019的可调试对话框与CListCtrl深度开发

简介&#xff1a;本资源是《精通MFC》配套光盘源代码&#xff0c;面向Windows桌面开发初学者与中级C程序员&#xff0c;系统解决MFC框架下窗口编程、消息机制、文档/视图架构、GDI/GDI绘图、多线程、DLL及COM组件等核心难点。压缩包含1114个文件&#xff0c;总计8.01MB&#xf…

作者头像 李华
网站建设 2026/11/6 3:53:20

UDP组播IPC自动探测:WS-Discovery原理与IGMP避坑

简介&#xff1a;面向从事视频监控系统集成、网络通信与设备对接开发的工程师&#xff0c;这份C示例工程围绕UDP组播实现海康网络摄像机&#xff08;IPC&#xff09;自动探测&#xff0c;并衔接ONVIF设备发现与后续管理流程。工程以MFC对话框程序组织&#xff0c;代码涉及组播套…

作者头像 李华
网站建设 2026/11/6 3:53:01

Docker容器化一键部署K8s集群:可复现、可验证、可交付

简介&#xff1a;这是一份面向DevOps工程师、Kubernetes初学者及运维人员的自动化部署工具包&#xff0c;聚焦容器化K8s集群的一键式快速搭建&#xff0c;显著降低手动配置Docker、cri-dockerd、kubeadm等组件的复杂度与出错风险。资源压缩包为ZIP格式&#xff0c;共7个文件&am…

作者头像 李华
网站建设 2026/11/6 3:52:25

GLM-4源码仓库实战:本地推理、OpenAI兼容服务与LoRA微调

简介&#xff1a;这份GLM-4代码仓库源码压缩包面向希望研读并二次开发GLM-4的开发者、学生与算法工程师&#xff0c;提供从基础调用到微调、多模态交互及服务化部署的参考实现&#xff0c;可用于课程实验、原型验证与工程迁移。包内共78个文件&#xff0c;以Python脚本、Markdo…

作者头像 李华
网站建设 2026/11/6 3:51:35

哈哈哈哈今年

BBvv吧不女厕电饭锅BBvv吧

作者头像 李华